Book Home Java Servlet Programming Search this book

11.2. Servlet Reuse

Another use for interservlet communication is to allow one servlet to reuse the abilities (the public methods) of another servlet. The major challenge with servlet reuse is for the "user" servlet to obtain the proper instance of "usee" servlet when the usee servlet has not yet been loaded into the server.

The obvious solutions don't always work. getServlet() isn't guaranteed to load the named servlet on all servers; it may just return null. Directly creating a new instance of the usee servlet doesn't work either, as the newly created servlet doesn't have access to its own ServletConfig and ServletContext objects. Plus, the server would be using a different instance to handle client requests, leaving the new instance of the servlet without the right state information.

The solution is for the user servlet to ask the server to load the usee servlet, then call getServlet() to get a reference to it. Unfortunately, the Servlet API distinctly lacks any methods whereby a servlet can control the servlet life cycle, for itself or for other servlets. This is considered a security risk and is officially "left for future consideration."

Fortunately, there's a back door we can use today. A servlet can open an HTTP connection to the server in which it's running, ask for the unloaded servlet, and effectively force the server to load the servlet to handle the request. Then a call to getServlet() gets the proper instance.[2]

[2] Unfortunately, this technique does not work directly for servlets running within a secure web server because a secure server accepts only encrypted HTTPS connections.

11.2.1. An Improved getServlet()

The com.oreilly.servlet.ServletUtils class has an improved getServlet() method that does just this. It returns the named servlet, loading it first via an HTTP request if necessary. The code is shown in Example 11-3.

Example 11-3. The code for an improved getServlet()

// Get the named servlet. Try loading it through an HTTP request if
// necessary. Returns null if there's a problem. Only loads HTTP
// servlets, of course.
public static Servlet getServlet(String name,
                                 ServletRequest req,
                                 ServletContext context) {
  try {
    // Try getting the servlet the old-fashioned way
    Servlet servlet = context.getServlet(name);
    if (servlet != null) return servlet;

    // If getServlet() returned null, we have to load it ourselves.
    // Do this by making an HTTP GET request to the servlet.
    // Use a raw socket connection so we can set a timeout.
    Socket socket = new Socket(req.getServerName(), req.getServerPort());
    socket.setSoTimeout(4000);  // wait up to 4 secs for a response
    PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
    out.println("GET /servlet/" + name + " HTTP/1.0");  // the request
    out.println();
    try {
      socket.getInputStream().read();  // Even one byte means its loaded
    }
    catch (InterruptedIOException e) { /* timeout: ignore, hope for the best */ }
    out.close();

    // Try getting the servlet again.
    return context.getServlet(name);
  }
  catch (Exception e) {
    // If there's any problem, return null.
    return null;
  }
}

This getServlet() method uses a raw socket connection to perform the HTTP GET request. This is so that it can set a time-out for how long it's willing to wait for a response. The URL and URLConnection classes don't provide this ability. In this case, the time-out is set to four seconds. If, after four seconds, the servlet hasn't written any response, the read() method throws an InterruptedIOException and the method continues. This time-out is necessary only for the special case where a servlet spends a long time preparing its response and we don't want to wait. It would appear this time-out could leave the loading servlet in an uninitialized state, if its init() method were to take five seconds, for example. A well-written server, however, should block in the getServlet() call until the servlet has been fully initialized. Note that because this ServletUtils.getServlet() method requires a ServletRequest parameter, it can be called only by methods with access to a ServletRequest, such as doGet() and doPost().

11.2.2. Reusing ChatServlet

An HTML-based chat servlet built around the abilities of last chapter's ChatServlet is an excellent example of servlet reuse. This new servlet, called ChatPage, wraps an HTML interface around the getNextMessage() and broadcastMessage() methods of ChatServlet. The code is shown in Example 11-4, while the output is shown in Figure 11-3.

Example 11-4. One servlet, ChatPage, reusing another servlet, ChatServlet

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.ServletUtils;

public class ChatPage extends HttpServlet implements Runnable {

  static final int MESSAGE_ARCHIVE_SIZE = 10;  // save the last 10 messages

  ChatServlet chat = null;  // the servlet to reuse
  String[] messages = new String[MESSAGE_ARCHIVE_SIZE];  // circular array
  int messageIndex = 0;  // index into the messages array
  Thread update = null;  // thread to update new messages

  // Gets new messages from the chat servlet and inserts them in 
  // the messages' circular array.
  public void run() {
    while (true) {
      String message = chat.getNextMessage();
      synchronized (this) {
        messages[messageIndex] = message;
        messageIndex = (messageIndex + 1) % MESSAGE_ARCHIVE_SIZE;
      }
    }
  }

