Tomcat System Architecture and Design patterns, Part 1

The working principle of

Series contents:

This is part 1 of a two-part series: Tomcat system Architecture and Design patterns

This article is based on Tomcat 5, as well as the latest Tomcat 6 and Tomcat 4. The basic design idea and architecture of Tomcat have some continuity.

Tomcat architecture

The structure of Tomcat is very complex, but Tomcat is also very modular, find the most core modules of Tomcat, you grasp the “seven inches” of Tomcat. Here is the overall structure of Tomcat:

Figure 1. The overall structure of Tomcat

As you can see from the figure above, the heart of Tomcat is two components: Connector and Container, which will be described in more detail later. The Connector component can be replaced, which gives the server designer more choices. The component is so important that it is not only relevant to the design of the server, but also to different application scenarios. Therefore, a Container can have multiple Connectors. Multiple connectors and a Container form a Service. The concept of a Service is familiar. A Service can provide services externally, but it needs an environment to survive. There must be someone who can give her life and control her life and death, and it must be Server. So the entire Tomcat life cycle is controlled by the Server.

Service as marriage

In Tomcat, Connector and Container as a whole are compared to a couple. Connector is mainly responsible for external communication, which can be compared to Boy. Container mainly deals with requests accepted by Connector, mainly dealing with internal affairs. Can be compared to being a Girl. The Service is the marriage certificate that connects the couple. It is the Service that connects them together to form a family. Of course there are many other elements that go into making a family.

A Service can have more than one Connector, but only one Container. The list of methods for the Service interface is as follows:

Figure 2. Service interface

As you can see from the methods defined in the Service interface, the main purpose of the Service interface is to associate the Connector with the Container and initialize other components below it. Note that the interface does not necessarily control the life cycle of the components below it. The Lifecycle of all components is controlled in a Lifecycle interface, and an important design pattern is used here, which will be described later.

The standard implementation class for the Service interface in Tomcat is StandardService which not only implements the Service excuse but also implements the Lifecycle interface so that it can control the Lifecycle of the components underneath it. The StandardService class structure diagram is as follows:

Figure 3. Class structure diagram for StandardService

In addition to implementing the Service interface’s Lifecycle interface and implementing the Lifecycle interface that controls the Lifecycle of the component, there are several methods that are implemented for listening on events. This is not only the Service component, but also other components in Tomcat. This is also a typical design pattern that will be described later.

The setContainer and addConnector methods are available in StandardService.

Listing 1. Standardservice.setcontainer
public void setContainer(Container container) { Container oldContainer = this.container; if ((oldContainer ! = null) && (oldContainer instanceof Engine)) ((Engine) oldContainer).setService(null); this.container = container; if ((this.container ! = null) && (this.container instanceof Engine)) ((Engine) this.container).setService(this); if (started && (this.container ! = null) && (this.container instanceof Lifecycle)) { try { ((Lifecycle) this.container).start(); } catch (LifecycleException e) { ; } } synchronized (connectors) { for (int i = 0; i < connectors.length; i++) connectors[i].setContainer(this.container); } if (started && (oldContainer ! = null) && (oldContainer instanceof Lifecycle)) { try { ((Lifecycle) oldContainer).stop(); } catch (LifecycleException e) { ; } } support.firePropertyChange("container", oldContainer, this.container); }Copy the code

Oldcontainer. setService(null) {oldContainer.setService(null) {oldContainer.setService(null) {oldContainer. If the oldContainer has been started, end its life cycle. The new association is then replaced, initialized, and the life cycle of the new Container begins. Finally, the process is notified to interested event listeners. It is worth noting that when modifying a Container, associate the new Container with each Connector. Fortunately, there is no bidirectional association between the Container and Connector, otherwise the association would be difficult to maintain.

Listing 2. Standardservice.addconnector
public void addConnector(Connector connector) { synchronized (connectors) { connector.setContainer(this.container); connector.setService(this); Connector results[] = new Connector[connectors.length + 1]; System.arraycopy(connectors, 0, results, 0, connectors.length); results[connectors.length] = connector; connectors = results; if (initialized) { try { connector.initialize(); } catch (LifecycleException e) { e.printStackTrace(System.err); } } if (started && (connector instanceof Lifecycle)) { try { ((Lifecycle) connector).start(); } catch (LifecycleException e) { ; } } support.firePropertyChange("connector", null, connector); }}Copy the code

Above is the addConnector method, which is also simple: first set up the association, then initialize it to start the new life cycle. It is interesting to note that the Connector uses an array instead of a List. The Connector uses an array but does not allocate a fixed size array as we would normally do. Create a new array object of the current size, and then copy the original array object into the new array, this way to achieve a similar dynamic array function, this implementation method, we should use for reference in the future.

StandardService is also largely unchanged in the latest Tomcat6, but from Tomcat5 onwards the Service, Server and container classes inherit the MBeanRegistration interface and Mbeans are more properly managed.

Server as “home”

As mentioned above, a couple becomes a couple because of Service. They have the basic conditions to form a family, but they still need to have a physical home, which is the foundation for their survival in society. With a home, they can serve the people at ease and create wealth for the society together.

The task of a Server is simply to provide an interface for other programs to access the collection of services, and to maintain the life cycle of all the services it contains, including how to initialize, terminate, and find services that others want to access. There are other minor tasks, such as registering with the local government to live in the area, and perhaps cooperating with the local police for routine security checks.

The class structure diagram of the Server is as follows:

Figure 4. Class structure diagram of the Server

Lifecycle and MbeanRegistration are implemented by StandardServer, which implements Lifecycle and MbeanRegistration. Let’s take a look at the implementation of addService, one of StandardServer’s most important methods:

Listing 3. StandardServer. The addService
public void addService(Service service) { service.setServer(this); synchronized (services) { Service results[] = new Service[services.length + 1]; System.arraycopy(services, 0, results, 0, services.length); results[services.length] = service; services = results; if (initialized) { try { service.initialize(); } catch (LifecycleException e) { e.printStackTrace(System.err); } } if (started && (service instanceof Lifecycle)) { try { ((Lifecycle) service).start(); } catch (LifecycleException e) { ; } } support.firePropertyChange("service", null, service); }}Copy the code

The Server manages the Service in the same way as the Service Connector. The Server also manages the Service in an array. The rest of the code also manages the life cycle of the newly added Service. There are no changes in Tomcat6 either.

Component’s lifeline “Lifecycle”

We’ve been saying that Services and Servers manage the life cycle of their underlying components. How do they manage this?

The Lifecycle of components in Tomcat is controlled through the Lifecycle interface. As long as components inherit this interface and implement its methods, they can be controlled by the components that own it. In this way, layer by layer until the highest level of components can control the Lifecycle of all components in Tomcat. The highest component is the Server, which is controlled by Startup, where you start and shut down Tomcat.

Here is the class structure diagram for the Lifecycle interface:

Figure 5. Lifecycle class structure diagram

In addition to the Start and Stop methods that control the lifecycle, there is a listening mechanism that does additional operations at the Start and end of the lifecycle. This mechanism is also used in other frameworks, such as Spring. This design pattern will be described later.

Lifecycle interface methods are implemented in other components. As mentioned earlier, the Lifecycle of a component is controlled by the parent component that contains it, so its Start method naturally calls the Start method of the component below it, as well as the Stop method. The Server Start method calls the Start method of the Service component. The Server Start method is as follows:

Listing 4. StandardServer. Start
public void start() throws LifecycleException {
    if (started) {
        log.debug(sm.getString("standardServer.start.started"));
        return;
    }
    lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
    lifecycle.fireLifecycleEvent(START_EVENT, null);
    started = true;
    synchronized (services) {
        for (int i = 0; i < services.length; i++) {
            if (services[i] instanceof Lifecycle)
                ((Lifecycle) services[i]).start();
        }
    }
    lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}Copy the code

The listening code will surround the Start of a Service component by simply looping through the Start method for all Service components, but all services must implement the Lifecycle interface, which is more flexible.

The Server Stop method is as follows:

Listing 5. StandardServer. Stop
public void stop() throws LifecycleException { if (! started) return; lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null); lifecycle.fireLifecycleEvent(STOP_EVENT, null); started = false; for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).stop(); } lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null); }Copy the code

All it does is similar to the Start method.

The Connector components

The Connector component is one of the two core components of Tomcat. Its main task is to receive the TCP connection Request sent by the browser and create a Request and Response object for exchanging data with the requestor. A thread is then created to process the Request and pass the resulting Request and Response objects to the thread that handles the Request, which is what the Container component does.

Due to the complexity of the process, the general flow can be explained by the following sequence diagram:

Figure 6. Connector processing sequence diagram

(View a larger image)

The default Connector in Tomcat5 is Coyote, which is optional. The most important function of a Connector is to receive a connection request and then assign a thread to a Container to handle the request. Therefore, it must be multithreaded. Multithreaded processing is the core of Connector design. Tomcat5 takes this process a step further by dividing the Connector into Connector, Processor, and Protocol. Coyote also defines its own Request and Response objects.

