In June 1999, Sun announced that they were contributing the source code
of the Tomcat servlet container, and the Josper (later Jasper) JSP engine,
to the Jakarta project
With the announcement of Jakarta, interest declined in a new version of
Apache JServ based on this architecture, based on the (quite reasonable)
conclusion that Jakarta would become the successor to Apache JServ when it
became available. Now that this has occurred, and we have had a chance to
examine the source code, it is clear that the core servlet container portions
of Tomcat (primarily in package org.apache.tomcat.core
) are in
need of the same architectural remodel, for the same reasons.
This document proposes just such an architecture for a future version of Tomcat, based on a core set of interfaces for which a variety of implementations may be created. These component implementations can then interoperate cleanly with other new and existing components, as long as the interface contracts are honored.
It is expected that conversion to this architecture will require a major refactoring of the existing Tomcat core package, as well as other packages with internal dependencies on the core. It will also require substantial changes in the deployment configuration files for the engine. Therefore, it seems appropriate to implement these changes at a major revision level update of Tomcat.
The new architecture was designed with the following goals in mind:
Tomcat should be easy to configure and deploy in a variety of operational environments, including:
This is accomplished by allowing the deployer to pick and choose among the required Container and Session implementations needed to support the required features.
Tomcat should allow the server administrator to choose whether or not certain functionality (such as persistent support for session data, or load balancing across servers) is required. In addition, it should be possible for server developers to provide customized versions of one or more Tomcat components, and have them interoperate with the remainder of the system seamlessly.
This is accomplished by virtue of the fact that all interactions between Tomcat components are based on standard Java interfaces, which can be implemented in a variety of ways but still interoperate.
Besides the standard request/response processing described in the servlet API, it is desireable in many environments to support additional request processing functionality within the server. Examples include:
This goal is met through the use of the Interceptor
architecture (similar in spirit to the Valve
architecture
from Apache JServ, as well as the existing Interceptor style support
that already exists within Tomcat.
There are three primary families of components in the proposed architecture. The details for each component will be presented in the following section.
HttpServletRequest
being processed, and the
HttpServletResponse
being produced, respectively.HttpSession
, and Manager, which
controls the pool of active sessions visible to a particular container.
The following interfaces define the major components related to communication adapters:
HttpServletRequest
that is utilized throughout the Tomcat
servlet container to represent the incoming request.HttpServletResponse
that is utilized throughout the Tomcat
servlet container to represent the generated response.Reduced to its essentials, the functional processing performed by an Adapter is described as follows:
invoke()
method of the selected container,
passing the initialized request and response instances as arguments.The fundamental servlet container interface is named, naturally enough,
Container. The fundamental responsibility of a
Container is to execute Requests received from a communications Adapter, or
from a parent Container, returning the corresponding Response. This
responsibility is represented by the invoke()
method of the
Container. Each container is optionally associated with an instance of each
of the Support Components described below, and also has the following
characteristics:
The service()
method of a Container is used to implement the
required processing logic of this hierarchical level of a Tomcat deployment,
independent of any Interceptors that may have been defined. See the following
subsection for more information about how request processing with Interceptors
is actually implemented.
Two extensions of the Container interface will be used in nearly every deployment of Tomcat:
ServletContext
which will typically be associated with a
child Wrapper container for each defined servlet.SingleThreadModel
).In an embedded deployment, a single Context instance, representing the entire embedded web application, will usually be the highest level Container component utilized.
When Tomcat is executed in a standalone deployment using an HTTP adapter, the following extensions of the Container interface are provided:
As mentioned above, a Container supports the
definition of a stack of Interceptor objects
that are allowed to participate in request processing both before and after
the service()
method of the owning Container is called. The
functional purpose of Interceptors is best understood by examining several
operational scenarios.
Under normal circumstances (each activated Interceptor returns
true
, and no exceptions are thrown by an Interceptor or the
service()
method of the associated Container), the following
processing logic must be performed by the invoke()
method of
a Container:
preService()
method of all Interceptors associated with
this Container are called, starting with the most recently added
Interceptor. It is assumed each preService()
method returns
true
to indicate that processing should continue. It is also
assumed for this scenario that no exception is thrown.service()
method of the owning Container itself is called.
It is assumed for this scenario that no exception is thrown.postService()
method of all Interceptors associated with
this Container are called, starting with the least recently added
Interceptor. It is assumed for this scenario that no exception is thrown.
A preService()
method of an Interceptor is allowed to return
false
instead of true
. Doing so is an indication
that this Interceptor has in fact created the corresponding response, and that
no further processing for this request is required. When such a return
happens, the following changes to the previously described logic occur:
preService()
methods of any Interceptors added to this
Container earlier than the current one are not called.service()
method of the owning Container itself is
not called.postService()
methods of any Interceptors added to this
Container earlier than the current one are not called.postService()
methods of the current Interceptor, and
any Interceptor added to this Container after the current Interceptor,
are called.If an exception is thrown by any call to a preService()
,
service()
, or postService()
method, all subsequent
method calls described above are skipped. The exception will be rippled out
to the highest level Container whose invoke()
method was
originally called, and from there to the calling Adapter.
The JavaDoc comments for an Interceptor outlines the requirements described above from the viewpoint of an individual Interceptor. Consult this documentation for additional information.
A single instance of each of the following support components may optionally
be associated with each Container, accessible through the usual JavaBeans
property accessor methods. As a general rule, a get
method
returning such a support component should return the component last specified
by a corresponding set
method call, if any; otherwise, a
corresponding get
method call to this Container's parent
Container (if any) should be performed. This approach allows a general
deployment style where support components are only attached at the highest
required hierarchical Container level.
log()
methods defined in the ServletContext
interface. Implementations can store log output wherever desired, or
integrate it with the log output of the server in which Tomcat is
embedded.Principal
can be authenticated, and from which
the set of roles associated with that Principal
can be
identified. Implementations can be created to integrate with existing
legacy security realms, integrate with the security realm of the server
in which Tomcat is embedded, or for custom, application-specific,
security realms.getResource()
and
getResourceAsStream()
by accessing data resources included
with the web application. In some environments (such as Tomcat connected
to a web server such as Apache), it is desireable to use the web server's
facilities to access these resources instead. This interface allows such
accesses to be customized.Support for the session management functionality of the servlet API specification is provided by components that implement the following interfaces:
HttpSession
that is utilized throughout the Tomcat
servlet container to represent the session and its associated
user data objects.A significant implementation challenge in this architecture is the elegant initialization and shutdown of the various components. This proposal suggests an approach, also used in the Apache JServ architecture, based on the Lifecycle interface. The use of this approach includes the following elements:
instanceof
operator). If the component does not
implement Lifecycle, the remaining steps are skipped.configure()
method, passing
an XML document fragment (org.w3c.dom.DocumentFragment
)
containing the configuration parameters for this component. This
approach is based on the assumption that XML-based configuration files
will be utilized to configure Tomcat components. [IMHO it is overkill
to convert such configuration parameters (which are typically processed
only once) into an internal XmlTree
architecture, as is
done in the current Tomcat approach.]start()
method. Successful
return from this method (no exception thrown) indicates that the
component has been completely configured, and is available for normal
use by other components.instanceof
operator). If the component does not
implement Lifecycle, the remaining steps are skipped.stop()
method. This call tells the
component to gracefully clean up its resources (for example, a JDBC
connection pool would close and release all of its allocated
connections to the underlying database).The current Tomcat servlet container architecture has nearly all of the
executable code in a single package (org.apache.tomcat.core
).
This proposal suggests the following package naming conventions:
org.apache.tomcat
- Contains the interface definitions for
all of the core components described in this document.org.apache.tomcat.adapter.xxxxx
- Contains implementations
of the each Adapter component, and their
associated Request and Response components, in subpackages named
for each implementation.org.apache.tomcat.container.xxxxx
- Contains implementations
of the various Container interfaces, and the
corresponding support component implementations, in subpackages named
for each implementation.org.apache.tomcat.session.xxxxx
- Contains implementations
of the Manager and Session interfaces, in subpackages named for
each implementation.The suggested naming conventions maximize the ability to localize private implementation details within a package (through the use of package private scoping of method names), while encouraging independence between packages by using only the commonly defined interfaces.
The following examples illustrate several types of functionality that might be implemented using the Interceptor feature of the proposed architecture:
<security-constraint>
elements included in a web
application's deployment descriptor. Such an Interceptor would include
logic in the preService()
method to examine the incoming
Request properties. If an authentication
challenge is required, the preService()
method would
complete the corresponding Response with
the correct challenge (based on the configuration specified in the
corresponding <login-config>
element), and return
false
to bypass any remaining processing for this Request.
false
from the preService()
method, to skip remaining request processing within Tomcat.
Requests that should be processed via normal servlet API functionality
are passed on by having the preService()
method return true
instead.preService()
method, and stopping that timer in the
corresponding postService()
method of the same
Interceptor. Measuring performance at
this point is useful, because it eliminates the variable overhead of
communications adapters. Such measurements may also contribute to
decisions made by a load balancing module in an application server that
supports distributed processing.postService()
method could be utilized
to capture hit count statistics for requested URIs, and store this
information in some persistent storage location.postService()
method. You can have an
access log per web application by attaching this Interceptor to the
corresponding Context instance, or a combined
log file by attaching it at either the Host or
Engine level, with no changes to any of the
Container implementation classes.The following implementation examples are included with this proposal, to illustrate the development approach that would normally be used:
In packageorg.apache.tomcat.core
:
getInfo()
and
service()
methods must be implemented (although additional
methods may be overridden if necessary).org.apache.tomcat.logger
:
org.apache.tomcat.security
:
org.apache.tomcat.session
: