Comparing RequestInterceptor and Valve Technology

$Id: filters.html,v 1.1 2000/08/11 05:21:04 craigmcc Exp $

[Introduction] [Basic Technology] [Performance Differences] [Functionality Differences] [Implementation Differences]


1. INTRODUCTION

One of the key differences between the Catalina architecture and the current architecture of Tomcat 3.2 is the mechanism by which plug-in functionality can be added (at run time) to intercept, modify, and possibly respond to, individual requests. In Tomcat 3.2, this approach is called RequestInterceptor, named after the fundamental Java interface that such a component must implement. The corresponding Java interface in Catalina is called Valve.

The purpose of this paper is to compare the two technologies, focusing on differences in runtime performance that are solely due to the chosen technique for request filtering (the term that will be used for comments applicable to both techniques), and the range of possible functionality that can be implemented using each technique.

The fundamental purpose of request filtering is to allow the addition, within the servlet container, to the normal functionality that processes incoming requests and produces the corresponding responses. Filters can be used to implement standard functionality of a servlet container (such as the rules by which a particular servlet is mapped from the request URI), or for adding customized functionality that is only required under certain circumstances. In general, accomplishing these goals requires a technique that supports the following capabilities:



2. BASIC TECHNOLOGY

In order to evaluate the desireability of either of these approaches to providing request filtering in a servlet container, we need to undestand a little more about the details of each approach. This section outlines the fundamental operation of request interceptors and valves; however, the reader is referred to the source code (particularly to the request interceptors and valves that have already been implemented) for gaining an in depth understanding.

2.1 RequestInterceptor Basics

The RequestInterceptor technique is modelled after the approach commonly implemented in web servers for extending the functionality of the server (modules in Apache, NSAPI in iPlanet, and ISAPI in Microsoft IIS). In such environments, the entire processing cycle for a request is divided into "phases", and a particular extension can choose to participate in whichever request processing phases it wants to.

An individual RequestInterceptor must implement the Java interface of the same name (org.apache.tomcat.core.RequestInterceptor), which has the following API contract:

/**
 * Provide a mechanism to customize the request processing.
 *
 * @author costin@dnt.ro
 */
public interface RequestInterceptor {

    public static final int OK=0;

    /** Will detect the context path for a request.
     *  It need to set: context, contextPath, lookupPath
     *
     *  A possible use for this would be a "user-home" interceptor
     *  that will implement ~costin servlets ( add and map them at run time).
     */
    public int contextMap(Request request);

    
    /** Handle mappings inside a context.
     *  You are required to respect the mappings in web.xml.
     */
    public int requestMap(Request request);

    
    /** 
     *  This callback is used to extract and verify the user identity
     *  and credentials.
     *
     *  It will set the RemoteUser field if it can authenticate.
     *  The auth event is generated by a user asking for the remote
     *  user field of by tomcat if a request requires authenticated
     *  id.
     */
    public int authenticate(Request request, Response response);


    /**
     *  Will check if the current ( authenticated ) user is authorized
     *  to access a resource, by checking if it have one of the
     *  required roles.
     *
     *  This is used by tomcat to delegate the authorization to modules.
     *  The authorize is called by isUserInRole() and by ContextManager
     *  if the request have security constraints.
     *
     *  @returns 0 if the module can't take a decision
     *           401 If the user is not authorized ( doesn't have
     *               any of the required roles )
     *           200 If the user have the right roles. No further module
     *               will be called.
     */
    public int authorize(Request request, Response response,
			 String reqRoles[]);


    /** Called before service method is invoked. 
     */
    public int preService(Request request, Response response);

    /** New Session notification - called when the servlet
	asks for a new session. You can do all kind of stuff with
	this notification - the most important is create a session
	object. This will be the base for controling the
	session allocation.
    */
    public int newSessionRequest( Request request, Response response);

    /** Called before the first body write, and before sending
     *  the headers. The interceptor have a chance to change the
     *  output headers. 
     */
    public int beforeBody( Request request, Response response);

    
    /** Called before the output buffer is commited.
     */
    public int beforeCommit( Request request, Response response);

    
    /** Called after the output stream is closed ( either by servlet
     *  or automatically at end of service ).
     */
    public int afterBody( Request request, Response response);

    
    /** Called after service method ends. Log is a particular use.
     */
    public int postService(Request request, Response response);