To see how Tomcat handles multi-threaded connection requests, see the main class diagram of Connector:

Figure 7. The main class diagram of Connector

(View a larger image)

Take a look at HttpConnector’s Start method:

Listing 6. HttpConnector. Start
public void start() throws LifecycleException { if (started) throw new LifecycleException (sm.getString("httpConnector.alreadyStarted")); threadName = "HttpConnector[" + port + "]"; lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; threadStart(); while (curProcessors < minProcessors) { if ((maxProcessors > 0) && (curProcessors >= maxProcessors)) break; HttpProcessor processor = newProcessor(); recycle(processor); }}Copy the code

The threadStart() execution enters a state of waiting for a request and does not proceed until a new request arrives. This is done in the HttpProcessor’s Assign method, which looks like this:

Listing 7. HttpProcessor. Assign
synchronized void assign(Socket socket) { while (available) { try { wait(); } catch (InterruptedException e) { } } this.socket = socket; available = true; notifyAll(); if ((debug >= 1) && (socket ! = null)) log(" An incoming request is being assigned"); }Copy the code

The HttpProcessor object is created with available set to false, so it does not enter a while loop when the request comes in, assigning the requested socket to the current socket, and setting Available to true. The HttpProcessor’s run method will be activated when available is set to true and the request will be processed.

The Run method looks like this:

Listing 8. HttpProcessor. Run
public void run() { 
    while (!stopped) { 
        Socket socket = await(); 
        if (socket == null) 
            continue; 
        try { 
            process(socket); 
        } catch (Throwable t) { 
            log("process.invoke", t); 
        } 
        connector.recycle(this); 
    } 
    synchronized (threadSync) { 
        threadSync.notifyAll(); 
    } 
}Copy the code

In the process method, the code fragment for the process method is as follows:

