Untangle your servlet code with reflection

Simplify servlet structure by breaking down functionality into separate methods

Despite the elegance of the Servlet API, real-world servlet classes tend to have pages of code in their doGet() and doPost() methods, often with complex conditional logic. Over time, more code must be added in response to evolving business needs. Long methods with complex branching result in a major headache for the unlucky developer who must maintain that code. Good programming style suggests that such branching should be avoided.

This article presents a technique to modularize the functionality of a complex servlet using reflection, thereby simplifying the servlet structure. Specifically, the CTLServlet base class parses the request URL to determine the name of an appropriate handler, finds a method by that name in the implementation class, and then invokes the method to handle the request. That makes it easy to break down servlet functionality into separate methods, and thus avoid very long method bodies within doGet() and doPost().

As I will describe in more detail, this technique has several other important advantages. It replaces long conditional dispatching code with a simple lookup. It encourages a disciplined relationship between URLs and handler methods, which makes it easier to analyze and extend servlet behavior. It is very lightweight, and thus imposes little burden upon developers. And it readily supports a number of interesting extensions, such as trace functionality for debugging, and a more object-oriented style.

About reflection

The Reflection API provides a way to discover what an object can do at runtime and to execute the discovered capabilities. As Michael T. Portwood, a presenter at JavaOne, put it, reflection can make applications more "simple, flexible, and pluggable" without sacrificing performance or increasing overall program complexity. The approach used in this article parallels a technique described in that JavaOne presentation (see Resources). This technique uses reflection to implement a generic ActionListener in Swing or AWT code (see Resources). The reflection-based code is much shorter and simpler than its conditional alternative.

For example, the conventional conditional approach compares the ActionEvent's command to a set of known strings and then branches to an appropriate handler method, like this:

