On the architecture design of Tomcat

Keywords: Java Apache Tomcat xml

Tomcat is implemented as a servlet container, which is a lightweight application server developed based on Java language. As an application server, Tomcat has the advantages of full open source, light weight, stable performance, low deployment cost, etc., so it has become the first choice for java development and application deployment at present. Almost every Java Web developer has used it, but have you ever understood and thought about the overall design of Tomcat?

This paper will analyze based on Tomcat8. The specific version is the latest version of Tomcat8 (2019-11-21 09:28) version v8.5.49

overall structure

There are many modules in the overall structure of Tomcat. The following figure lists the main modules in the structure we will analyze. The main analysis is Service, Connector, Engine, Host, Context, Wrapper. In order to avoid the layer looking too disorderly, n in the figure below represents that multiple components can be allowed.

As shown in the figure above, the Server is a tomcat Server, and there can be multiple Service services in the Server. There can be multiple connectors and a Servlet Engine in each Service, and multiple connectors in one Service correspond to one Engine In each Engine, there can be multiple domain names. Here, the concept of virtual Host can be used to represent Host. There can be multiple application contexts in each Host.
The relationship among Server, Service, Connector, Engine, Host, Context and Wrapper. Except for Connector and Engine, they are parallel relationships, and other relationships are inclusive. At the same time, they all inherit the Lifecycle interface, which provides Lifecycle management, including init, start, stop and destroy. When its parent container starts, it calls the start of its child container, and the stop is the same.

In the above figure, you can also see that Engine, Host, Context and Wrapper all inherit from Container. It has a backgroundProcess() method, which is processed asynchronously in the background, so it is convenient to create asynchronous threads after inheriting it.
In Tomcat7, we see that the Service holds the Container, not the Engine. It is estimated that this is also why the name of the Engine method is setContainer in the current version.

Server

The Tomcat source code provides the org.apache.catalina.Server interface. The corresponding default implementation class is org.apache.catalina.core.StandardServer. The interface provides the following methods.

In the above figure, you can know what the Server does: manage services, addresses, ports, Catalina, and global named resources.
During Server initialization, the data configured in server.xml will be loaded.

Here, the addService of the Service operation adds a new Service to the defined Service set for analysis:

// Save the service set of the service
private Service services[] = new Service[0];

final PropertyChangeSupport support = new PropertyChangeSupport(this);

@Override
public void addService(Service service) {
    // Interrelated
    service.setServer(this);
    
    // Use synchronous lock to prevent concurrent access source: https://ytao.top
    synchronized (servicesLock) {
        Service results[] = new Service[services.length + 1];
        // copy old service to new array
        System.arraycopy(services, 0, results, 0, services.length);
        // Add a new service
        results[services.length] = service;
        services = results;
    
        // If the current server has been started, the currently added service will start
        if (getState().isAvailable()) {
            try {
                service.start();
            } catch (LifecycleException e) {
                // Ignore
            }
        }
        
        // Using observer mode, the listener is notified when the property value of the monitored object changes, and remove is also called.
        support.firePropertyChange("service", null, service);
    }

}

As you can see in the source code, after adding services to the server, the service will be started randomly, in fact, the service start portal.

Service

The main responsibility of Service is to assemble the Connector and Engine together. The purpose of separating the two is to decouple request monitoring and request processing, which can have better scalability. Each Service is independent of each other, but shares a JVM and system class library. The org.apache.catalina.Service interface and the default implementation class org.apache.catalina.coreStandardService are provided here.

In the implementation class StandardService, it mainly analyzes two methods: setContainer and addConnector.


private Engine engine = null;

protected final MapperListener mapperListener = new MapperListener(this);

@Override
public void setContainer(Engine engine) {
    Engine oldEngine = this.engine;
    // Determine whether the current Service is associated with an Engine
    if (oldEngine != null) {
        // If there is an Engine associated with the current Service, remove the currently associated Engine
        oldEngine.setService(null);
    }
    // If the current new Engine is not empty, then the Engine is associated with the current Service, which is a two-way Association
    this.engine = engine;
    if (this.engine != null) {
        this.engine.setService(this);
    }
    // If the current Service is started, start the current new Engine
    if (getState().isAvailable()) {
        if (this.engine != null) {
            try {
                this.engine.start();
            } catch (LifecycleException e) {
                log.error(sm.getString("standardService.engine.startFailed"), e);
            }
        }
        // Restart MapperListener to get a new Engine, which must be the current input Engine
        try {
            mapperListener.stop();
        } catch (LifecycleException e) {
            log.error(sm.getString("standardService.mapperListener.stopFailed"), e);
        }
        try {
            mapperListener.start();
        } catch (LifecycleException e) {
            log.error(sm.getString("standardService.mapperListener.startFailed"), e);
        }

        // If there is Engine Association before the current Service, stop the previous Engine
        if (oldEngine != null) {
            try {
                oldEngine.stop();
            } catch (LifecycleException e) {
                log.error(sm.getString("standardService.engine.stopFailed"), e);
            }
        }
    }

    // Report this property change to interested listeners
    support.firePropertyChange("container", oldEngine, this.engine);
}

