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:
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.
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.
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:
getNext().invoke(request, response)
.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.
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).
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:
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:
SingleThreadModel
interface).
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:
invoke()
method does NOT return
control until all subsequent processing for this request is completed
(unlike a RequestInterceptor where a return occurs after each phase).
Therefore, the local variables will not disappear from the stack
during the period of interest for this filter.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.)
A few brief examples and implementation sketches for a variety of useful filter functionality will illustrate the performance differences identified above.
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.
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:
doPostService()
method of the first
interceptor encountered, rather than the last. Any time consumed within
doPostService()
for other request interceptors is ignored
by this implementation. It would require two RequestInterceptors
(configured to be the first and last ones used) to get accurate
total timings.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:
org.apache.tomcat.request.AccessInterceptor
interceptor
determines whether this particular request is subject to the requirements
of a <security-constraint>
defined in the application
deployment descriptor. If it is, the "notes" mechanism is used to record
the relevant state information (which constraint matched; what the
transport constraint is, if any; and the roles required, if any) inside
the request object.org.apache.tomcat.request.SimpleRealm
interceptor
(or one you replace it with if you prefer to use a realm other than the
simple XML file "conf/tomcat-users.xml" supported by the default) then
uses to different request interceptor entry points to perform the
required checks:
authenticate()
method extracts the username and
password (if any) from the incoming request, and validates them in
the underlying realm. If they identify a valid user, the values
to be returned by getRemoteUser()
and
getUserPrincipal()
are set.
(which will trigger an appropriate challenge for credentials).authorize()
method checks that the user who has
been authenticated has the appropriate role required to access the
requested resource. If the authenticated user does not have the
required role (or no user was successfully authenticated), an HTTP
"401" (HttpServletResponse.SC_UNAUTHORIZED) status code is returned.
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:
org.apache.tomcat.security.HttpSecurityBase
) that implements
all of the common logic for authentication and access control, and differ
only in the way that they extract username and password information from
the underlying request.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:
conf/server.xml
file.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).
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:
text/html
and perform URL rewriting for session
maintenance even in the absence of cookies.Accept-Encoding
header indicating, for example, that the
client knows how to accept a GZIP'd output stream and expand it on the
fly. If this request header was present, and if the response data is
at least of a configurable minimum length (to make it worth compressing),
compress it (and add the corresponding Content-Encoding
header to the response).text/xml
.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:
Response
interface, but accumulates all output (both
header setting and data content) into internal buffers.invoke()
method of the next Valve in the pipeline.invoke()
method, performing
whatever modifications are required to implement the desired
filtering functionality.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.
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.
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:
RealmInterceptor
to perform their
username/password authentication and role lookups. Unless you code
your own interceptor, this means that all web apps are essentially
sharing a common user database./servlet/xxxxx
to a corresponding servlet whose Java
class name is "xxxxx") in one web application, this capability is
enabled for all of them.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.