    /** Will return the methods fow which this interceptor is interested
     *  in notification.
     *  This will be used by ContextManager to call only the interceptors
     *  that are interested, avoiding empty calls.
     *  ( not implemented yet ). 
     */
    public String[] getMethods();
}

As you can see, a number of phases (currently ten) are defined. The method for each phase is required to return an integer value that (theoretically) determines whether or not subsequent request interceptors should be given a chance to process this request (zero return value), or whether further processing for this phase should be skipped (non-zero return value). Note that there is no defined mechanism to skip all further processing of future phases.

A convenience base class (org.apache.tomcat.core.BaseInterceptor) is included in Tomcat, which includes a phase handling method that simply returns zero. Thus, the developer of a request interceptor need only extend the BaseInterceptor class, and implement methods for the phases he or she desires to process.

Within the core portion of the Tomcat 3.2 servlet container, the org.apache.tomcat.core.Handler that is processing this request calls a method within org.apache.tomcat.core.ContextManager to call all the request interceptors that are relevant for that phase. For example, immediately after the service() method of the application servlet has been called, Handler executes the following code:

    if( ! internal )
        contextM.doPostService( req, res );

which causes the doPostService() method of ContextManager to be called. This method executes:

    int doPostService( Request req, Response res ) {
        RequestInterceptor reqI[]= getRequestInterceptors(req);
        for( int i=0; i< reqI.length; i++ ) {
            reqI[i].postService( req, res );
        }
        return 0;
    }

to pass this request and response to all of the defined request interceptors. A similar pattern calls all of the request interceptors for the other phases at the appropriate times as well.

2.2 Valve Basics