Listing 9. HttpProcessor. Process
private void process(Socket socket) { boolean ok = true; boolean finishResponse = true; SocketInputStream input = null; OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(),connector.getBufferSize()); } catch (Exception e) { log("process.create", e); ok = false; } keepAlive = true; while (! stopped && ok && keepAlive) { finishResponse = true; try { request.setStream(input); request.setResponse(response); output = socket.getOutputStream(); response.setStream(output); response.setRequest(request); ((HttpServletResponse) response.getResponse()) .setHeader("Server", SERVER_INFO); } catch (Exception e) { log("process.create", e); ok = false; } try { if (ok) { parseConnection(socket); parseRequest(input, output); if (! request.getRequest().getProtocol().startsWith("HTTP/0")) parseHeaders(input); if (http11) { ackRequest(output); if (connector.isChunkingAllowed()) response.setAllowChunking(true); }}...... try { ((HttpServletResponse) response).setHeader ("Date", FastHttpDateFormat.getCurrentDate()); if (ok) { connector.getContainer().invoke(request, response); }... } try { shutdownInput(input); socket.close(); } catch (IOException e) { ; } catch (Throwable e) { log("process.invoke", e); } socket = null; }Copy the code

After the Connector wraps the socket connection into request and Response objects, the Container takes care of the rest.

The Servlet Container “Container”

Container is the parent interface of a Container, and all child containers must implement this interface. Container is designed in a typical responsibility chain design pattern. It consists of four child Container components: The Engine,Host, Context, and Wrapper components are not parallel, but parent-child. Engine contains Host,Host contains Context, and Context contains Wrapper. Usually, one Servlet class corresponds to one Wrapper. If there are multiple servlets, you can define multiple wrappers. If there are multiple wrappers, you need to define a higher Container, such as Context. Context usually corresponds to the following configuration:

Listing 10. The Server. The XML
<Context
    path="/library"
    docBase="D:\projects\library\deploy\target\library.war"
    reloadable="true"
/>Copy the code

The overall design of the container

The Context can also be defined in the parent container Host, which is not required, but is required to run the war program, because there must be a web. XML file in the war, and the parsing of that file requires Host. If you want multiple hosts, you define a top container Engine. Engine has no parent container, and an Engine represents a complete Servlet Engine.

So how do these containers work together? Let’s take a look at their relationship:

Figure 8. Diagram of the four containers

(View a larger image)

When a Connector receives a connection request, it sends the request to the Container. How does the Container handle the request? How do these four components work, and how do they pass requests to specific child containers? And how to hand the final request to the Servlet. Here’s a sequence diagram of the process:

Figure 9. A sequence diagram of Engine and Host processing requests

(View a larger image)

Valve’s design is also useful in other frameworks. Similarly, the principle of a Pipeline is similar. It is a Pipeline, and both Engine and Host execute the Pipeline. You can add any number of valves to the pipeline, Tomcat will execute them one by one, and each of the four components will have its own set of valves. How do you define your Valve? In the server. XML file, you can add Valve for Engine and Host as follows:

Listing 11. The Server. The XML
< Engine the defaultHost = "localhost" name = "Catalina" > < Valve className = "org. Apache. Catalina. Valves. RequestDumperValve" / >...... <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false"> <Valve className="org.apache.catalina.valves.FastCommonAccessLogValve" directory="logs" Prefix ="localhost_access_log." suffix=".txt" pattern="common" resolveHosts="false"/> ………… </Host> </Engine>Copy the code

StandardEngineValve and StandardHostValve are the default valves for Engine and Host, and are the last Valve responsible for passing requests to their child containers for further execution.

Engine and Host requests are handled by the Engine and Host containers. Context and Wrapper requests are handled by the Engine and Host containers. Here is the sequence diagram for processing the request:

Figure 10. Sequence diagram of processing requests for Context and Wrapper

(View a larger image)

Since Tomcat5, routes to subcontainers are placed in requests that hold hosts, Context, and Wrappers that are being processed by the current request.

Engine container

The Engine container is relatively simple and only defines some basic associations. The interface class diagram is as follows:

Figure 11. Class structure of the Engine interface

The standard implementation class is StandardEngine, which has no parent container and will get an error if setParent is called. Add child containers can only be of type Host as follows:

Listing 12. Standardengine.addchild
public void addChild(Container child) { if (! (child instanceof Host)) throw new IllegalArgumentException (sm.getString("standardEngine.notHost")); super.addChild(child); } public void setParent(Container container) { throw new IllegalArgumentException (sm.getString("standardEngine.notParent")); }Copy the code

Its initialization method is to initialize its associated components, as well as some events listening.

The Host container

A Host is a word container for Engine. A Host in Engine represents a virtual Host that runs multiple applications, installs and expands them, and identifies them so that they can be distinguished. Its child container is usually the Context, which in addition to associating the child container, also holds the information that a host should have.

Here is the class association diagram associated with Host:

Figure 12. Class diagram related to Host

(View a larger image)

As you can see from the figure above, StandardHost implements the Deployer interface in addition to ContainerBase, which all containers inherit, which clearly lists the main methods for installing, expanding, starting, and ending each Web Application.

The implementation of the Deployer interface is StandardHostDeployer, a class that implements the most important methods that Host can call for application deployment, etc.

The Context container

Context represents the Context of the Servlet, which has the basic environment for the Servlet to run. In theory, a Servlet can run as long as there is a Context. Simple Tomcat can do without Engine and Host.

One of the most important functions of a Context is to manage its Servlet instances. Servlet instances appear in a Wrapper inside the Context. How does the Context find the right Servlet to execute it? Tomcat5 used to be managed through a Mapper class. After Tomcat5, this function was moved to Request. As you can see from the previous sequence diagram, fetching subcontainers is allocated through Request.

Context prepares the Servlet’s runtime environment in the Start method, which has the following code snippet:

Listing 13. StandardContext. Start
Public synchronized void start() throws LifecycleException {...... if( ! initialized ) { try { init(); } catch( Exception ex ) { throw new LifecycleException("Error initializaing ", ex); }}...... lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); setAvailable(false); setConfigured(false); boolean ok = true; File configBase = getConfigBase(); if (configBase ! = null) { if (getConfigFile() == null) { File file = new File(configBase, getDefaultConfigFile()); setConfigFile(file.getPath()); try { File appBaseFile = new File(getAppBase()); if (! appBaseFile.isAbsolute()) { appBaseFile = new File(engineBase(), getAppBase()); } String appBase = appBaseFile.getCanonicalPath(); String basePath = (new File(getBasePath())).getCanonicalPath(); if (! basePath.startsWith(appBase)) { Server server = ServerFactory.getServer(); ((StandardServer) server).storeContext(this); } } catch (Exception e) { log.warn("Error storing config file", e); } } else { try { String canConfigFile = (new File(getConfigFile())).getCanonicalPath(); if (! canConfigFile.startsWith (configBase.getCanonicalPath())) { File file = new File(configBase, getDefaultConfigFile()); if (copy(new File(canConfigFile), file)) { setConfigFile(file.getPath()); } } } catch (Exception e) { log.warn("Error setting config file", e); }}}...... Container children[] = findChildren(); for (int i = 0; i < children.length; i++) { if (children[i] instanceof Lifecycle) ((Lifecycle) children[i]).start(); } if (pipeline instanceof Lifecycle) ((Lifecycle) pipeline).start(); ...... }Copy the code