/**
* The implementation method is similar to standard server ා addservice, not described in detail
* Note that there is no bidirectional association between Connector and Service implementation like Engine
*/
@Override
public void addConnector(Connector connector) {

    synchronized (connectorsLock) {
        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 (getState().isAvailable()) {
            try {
                connector.start();
            } catch (LifecycleException e) {
                log.error(sm.getString(
                        "standardService.connector.startFailed",
                        connector), e);
            }
        }

        // Report this property change to interested listeners
        support.firePropertyChange("connector", null, connector);
    }

}

Connector

Connector is mainly used to receive the request, and then hand it to the Engine to process the request, and then to the connector to return it to the client after processing. Currently, the supported protocols are: HTTP, HHTP/2, AJP, NIO, NIO2, APR
The main functions include:

  • Listen on the server port to read requests from clients.
  • Parse the protocol and give it to the corresponding container to process the request.
  • Return the processed information to the client

Example of configuration information in server.xml corresponding to Connector:

<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

Here, you can configure the port number port to listen to, specify the processing protocol protocol, and the redirection address redirectPort.
The protocol processing type is set when the connector is instantiated:

public Connector() {
    // No parameter construction, HTTP/1.1 is used by default in the following setProtocol
    this(null);
}

public Connector(String protocol) {
    // Set the current connector protocol processing type
    setProtocol(protocol);
    // Instantiate the protocol handler and save it to the current Connector
    ProtocolHandler p = null;
    try {
        Class<?> clazz = Class.forName(protocolHandlerClassName);
        p = (ProtocolHandler) clazz.getConstructor().newInstance();
    } catch (Exception e) {
        log.error(sm.getString(
                "coyoteConnector.protocolHandlerInstantiationFailed"), e);
    } finally {
        this.protocolHandler = p;
    }

    if (Globals.STRICT_SERVLET_COMPLIANCE) {
        uriCharset = StandardCharsets.ISO_8859_1;
    } else {
        uriCharset = StandardCharsets.UTF_8;
    }
}

/**
* This setting is removed from tomcat9 and changed to required
*/
public void setProtocol(String protocol) {

    boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
            AprLifecycleListener.getUseAprConnector();

    // The default protocol specified here is the same as HTTP/1.1
    if ("HTTP/1.1".equals(protocol) || protocol == null) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
        }
    } else if ("AJP/1.3".equals(protocol)) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
        }
    } else {
        // Finally, if you do not specify the protocol of HTTP/1.1 and AJP/1.3, you can instantiate a protocol processor by class name
        setProtocolHandlerClassName(protocol);
    }
}

Protocol handler is a protocol processor that provides different implementations for different requests. The implementation of class AbstractProtocol initializes an abstract class AbstractEndpoint in the initialization stage to start the thread to monitor the server port. When the request is received, it calls Processor to read the request and then sends it to Engine to process the request.

Engine

Engine corresponds to the org.apache.catalina.Engine interface and the org.apache.catalina.core.StandardEngine default implementation class.
The function of Engine is also relatively simple, dealing with the association of container relations.

But the addChild() in the implementation class does not refer to the child Engine, but can only be the Host. Without a parent container, setParent is not allowed to be set by operation.

@Override
public void addChild(Container child) {
    // Added child container must be Host 
    if (!(child instanceof Host))
        throw new IllegalArgumentException
            (sm.getString("standardEngine.notHost"));
    super.addChild(child);
}

@Override
public void setParent(Container container) {

    throw new IllegalArgumentException
        (sm.getString("standardEngine.notParent"));

}

server.xml can configure our data:

<!-- Configure default Host,and jvmRoute -->
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">

Host