The Valve technique uses a paradigm for request and response filtering or wrapping that is commonly seen inside operating systems and transaction processing systems. Within the servlet world, you will see similar approaches taken in the Paperclips servlet container (written by Nic Ferrier), and (in a limited form) in those servlet containers that implement servlet chaining. It is an integral part of the architecture that was going to be Apache JServ 2.0 (which was suspended when Sun's contribution of JSWDK to the Apache Software Foundation was announced in June 1999), and is present in essentially the same form in the Catalina architecture.

Like a request interceptor, a Valve must implement a particular Java interface (org.apache.tomcat.Valve) with the following API:

/**
 * A Valve is a request processing component associated with a
 * particular Container.  A series of Valves may be associated with the
 * same Container, creating a request processing pipeline.
 * 
 * HISTORICAL NOTE:  The "Valve" name was assigned to this concept
 * because a valve is what you use in a real world pipeline to control and/or
 * modify flows through it.
 *
 * @author Craig R. McClanahan
 * @author Gunnar Rjnning
 * @version $Revision: 1.1 $ $Date: 2000/08/11 05:21:04 $
 */

public interface Valve {


    //-------------------------------------------------------------- Properties


    /**
     * Return the Container with which this Valve is associated, if any.
     */
    public Container getContainer();


    /**
     * Set the Container with which this Valve is associated.
     *
     * @param container The newly associated Container
     *
     * @exception IllegalArgumentException if this Valve refused to be
     *  associated with the specified Container
     * @exception IllegalStateException if this Valve is already
     *  associated a different Container.
     */
    public void setContainer(Container container);


    /**
     * Return descriptive information about this Valve implementation.
     */
    public String getInfo();


    /**
     * Return the next Valve in the pipeline containing this Valve, if any.
     */
    public Valve getNext();


    /**
     * Set the next Valve in the pipeline containing this Valve.
     *
     * @param valve The new next valve, or null if none
     */
    public void setNext(Valve valve);


    /**
     * Return the previous Valve in the pipeline containing this Valve, if any.
     */
    public Valve getPrevious();


    /**
     * Set the previous Valve in the pipeline containing this Valve.
     *
     * @param valve The new previous valve, or null if none
     */
    public void setPrevious(Valve valve);


    //---------------------------------------------------------- Public Methods


    /**
     * Perform request processing as required by this Valve.
     *
     * An individual Valve MAY perform the following actions, in
     * the specified order:
     * 
     * *   Examine and/or modify the properties of the specified Request and
     *     Response.
     * *   Examine the properties of the specified Request, completely generate
     *     the corresponding Response, and return control to the caller.
     * *   If the corresponding Response was not generated (and control was not
     *     returned, call the next Valve in the pipeline (if there is one) by
     *     executing getNext().invoke().
     * *   Examine, and possibly modify, the properties of the resulting
     *     Response (which was created by a subsequently invoked Valve or
     *     Container).
     *
     * A Valve MUST NOT do any of the following things:
     *
     * *   Change request properties that have already been used to direct
     *     the flow of processing control for this request (for instance,
     *     trying to change the virtual host to which a Request should be
     *     sent from a pipeline attached to a Host or Context in the
     *     standard implementation).
     * *   Create a completed Response AND pass this
     *     Request and Response on to the next Valve in the pipeline.
     * *   Consume bytes from the input stream associated with the Request,
     *     unless it is completely generating the response.
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    public void invoke(Request request, Response response)
	throws IOException, ServletException;


}

In contrast to request interceptors, the Valve design does not divide request processing into "phases" the way that a web server does. The primary reason for this choice is that the processing to be performed for many of these phases is fixed by the Servlet API Specification, so that there is no need for flexibility (at run time) to insert different processing for that phase. A second reason is that you can normally simulate dealing with multiple "phases" of a request by injecting more than one Valve at various points in the overall request processing flow of control.

The Valves that are associated with a particular Container (see the section below on Functionality Differences for more information about this concept) are organized into a "pipeline". The primary functionality of the Valve is contained in the invoke() method, which generally follows this outline:

Within the invoke() method of a Container that has an associated pipeline of Valves, executing the filtering implemented by this pipeline is very simple:

    Valve first = ... the first valve in the pipeline ...
    first.invoke(request, response);

One interesting question when using pipelines in this manner is: how do you make sure that you do not fall off the end of the pipeline? Ultimately, one of the getNext() calls will return null, and would cause a null pointer exception using the code above. This question is answered when we understand that the basic functionality of each Container within Catalina is actually implemented as a Valve itself. For example, the primary function of the invoke() method in the org.apache.tomcat.core.StandardContext Container, which corresponds to a ServletContext, is to select the Wrapper for the servlet that should be executed (based on the request mapping rules in the Servlet API Specification), call the invoke() method of that Wrapper, and then return. This code is implemented in the org.apache.tomcat.core.StandardContextValve Valve implementation -- any Valves that are added to the pipeline for this Container are added before this Valve, so that the getNext() call for the last sysadmin-defined Valve automatically invokes the standard functionality for this container.



3. PERFORMANCE DIFFERENCES

Now that we understand the basics of each approach to filtering, we can start to understand what kinds of performance differences there would be when implementing exactly the same filtering functionality using the two approaches (varying only the implementation technique).

3.1 Method Calls Required

As described above, RequestInterceptor classes have multiple "entry points" (currently ten), each of which is called to process every request. In a C implementation of this idiom (using a dispatch table of function pointers, with a null pointer indicating that this particular module is not interested in a particular phase), it is quite efficient to call only the module functions that care about particular phases. In the Java implementation, however, all ten of those method calls happen on every request, for every request interceptor to be called (with a few minor exceptions in specially handled cases). Even if your own RequestInterceptor does not implement the methods for the phases you are not interested in, the method calls happen (to the do-nothing methods in the underlying base class.

By contrast, implementing filtering in a Valve requires one method call per Valve. Thus, as long as you can implement the particular functionality of a particular filter in ten or less Valves, the Valve approach will require less method call overhead solely due to the chosen implementation technique for filters. Therefore, let us examine the possible cases as a series of assertions about the performance differences between the two approaches to implementing request filtering:

3.2 Request State Maintenance

A second performance difference between request interceptors and valves becomes apparent when we recall that servlet containers operate in a multi-threaded environment. Just like application level servlets themselves, request interceptors and valves must be able to process multiple requests (on multiple threads) simultaneously. Some filtering functionality requires the filtering component to maintain state information about a particular request across calls to other filtering components (or to the application level servlets and JSP pages themselves).

With request interceptors, the choices of where to maintain such state information are limited:

Therefore, a RequestInterceptor that needs to maintain state information is left with two primary choices: use a collection class (like a Hashtable) within the request interceptor class, keyed by the request processing thread, or store information in the request object itself (which will then be accessed by later phase processing methods that are passed the same request instance). The latter technique is typically used in the RequestInterceptors that have been created so far, using either customized additional properties and methods of the internal Request and Response implementation objects (which can lead to encapsulation problems from an object oriented design perspective), or the relatively new "notes" facility (which stores an array of arbitrary objects in each request for the purpose of maintaining per-request state information for request interceptors).

By contrast, a Valve's invoke() method can use local variables for maintaining the state information related to a particular request. This is possible for the following reasons:

Since an access to a local stack variable will be more efficient (at the bytecode level) than a method call plus an array-element access (which is how the "notes" facility is currently implemented), we can see that Valves offer a performance advantage whenever state information needs to be saved. (There is also a reliability advantage, because there is no possibility that the state information for a particular request will be overwritten by subsequent filters operating on that same request -- as is possible if two RequestInterceptors inadvertently use the same index into the notes array for their information due to an implementation bug.)

3.3 Example Filter Implementations

A few brief examples and implementation sketches for a variety of useful filter functionality will illustrate the performance differences identified above.

3.3.1 Access Log Creation

When running Tomcat behind a web server, the web server is responsible for creating the "access log" file that is typically used by hit count analysis programs. However, when you are running Tomcat stand-alone, it would be nice if Tomcat could optionally include functionality to perform this task for us. (Such a Valve already exists in Catalina).

To implement this functionality as a RequestInterceptor, we might create something like this:

    public final class AccessLogInterceptor extends BaseInterceptor {

        public int postService(Request request, Response response) {
	    ... log the interesting details ...
	}

    }

A corresponding Valve implementation would look like this:

    public final class AccessLogValve extends ValveBase {

        public void invoke(Request request, Response response)
	  throws IOException, ServletException {
	    getNext().invoke(request, response);
	    ... log the interesting details ...
	}

    }

As you can see, both approaches can easily implement the required functionality. The only important difference is that the Valve must specifically decide to call the next valve in the pipeline (as described under "Valve Basics" above), while filter management in the RequestInterceptor environment is the responsibility of the container itself. What is not visible here, however, is the fact that the container is also calling the other nine entry points of AccessLogInterceptor, even though they do nothing at all useful.

3.3.2 Response Time Measurement

A common desire during application development is to benchmark the time required to process a particular request. It seems reasonable to implement such a timer as a filter, so that it can be optionally configured during testing, without requiring that any code be added specifically to the servlet container or the application to make this possible. Note that, for accurate timing, you will want to configure this filter as the "outermost" filter that is encountered during request processing.

A simple (see below for more information) implementation of such a timer as a RequestInterceptor might look like this:

    public final class TimerInterceptor extends BaseInterceptor {

        public int contextMap(Request request) {
	    long startTime = System.currentTimeMillis();
	    request.setNote(TIMER_INDEX, new Long(startTime));
	}

        public int postService(Request request, Response response) {
	    long startTime =
	        ((Long) request.getNote(TIMER_INDEX)).longValue();
	    long stopTime = System.currentTimeMillis();
	    ... log (stopTime - startTime) ...
	}

    }

The corresponding Valve version might look like:

    public final class TimerValve extends ValveBase {

        public void invoke(Request request, Response response)
	  throws IOException, ServletException {
	    long startTime = System.getTimeMillis();
	    getNext().invoke(request, response);
	    long stopTime = System.getTimeMillis();
	    ... log (stopTime - startTime) ...
	}

    }

Note that the RequestInterceptor version is required to store the state information (i.e. the time that processing started for this request). For the reasons stated earlier, it uses the "notes" facility of the request for this purpose, which requires (among other things) creation and storage of a new Long object, followed by subsequent access to that object. The Valve implementation, on the other hand, uses a local variable for the start time state information, and is able to access it faster.

The Valve implementation of this timer also illustrates that not every RequestInterceptor implementation that uses multiple phase methods will need to be replaced by multiple valves. In this particular example, there are still ten method calls to the timer interceptor (the two illustrated plus the eight empty ones in the base class), compared to one for the valve.

Tomcat 3.2 includes a more complex version of a timer interceptor, org.apache.tomcat.request.AccountingInterceptor with several interesting differences from the one described above:

3.3.3 Authentication Checking and Access Control

A complete analysis of the way that Tomcat 3.2 uses RequestInterceptors (and Catalina uses Valves) for performing authentication checking and access control is beyond the scope of this document. However, the following paragraphs compare and contrast the approaches that are used.

Tomcat 3.2 uses request interceptors for this function as follows:

Catalina uses a combination of a Valve and a Realm implementation that is attached to the Context container for this application (or a higher level container if you want to share it) for this purpose, as follows:

When comparing the performance impacts of the two approaches, we note the following differences in performance, based on the choice of filtering technology in use:



4. FUNCTIONALITY DIFFERENCES

In addition to performance, a key issue when comparing request interceptor and valve technology is to understand whether certain types of desireable functionality can be implemented only with difficulty (or not at all) using one or the other approaches. Of course, we also need to be aware that using approaches other than request filtering may also be employed. The following use cases illustrate some differences between request interceptors and valves that are based on the choice of design pattern (versus differences in current implementations in Tomcat 3.2 and Catalina, which are discussed in the following section).

4.1 Response Wrapping

In many scenarios, it would be useful for the servlet container to be able to provide "post processing" on the output from a servlet (or JSP page). Examples of interesting post processing functionality include:

Implementing this type of functionality using request interceptors is problematic, because there is no opportunity to replace the response object that is seen by the remainder of the processing for this request. Thus, it would become necessary to know enough about the internal implementation of the response to temporarily replace its underlying output stream and/or writer with streams that point at an internal buffer. It must also be known when and how the response implementation injects headers into the ultimate response (which in turn depends on whether it is directly connected to a TCP socket, or returning information to a web server through a JNI connector, for example).

Using Valves, on the other hand, this type of functionality can be implemented by creating a "wrapper" Response that is passed on to subsequent valves in the processing pipeline (and the ultimate servlet that is executed). The invoke() method for a Valve implementing this would follow a design pattern like this:

4.2 Request Wrapping

In a similar manner, it is desireable in some circumstances to perform pre-processing on the request (either the headers, the corresponding input data, or both), prior to making this request available to the rest of the servlet container for processing. Consider, for example, a B2B application that wishes to apply customer-specific XSLT stylesheets to input XML based transactions, in order to transform the request into a format expected by the remainder of this server's application processing.

Performing this task using request interceptors is problematic for the same reasons that response wrapping is, because there is no opportunity to replace the response object seen by the rest of the container. Using Valves, on the other hand, such a transformation is easily handled by creating a new Request that contains the results of applying the appropriate transformations to the original request. The remainder of the processing pipeline has no knowledge (or need to know) about the fact that this transformation took place.

4.3 Form-Based Login

The servlet API specification, version 2.2, describes requirements for form-based authentication in Section 11.5.3. After authentication, the servlet container is expected to reproduce the original request (i.e. the one that triggered the need for authentication) as faithfully as possible. This mirrors the user experience when BASIC or DIGEST authentication is used, and the browser reproduces the original request.

Implementing this functionality using request interceptors is difficult, because the only mechanism available to an interceptor (that is processing the login form) is the equivalent of HttpServletResponse.sendRedirect(). Specific headers and cookies that were included with the original request are lost.

Using Valves, the request headers and cookies included on the original request are cached (in the user's session). When the user is successfully authenticated, the request actually seen by the remainder of the processing pipeline is reconstructed from the cached information (a specialized version of request wrapping discussed in the previous section).

NOTE: The implementation of form-based login in both Tomcat 3.2 and Catalina does have one common restriction -- the servlet container does not faithfully reproduce the original request when an HTTP POST triggered the authentication dialog, because the input data is not cached. This feature can be added to Catalina by including some sort of data buffer in the cached information about the original request (dealing correctly with the case where the input data is so large that you might not want to cache it in memory). Lifting this restriction with Tomcat 3.2 is going to require specialized logic that goes outside the capabilities of RequestInterceptors as they are currently defined.



5. IMPLEMENTATION DIFFERENCES

Most of this paper has discussed the implications of RequestInterceptors and Valves that are fundamental to the use of that particular paradigm for implementing filtering functionality. However, there is an issue about the current implementation of RequestInterceptors in Tomcat 3.2 that has nothing to do with the design choice of one approach or the other -- in other words, it can be changed by modifications to the org.apache.tomcat.core package -- but in the mean time it is a significant limitation on the overall flexibility of request interceptors.

In the current design of Tomcat 3.2, the list of request interceptors to be utilized can be configured in the conf/server.xml file, by using <RequestInterceptor> elements at the appropriate point. However, this list is global to all web applications running within a particular instance of Tomcat -- there is currently NO mechanism to customize the list of request interceptors on a per-application (or per-virtual-host) basis.

Because of this, any functionality that is implemented using request interceptors is consistently applied (by Tomcat 3.2) to all requests for all web applications. Among the issues that this causes:

In contrast, the way that Valves and valve pipelines are implemented in Catalina, you can have a custom set of Valves at multiple levels (entire servlet container, virtual host, and web application), so that you can customize (or share) request filtering for whatever universe of requests you wish to filter.