It mainly sets up various resource properties and management components, and most importantly, starts the child container and Pipeline.

We know that the Context configuration file has a reloadable property as follows:

Listing 14. The Server. The XML
<Context
    path="/library"
    docBase="D:\projects\library\deploy\target\library.war"
    reloadable="true"
/>Copy the code

When the reloadable is set to true, Tomcat automatically reloads the application after war is modified. How did you do that? This function is implemented in StandardContext’s backgroundProcess method, which is coded as follows:

Listing 15. StandardContext. BackgroundProcess
public void backgroundProcess() { if (! started) return; count = (count + 1) % managerChecksFrequency; if ((getManager() ! = null) && (count == 0)) { try { getManager().backgroundProcess(); } catch ( Exception x ) { log.warn("Unable to perform background process on manager",x); } } if (getLoader() ! = null) { if (reloadable && (getLoader().modified())) { try { Thread.currentThread().setContextClassLoader (StandardContext.class.getClassLoader()); reload(); } finally { if (getLoader() ! = null) { Thread.currentThread().setContextClassLoader (getLoader().getClassLoader()); } } } if (getLoader() instanceof WebappLoader) { ((WebappLoader) getLoader()).closeJARs(false); }}}Copy the code

It calls the Reload method, which calls the stop method and then the Start method to complete a reload of the Context. The reload method is executed if reloadable is true and the application is modified. How is the backgroundProcess called?

This method is defined in ContainerBase type of inner class ContainerBackgroundProcessor be invoked by cycle, this class is running in a background thread, it will cycle the execution of the run method, Its run method periodically calls the backgroundProcess methods of all containers, and since all containers inherit from the ContainerBase class, all containers can define periodic events in the backgroundProcess method.

Wrapper container

Wrapper represents a Servlet that manages a Servlet, including its loading, initialization, execution, and resource recycling. Wrapper is the lowest level container, it has no children, so calling its addChild will report an error.

The Wrapper implementation class is StandardWrapper, which also implements a ServletConfig with a Servlet initialization message, This shows that StandardWrapper will work directly with the various messages of servlets.

Let’s take a look at a very important method, loadServlet, with the following code snippet:

Listing 16. StandardWrapper loadServlet
Public synchronized Servlet loadServlet() throws ServletException {...... Servlet servlet; Try {...... ClassLoader classLoader = loader.getClassLoader(); ...... Class classClass = null; ...... servlet = (Servlet) classClass.newInstance(); if ((servlet instanceof ContainerServlet) && (isContainerProvidedServlet(actualClass) || ((Context)getParent()).getPrivileged() )) { ((ContainerServlet) servlet).setWrapper(this); } classLoadTime=(int) (System.currentTimeMillis() -t1); try { instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,servlet); if( System.getSecurityManager() ! = null) { Class[] classType = new Class[]{ServletConfig.class}; Object[] args = new Object[]{((ServletConfig)facade)}; SecurityUtil.doAsPrivilege("init",servlet,classType,args); } else { servlet.init(facade); } if ((loadOnStartup >= 0) && (jspFile ! = null) {...... if( System.getSecurityManager() ! = null) { Class[] classType = new Class[]{ServletRequest.class, ServletResponse.class}; Object[] args = new Object[]{req, res}; SecurityUtil.doAsPrivilege("service",servlet,classType,args); } else { servlet.service(req, res); } } instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet); ...... return servlet; }Copy the code

It basically describes what happens to the Servlet. When the Servlet is loaded, the init method of the Servlet is called, anda StandardWrapperFacade object is passed to the Servlet, This object wraps StandardWrapper, and the ServletConfig diagram is as follows:

Figure 13. The relationship between ServletConfig and StandardWrapperFacade and StandardWrapper

The information available to the Servlet is wrapped in the StandardWrapper facade, which is retrieved in the StandardWrapper object. So servlets can get limited container information through ServletConfig.

Once the Servlet is initialized, it is left to StandardWrapperValve to call its service method, which calls all of the Servlet’s filters.

Other components in Tomcat

Tomcat also has other important components, such as the security component, logger log component, session, MBeans, naming and other components. Together, these components provide the necessary services for the Connector and Container.

On the topic

  • See Part 2 of this series, “Analysis of Tomcat Design Patterns.”
  • DeveloperWorks Java Technology zone: Hundreds of articles on all aspects of Java programming.