  // Prints the message archive (the 10 latest messages) and a text 
  // field where the reader can input a new message.
  public void doGet(HttpServletRequest req, HttpServletResponse res)
                               throws ServletException, IOException {
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();

    // Turn off caching, so the latest messages are always displayed.
    // (Works around a Netscape problem.)
    res.setHeader("Pragma", "no-cache");

    // For our first request, "chat" is null and we need to use
    // ServletUtils.getServlet() to get the ChatServlet instance.
    // Then we need to start another thread to listen for chat's
    // new messages.
    if (chat == null) {
      chat = (ChatServlet)ServletUtils.getServlet(
                 "ChatServlet", req, getServletContext());
      if (chat != null) {
        update = new Thread(this);
        update.start();
      }
    }

    // Print a pretty header.
    out.println("<HTML><HEAD>");
    out.println("<TITLE>ChatPage</TITLE>");
    out.println("</HEAD><BODY>");
    out.println("<CENTER><H1>Welcome to ChatPage!</H1></CENTER>");

    // Print the message archive, oldest first.
    // Synchronized so it doesn't change while we're printing it.
    synchronized (this) {
      out.println("<FONT SIZE=4>Recent messages:</FONT><P>");
      int i = messageIndex;
      do {
        String message = messages[i];
        if (message != null) out.println(message + "<P>");
        i = (i + 1) % MESSAGE_ARCHIVE_SIZE;
      } while (i != messageIndex);
    }

    // Print a button that gets new messages.
    out.println("<FORM METHOD=GET>");
    out.println("<INPUT TYPE=submit VALUE=\"Get New Messages\">");
    out.println("</FORM>");

    // Print a form where the reader can submit a new message.
    out.println("<HR>");
    out.println("<FORM METHOD=POST>");
    out.println("<FONT SIZE=4>Submit a message:</FONT>");
    out.println("<INPUT TYPE=text NAME=message>");
    out.println("</FORM>");

    // Print a pretty footer.
    out.println("<HR>");
    out.println("<CENTER><FONT SIZE=2><B>");
    out.println("Special thanks to ChatServlet for acting as our back-end");
    out.println("</B></FONT></CENTER>");
    out.println("</BODY></HTML>");
  }

  // Accepts messages for broadcast.
  public void doPost(HttpServletRequest req, HttpServletResponse res)
                                throws ServletException, IOException {
    // If our first request happens to be a POST, we need to set "chat"
    // and start our update thread just as we do for a GET request.
    if (chat == null) {
      chat = (ChatServlet)ServletUtils.getServlet(
                 "ChatServlet", req, getServletContext());
      if (chat != null) {
        update = new Thread(this);
        update.start();
        Thread.currentThread().yield();  // let the run() method start
      }
    }

    // Get the client's username. It's non-null only if ChatPage is
    // protected by client authentication.
    String user = req.getRemoteUser();
    if (user == null) user = "anonymous";

    // Get and broadcast the message.
    String message = req.getParameter("message");
    if (message != null && chat != null) {
      chat.broadcastMessage(user + ": " + message);
      Thread.currentThread().yield();  // let the message be broadcast
    }

    // Have doGet() print the updated message archive and the form.
    doGet(req, res);
  }

  // Stops the background thread. 
  public void destroy() {
    if (update != null)
      update.stop();
  }

  public String getServletInfo() {
    return "An HTML chat server front end, reusing ChatServlet";
  }
}
figure

Figure 11-3. Another interface to ChatServlet

The core logic for running the chat service remains in ChatServlet. ChatPage just uses the public methods of ChatServlet to present an alternative front end to the user. ChatPage gains access to the server's ChatServlet instance with the following line of code:

chat = (ChatServlet)ServletUtils.getServlet(
           "ChatServlet", req, getServletContext());

Remember that this cast can throw a ClassCastException if either ChatServlet or ChatPage was ever reloaded. To avoid this, put the class file for ChatServlet in the server's classpath. This ensures that ChatServlet isn't reloaded. (And what if ChatPage is reloaded? That won't be a problem as long as ChatServlet was loaded by the primordial class loader.) Not allowing ChatServlet to reload also guarantees that the background update thread of ChatPage won't find itself calling an old version of ChatServlet.

ChatPage uses the returned ChatServlet instance for its back end. It calls chat.getNextMessage() to fill its array of recent messages and chat.broadcastMessage() to broadcast each new message as it's entered by the user.

As often happens with servlet reuse, not everything fits together elegantly in this example. ChatServlet wasn't intended to be used by another servlet,[3] so ChatPage requires some extra code to work around some issues that could have been solved with a better back-end design. Specifically, the doPost() method has two points where the current thread yields to allow the update thread to proceed with its work. First, doPost() calls yield() after starting the update thread. This gives the new thread a chance to start listening for chat messages. Second, doPost() calls yield() after broadcasting its message. This gives the update thread a chance to receive the broadcasted message. Without these yields, the thread calling doPost() may broadcast the message before the update thread is able to receive the message, resulting in a response that doesn't include the latest message. (And even with the yields, it's possible this could happen anyway due to unfortunate thread scheduling.)

[3] Honest! The examples from this chapter were dreamed up only after Chapter 10, "Applet-Servlet Communication" had been written.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.