Host represents a virtual host. We should set multiple domain names for our server, such as demo.ytao.top and dev.ytao.top. Then we need to set up two different hosts to handle the requests of different domain names. When the domain name of the request is demo.ytao.top, it will go to the Context under the domain name host.
So our server.xml configuration file also provides this configuration:

<!-- name Virtual host domain name when setting -->
<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

Context

When you come to Context, you will have the Servlet running environment. Engine and Host are mainly used to maintain the container relationship, but not the running environment.
For now, we can understand Context as an application. For example, we have two applications in the root directory: ytao-demo-1 and ytao-demo-2. Here are two contexts.
The main addChild method introduced here is Wrapper:

@Override
public void addChild(Container child) {

    // Global JspServlet
    Wrapper oldJspServlet = null;

    // The child containers added here can only be wrappers
    if (!(child instanceof Wrapper)) {
        throw new IllegalArgumentException
            (sm.getString("standardContext.notWrapper"));
    }

    // Determine whether the subcontainer Wrapper is a JspServlet
    boolean isJspServlet = "jsp".equals(child.getName());

    // Allow webapp to override JspServlet inherited from global web.xml.
    if (isJspServlet) {
        oldJspServlet = (Wrapper) findChild("jsp");
        if (oldJspServlet != null) {
            removeChild(oldJspServlet);
        }
    }

    super.addChild(child);

    // Add servlet mapping to Context component
    if (isJspServlet && oldJspServlet != null) {
        /*
         * The webapp-specific JspServlet inherits all the mappings
         * specified in the global web.xml, and may add additional ones.
         */
        String[] jspMappings = oldJspServlet.findMappings();
        for (int i=0; jspMappings!=null && i<jspMappings.length; i++) {
            addServletMappingDecoded(jspMappings[i], child.getName());
        }
    }
}

This is the Servlet management center in each application.

Wrapper

Wrapper is the management center of a Servlet. It has the entire life cycle of the Servlet. It has no sub containers, because it is the lowest container itself.
Here is the analysis of Servlet loading:

public synchronized Servlet loadServlet() throws ServletException {

    // If you have instantiated or used the instantiation pool, you can directly return
    if (!singleThreadModel && (instance != null))
        return instance;

    PrintStream out = System.out;
    if (swallowOutput) {
        SystemLogHandler.startCapture();
    }

    Servlet servlet;
    try {
        long t1=System.currentTimeMillis();
        // If the servlet class name is empty, a servlet exception is thrown directly
        if (servletClass == null) {
            unavailable(null);
            throw new ServletException
                (sm.getString("standardWrapper.notClass", getName()));
        }

        // Get Servlet from Context
        InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
        try {
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } catch (ClassCastException e) {
            unavailable(null);
            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.notServlet", servletClass), e);
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            unavailable(null);

            // Added extra log statement for Bugzilla 36630:
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=36630
            if(log.isDebugEnabled()) {
                log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);
            }

            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.instantiate", servletClass), e);
        }

        // Load information declaring MultipartConfig annotation
        if (multipartConfigElement == null) {
            MultipartConfig annotation =
                    servlet.getClass().getAnnotation(MultipartConfig.class);
            if (annotation != null) {
                multipartConfigElement =
                        new MultipartConfigElement(annotation);
            }
        }

        // Check servlet type
        if (servlet instanceof ContainerServlet) {
            ((ContainerServlet) servlet).setWrapper(this);
        }

        classLoadTime=(int) (System.currentTimeMillis() -t1);

        if (servlet instanceof SingleThreadModel) {
            if (instancePool == null) {
                instancePool = new Stack<>();
            }
            singleThreadModel = true;
        }

        // Initialize servlet
        initServlet(servlet);

        fireContainerEvent("load", this);

        loadTime=System.currentTimeMillis() -t1;
    } finally {
        if (swallowOutput) {
            String log = SystemLogHandler.stopCapture();
            if (log != null && log.length() > 0) {
                if (getServletContext() != null) {
                    getServletContext().log(log);
                } else {
                    out.println(log);
                }
            }
        }
    }
    return servlet;

}

The Servlet is loaded here. If the Servlet has not been instantiated, it must be loaded.

So far, I have introduced the main components of Tomcat 8, and I have a general understanding of the overall architecture of Tomcat. After the reconstruction of Tomcat source code, the readability is really much better. I suggest that you can try to analyze some of the design patterns used in it, which we can use for reference in the actual coding process.

Personal blog: https://ytao.top
My public address ytao

Posted by ade1982 on Mon, 25 Nov 2019 05:08:23 -0800