public void actionPerformed(ActionEvent ev) {
   String command = ev.getActionCommand();
   if (command.equals("start") {
      start();
   }
   else if (command.equals("stop")) {
      stop();
   }
   else if (command.equals("another")) {
     another();
   }
}

Notice that you dispatch to a method that matches the name of the action command in the ActionEvent object. Reflection can capture this dispatching pattern, as follows:

public void actionPerformed(ActionEvent ev) {
  try {
         Method m = getClass().getMethod(getActionCommand(), new Class[0]);
         m.invoke(this,new Object[0]);
  }
  catch (Exception ex) {
  
  }
}

Now you can easily add new command handlers, simply by adding new zero-argument methods to your class -- without ever updating the dispatching code.

The extension of this idea to servlets is pretty straightforward, although surprisingly powerful. Instead of finding the name of a command in an ActionEvent object, you find the name in a component of the request URL. Instead of calling a zero-argument method with that name, you call a method that takes two arguments, one of type HttpServletRequest and the other of HttpServletResponse. With that approach, you can break up the pages of code in your doGet() and doPost() methods into many smaller well-named methods; eliminate complex dispatching code; and make it much easier to add new functionality to an existing servlet.

The part of the Reflection API you need to understand to implement this approach is pretty concise. You already know that all Java objects are instances of a class. Reflection introduces a special class called Class to represent an instance of class. You can get a Class object by calling instance.getClass(). Suppose you have an object, o, that is an instance of a servlet. You can get its class as:

o.getClass();

The Class class has a number of methods to return information about a class, such as its name, its fields, and, of course, its methods. The latter are represented by a Method object, which contains information about a method. You can get a particular method of a class by calling getMethod() with an array of Classes, one for each formal argument to the method. Support your o object with a doSomething(String s) method. You can get this method by calling:

o.getClass().getMethod("doSomething",new Class[] {String.class});

Once you have the method, you can invoke it by calling the Method object's invoke method. For example, you can invoke doSomething on o with the argument "hello" by calling:

o.getClass().getMethod("doSomething",new Class[] {String.class}).
   invoke(o,new Object[] {"hello"});

Except for some exception-handling details, that is all you need to know about reflection for this article.

Model-view-controller architecture

I will elaborate the use of reflection within the model-view-controller (MVC) architecture, which is recommended as a server-side Java architecture. Detailed coverage of MVC's application to servlets can be found in Resources. Important to this discussion is organizing your request-processing code into three categories:

  1. State information (models) will be coded as JavaBeans and stored in the user's session.
  2. Code to process the request and update the state (controllers) will be written as methods of the servlet class.
  3. Code to format the response and return it to the client (views) will be written as JavaServer Pages (JSPs), which render the updated state into HTML.

Therefore, methods that handle specific requests will hereafter be called controllers. Later in the article, you will see that you can extend the reflection concept into a more object-oriented design that leverages the ability of state information to be stored as beans in the user's session.

Disciplining URLs

In addition to MVC, another important idea is that request URLs to a servlet should follow a disciplined pattern. This is not a new idea: the base URL for your servlet already follows a pattern, either http://yourserver.com/servlet/YourServlet or a more convenient alias like http://yourserver.com/YourServlet. But then you are free to improvise, either by adding extra path information or request parameters. Extra path information is any text after the servlet name but before the "?" that marks the beginning of the request parameters. The request parameters are the name-value pairs that follow the "?" symbol in the URL. For example, in the URL "/YourServlet/extra/path/information?p1=1&p2=2" the extra path information would be "/extra/path/information" and the request parameters are "p1" and "p2."

As more functionality is added to the servlet, there is a tendency to use an increasing number of arbitrarily named parameters in combination with path information to specify servlet functions. You should impose some discipline on this chaos. Indeed for a more automated approach to dispatching, it is essential to settle on some additional conventions.

In this article, I'll require the first piece of extra path information to be the name of the major function being called and allow the parameters to continue to vary arbitrarily. Hence, the URL "/Calculator/add?x=1&y=2" would invoke the add function of the Calculator servlet, whereas "/Calculator/subtract?x=1&y=2" would invoke the subtract function. (An equally workable alternative is to use a specially named parameter, such as "fn" to contain the function name, in URLs such as "/Calculator?fn=add&x=1&y=2" or "/Calculator?fn=subtract&x=1&y=2."

Dispatching via reflection using CTLServlet

Now I'll introduce your base class, CTLServlet. This base class will implement doGet() and doPost(); deployable servlets will be subclasses that add controller methods. The CTLServlet class is abstract, because concrete subclasses will have to implement at least two methods. The first, handleRequest(), will be called by default, if a URL invokes the servlet without specifying a particular controller. The second, handleControllerNotFound(), will be invoked if a URL invokes the servlet and specifies a particular controller, but that controller cannot be found. In addition, concrete subclasses will add one or more controller methods.

abstract public class CTLServlet extends HttpServlet {
      public void doGet(HttpServletRequest request, HttpServletResponse response) {
            doGetOrPost(request, response);
      }
      public void doPost(HttpServletRequest request, HttpServletResponse response) {
            doGetOrPost(request,response);
      }

You can handle HTTP GET and HTTP POST requests identically, each implementation delegating to a single doGetOrPost() method. This method dispatches the request to a controller. The controller is found by extracting a method name from the request. If no controller name is found, a default method, handleRequest, is called. To make sure the concrete class has such a method with the right signature, you declare it as abstract. You also declare the abstract method handleControllerNotFound(), which is called if the request specifies a controller but a corresponding method doesn't exist.

You will also note that doGetOrPost() adds a servletPath attribute to the request. This is because JSPs often need this information to correctly compute URLs to call back to the servlet. It also illustrates a general point about this approach. Even though your functionality will be modularized into different methods, every call through your servlet goes through a single "bottleneck" method. This method, doGetOrPost, can hold the common code that the controller needs.

      private void doGetOrPost(HttpServletRequest request, HttpServletResponse response) {
            try {
                  request.setAttribute("servletPath",request.getServletPath());
                  dispatch( getController(this,getMethodName(request)),
                          this,
                          request,
                          response);
            }
            catch (ControllerNotFoundException ex) {
                  handleControllerNotFound(request, response);
            }     
        }
        abstract protected void handleControllerNotFound(HttpServletRequest request, HttpServletResponse response);
  
        abstract public void handleRequest(HttpServletRequest request, HttpServletResponse response);

The dispatch() method simply invokes the method on the target object. It throws a ControllerNotFoundException should anything go wrong.

      protected void dispatch(Method m, 
                                          Object target, 
                                          HttpServletRequest request, 
                                          HttpServletResponse response) 
                                    throws ControllerNotFoundException {
          try {
            m.invoke(target,new Object[] {request,response});
          } catch (IllegalAccessException ex) {
            throw new ControllerNotFoundException("couldn't access method");
      } catch (InvocationTargetException ex) {
              throw new ControllerNotFoundException("object doesn't have method");
      }
      }

The getController() method finds a named controller using reflection. If a method name is not supplied in the URL, the handleRequest() method is returned. This abstract method must be implemented in the concrete servlet and should provide appropriate default behavior (for example, showing a home page).

      static final Class[] sFormalArgs = {HttpServletRequest.class,HttpServletResponse.class};
      protected Method getController(Object target, String methodName) 
                  throws ControllerNotFoundException {
            try {
                  return target.getClass().getMethod(methodName,sFormalArgs);
            }
            catch(NoSuchMethodException ex) {
                  throw new ControllerNotFoundException("couldn't get method");
            }
      }
   }

The last method of CTLServlet, getMethodName(), parses the extra path information in a request URL to find a method name. You do this by using a StringTokenizer and finding the first token of the extra path information, assuming that "/" delimits tokens:

  protected String getMethodName(HttpServletRequest req) {
      String defaultMethod = "handleRequest";
      if (req.getPathInfo() == null) return defaultMethod;
       
       
      StringTokenizer tokens = new StringTokenizer(req.getPathInfo(),"/");
      if (tokens.hasMoreTokens()) {
          return tokens.nextToken();
      }     
      else return defaultMethod;
  }

Finally you define your own class of exception, which is used internally to collapse all the possible reflection exceptions into one; you shouldn't care why reflection failed, and you can report in all cases "controller not found." The exception text will contain the detailed reason for the failure:

  class ControllerNotFoundException extends ServletException {
  
            ControllerNotFoundException(String reason) {
                  super(reason);
            }
             
  }
1 2 3 Page 1