3. What are Tomcat’s “top managers” responsible for?

For those of you who have used Tomcat, we can start Tomcat with the startup.sh script in /bin of Tomcat. Do you know what happens when we execute this script? You can use this flow chart to get an idea.

  1. Tomcat is essentially a Java program, so the startup.sh script starts a JVM to run Tomcat’s startup class, Bootstrap.
  2. The main task of Bootstrap is to initialize Tomcat’s classloader and create Catalina.
  3. Catalina is a startup class that parses server.xml, creates the corresponding component, and calls the Server’s start method.
  4. The Server component is responsible for managing the Service component, which calls the start method of the Service.
  5. The Service component is responsible for managing the connector and the top-level container Engine, so it calls the start methods of the connector and Engine.

This completes the start of Tomcat. Let me take a closer look at some of the key startup classes and components mentioned in this startup process.

Instead of handling specific requests, these startup classes or components are primarily “managing,” managing the lifecycle of lower-level components and assigning tasks to lower-level components, namely routing requests to the component that does the “work.” So I liken them to the “high level” of Tomcat.

Today we’ll take a look at these “high-level” implementation details to give us a step-by-step understanding of how Tomcat works. On the other hand, software systems often have administrative components, and you can learn how Tomcat implements these components.

Catalina

The main task of Catalina is to create a Server. Instead of simply creating a new Server instance, you need to parse server.xml and create the components that are configured in server.xml. The Server component’s init and start methods are then called, and the entire Tomcat is started. As a “manager,” Catalina also had to deal with “exception” situations, such as how gracefully Tomcat stopped and cleaned up when we shut it down with Ctrl + C. So Catalina registers a “close hook” in the JVM.

Public void start() {//1. If (getServer() == null) {load(); If (getServer() == null) {log.fatal(sm.getString(" catalina.noserver "));} //2. return; } //3. Start Server try {getServer().start(); } catch (LifecycleException e) { return; } if (useShutdownHook) {if (shutdownHook == null) {shutdownHook = new CatalinaShutdownHook(); } Runtime.getRuntime().addShutdownHook(shutdownHook); } // stop the request with await method if (await) {await(); stop(); }}Copy the code

What is a “closing hook” and what does it do? If we need to do some cleanup when the JVM shuts down, such as flushing cached data to disk or cleaning up temporary files, we can register a “shutdown hook” with the JVM. A “close hook” is simply a thread whose run method the JVM tries to execute before stopping. Let’s take a look at what Tomcat’s “closing hook” CatalinaShutdownHook does.

protected class CatalinaShutdownHook extends Thread { @Override public void run() { try { if (getServer() ! = null) { Catalina.this.stop(); } } catch (Throwable ex) { ... }}}Copy the code

As you can see from this code, Tomcat’s “close hook” actually executes Server’s stop method, which frees and cleans up all resources.

The Server component

The implementation class for the Server component is StandardServer. Let’s take a look at what StandardServer does. The Server inherits LifeCycleBase, whose lifecycle is centrally managed and whose child components are Services, so it also needs to manage the lifecycle of services, meaning that the start methods of Service components are called when they are started and their stop methods are called when they are stopped. The Server maintains several Service components, which are stored in an array. How does the Server add a Service to the array?

@Override public void addService(Service service) { service.setServer(this); Synchronized (servicesLock) {// Create a new array of length +1 Service results[] = new Service[services.length +1]; System.arraycopy(services, 0, results, 0, services.length); results[services.length] = service; services = results; If (getState().isavailable ()) {try {service.start(); } the catch (LifecycleException e) {/ / Ignore}} / / support. The trigger to monitor events firePropertyChange (" service ", null, service); }}Copy the code

You can see from the above code, it does not assign a long array from the start, but in the process of adding dynamically extend the length of the array, when adding a new Service instance, creates a new array and copy the original array contents to the new array, the aim is to save memory space.

In addition, an important task of the Server component is to start a Socket to listen for the stop port, which is why you can shutdown Tomcat with the shutdown command. In case you noticed, the last line of the Caralina start method above calls the await method of the Server.

In the await method, a Socket is created to listen on port 8005 and receives connection requests on the Socket in an infinite loop. If a new connection arrives, the connection is established and data is read from the Socket. If the data read is the stop command “SHUTDOWN”, exit the loop and enter the stop process.

The Service component

The implementation class for the Service component is StandardService, and let’s first look at its definition and key member variables.

Public class StandardService extends LifecycleBase implements Service {private String name = null; Private Server Server = null; Protected Connector Connectors [] = new Connector[0]; private final Object connectorsLock = new Object(); Private Engine Engine = null; Protected final Mapper Mapper = new Mapper(); protected final MapperListener mapperListener = new MapperListener(this);Copy the code

StandardService inherits the LifecycleBase abstract class, along with familiar components such as Server, Connector, Engine, and Mapper.

So why is there a MapperListener? This is because Tomcat supports hot deployment. When the deployment of a Web application changes, the mapping information in the Mapper also changes. The MapperListener is a listener that listens for changes in the container and updates the information to the Mapper.

The most important thing for a component in a “management” role is to maintain the life cycle of other components. Also, when starting various components, pay attention to their dependencies, that is, the order in which they are started. Let’s look at the Service startup method:

protected void startInternal() throws LifecycleException { //1. Trigger the start listener setState(lifecyclestate.starting); //2. Start Engine. If (Engine! = null) { synchronized (engine) { engine.start(); }} //3. Start mapperListener.start(); Synchronized (connectorsLock) {for (Connector Connector: connectors) { if (connector.getState() ! = LifecycleState.FAILED) { connector.start(); }}}}Copy the code

As you can see from the startup method, the Service starts the Engine component first, then the Mapper listener, and finally the connector. This makes sense, because the inner component is started before it can provide services externally, and the outer connector component can be started. Mapper also relies on container components to listen for changes when container components are started, so Mapper and MapperListener start after container components. The order in which components stop is the reverse of the order in which they start, also based on their dependencies.

Engine components

Finally, let’s look at how the top-level container component Engine is implemented. Engine is essentially a container, so it inherits the ContainerBase class and implements the Engine interface.

public class StandardEngine extends ContainerBase implements Engine {
}
Copy the code

We know that Engine’s child container is Host, so it holds an array of Host containers, all of which is abstracted into ContainerBase, which has this data structure:

protected final HashMap<String, Container> children = new HashMap<>();
Copy the code

ContainerBase uses HashMap to store its children. It also implements “add, delete, change, and search” for children. It even provides default implementations for starting and stopping children, such as using a dedicated thread pool to start children.

for (int i = 0; i < children.length; i++) {
   results.add(startStopExecutor.submit(new StartChild(children[i])));
}
Copy the code

So Engine directly reuses this method when it starts the Host child container.

So what does Engine do by itself? As we know, the most important function of a container component is to handle requests. Engine “handles” requests by forwarding them to a Host child, which Valve does.

Each container component has a Pipeline, and the Pipeline has a Basic Valve. The base Valve of the Engine container is defined as follows:

final class StandardEngineValve extends ValveBase { public final void invoke(Request request, Throws IOException, ServletException {// Get the Host container in the request. Host Host = request.gethost (); if (host == null) { return; } // invoke host.getPipeline().getfirst ().invoke(request, response); }}Copy the code

The basic valve implementation is very simple and simply forwards requests to the Host container. You might be wondering, as you can see from the code, how can a Host container be in a request object? This is because the Mapper component routes the request to the Engine container before it reaches the Engine container. The Mapper component locates the corresponding container by the request URL and stores the container object in the request object.

Fourth, refine the componentized design specification from Tomcat

Componentized and configurable

The overall architecture of Tomcat is component-based. You can configure these components in XML files or code. For example, you can configure Tomcat connectors and container components in server.xml. The corresponding. In other words, Tomcat provides a stack of building blocks that are up to you. You have the flexibility to build your Web container based on your needs, as well as the ability to customize components. This design provides depth of customization for the Web container.

So how does the Web container implement this componentized design? I think there are two main points:

  • The first is interface oriented programming. We need to split the functionality of the system according to the principle of “high cohesion, low coupling”, each component has a corresponding interface, communication between components through the interface, so that components can be easily replaced. For example, we can choose different connector types, as long as the connector components implement the same interface.
  • The second is that the Web container provides a vehicle for putting components together to work. A component’s job is simply to process requests, so the container sends requests to the component in turn through the chain of responsibility pattern. For the user, I just need to tell the Web container which components handle the request. Organizing components requires a “manager,” which is why both Tomcat and Jetty have the concept of a Server. A Server is the carrier of components, containing connector components and container components. The container also needs to delegate requests to child container components for processing, and Tomcat is implemented in the chain of responsibility pattern.

Components are assembled by the user through configuration, similar to dependency injection for beans in Spring. Spring users can assemble beans through configuration files or annotations, and their bean-to-bean dependencies are entirely up to the user. This is different from Web containers, where the relationship between components is fixed. For example, in Tomcat, the Engine component has a Host component, the Host component has a Context component, and so on. But you can’t “inject” a Wrapper component into the Host component because of the Web container’s own capabilities.

Component creation

Because components can be configured, the Web container does not know which components to create until it is started, that is, they cannot be hardcoded to instantiate, but need to be created dynamically through reflection. Specifically, the Web container does not instantiate the component object through the new method, but instead creates the component through class.forname. Either way, the Web container needs to load the component classes into the JVM before instantiating a class. This involves a classloading problem, and the Web container designs its own classloader.

Spring also uses reflection to dynamically instantiate beans, so where does the classloader it uses come from? The Web container creates a class loader for each Web application, and Spring uses the class loader passed to it by the Web container.

Component lifecycle management

Different types of components have parent-child hierarchies, with the parent processing the request and passing it on to a child component.

Tomcat uses the concept of containers, putting small containers into large containers to achieve parent-child relationship. In fact, their essence is the same. It’s really about how to manage these components in a unified way, how to start and stop with one click.

Tomcat takes a similar approach to managing component lifecycle. there are two main points,

  • The parent component is responsible for creating, starting, stopping, and destroying child components. Once the topmost component is started, the entire Web container is started, enabling one-click start and stop.
  • Tomcat defines the life cycle state of components, and defines the change of component state as an event. The change of the state of a component will trigger the change of sub-components. For example, the startup event of the Host container will trigger the scanning and loading of Web applications, and finally create the corresponding Context container under the Host container. The Context component’s start event triggers a Servlet scan to create the Wrapper component. So how do you achieve this linkage? The answer is the observer model. Specifically, listeners are created to listen for state changes in the container and implement actions in the listener’s methods. These listeners are “extension points” in the lifecycle of the component.

A similar design is adopted by Spring, which provides many “extension points” for Bean lifecycle states. These extension points are defined as interfaces, and Spring is responsible for calling those interfaces whenever your Bean implements them. The idea is that when control over Bean creation, initialization, and destruction is given to Spring, Spring gives you the opportunity to execute your logic throughout the Bean’s life cycle. Here’s a diagram to help you understand the Spring Bean lifecycle process:

Skeleton abstract classes and template patterns for components

Specifically to the design and implementation of components, Tomcat largely uses skeleton abstract classes and template patterns. For example, the ProtocolHandler interface in Tomcat has an abstract base class, AbstractProtocol, which implements the framework and general logic of the protocol processing layer. Such as HttpProtocol and AjpProtocol. For Jetty, the AbstractHandler and Connector interfaces are AbstractorConnector. These abstract skeleton classes implement general logic and define abstract methods that are implemented by subclasses. Abstract skeleton classes invoke abstract methods to implement skeleton logic.

This is a common design specification that is used everywhere by Web containers, Spring, and even the JDK itself, such as AbstractSet, AbstractMap, etc. in Java collections. Notably, since Java 8, interfaces have been allowed to have default methods so that we can put the generic logic of abstracting skeleton classes into interfaces.

Optimize and improve Tomcat startup speed

Clean up your Tomcat

1. Clear unnecessary Web applications

The first thing we need to do is delete the projects in the Webapps folder that we don’t need. This is usually the default projects such as host-manager, Example, and doc. There may also be projects that were added before but are not needed now. If you look at the Tomcat startup log, you can see that each time Tomcat is started, these projects are redeployed.

2. Clear the XML configuration file

We know that Tomcat parses all of the XML configuration files at startup, but XML parsing is expensive, so we want to keep the configuration files as simple as possible. The fewer things you have to parse, the faster it will be.

3. Clear the JAR file

We can also delete any unwanted JAR files. When loading a class, the JVM’s classloader needs to look through each JAR file to find the desired class. If you delete unwanted JAR files, lookups will be faster. Note here: There should be no Servlet API or Tomcat’s own JARS in the Lib directory of a Web application, which are provided by Tomcat. If you are building your application using Maven, the dependency on the Servlet API should be specified as

provided
.

4. Clear other files

Clean up logs in time and delete unnecessary log files in the logs folder. There is also the Catalina folder in the Work folder, which is actually Tomcat’s working directory for converting JSPS to Class files. In some cases, if you change the code and restart Tomcat, but it still doesn’t work, you can delete the folder and Tomcat will be regenerated the next time it starts.

Disable Tomcat TLD scanning

In order to support JSP, Tomcat scans the TLD file in the JAR at application startup and loads the tag library defined in the JAR. In the Tomcat startup log, you may encounter this message:

At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of  JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.Copy the code

Tomcat means that I scanned the JAR package under your Web application and found no TLD file in the JAR package. I recommend configuring Tomcat not to scan these JARS to speed up Tomcat startup and save JSP compilation time.

How to configure not to scan JAR packages?

  • If your project does not use JSP as a Web page template, but instead uses a templating engine like Velocity, you can completely disable TLD scanning. To do this, go to the context. XML file in Tomcat’s conf/ directory and add the JarScanner and JarScanFilter subtags to the context tag, as shown below.
  • If your project uses JSPS as Web page modules, that means TLD scans are unavoidable, but we can configure it to tell Tomcat to scan only those JAR packages that contain TLD files. To do this, go to the Catalina. properties file in Tomcat’s conf/ directory and add your JAR package to the jarsToSkip configuration item in that file.

    tomcat.util.scan.StandardJarScanFilter.jarsToSkip=xxx.jar

Disable WebSocket support

Tomcat scans API implementations of WebSocket annotations, such as the @ServerEndpoint annotation class. As we know, annotation scanning is generally slow and can be turned off if WebSockets are not needed. To do this, go to the Context. XML file in Tomcat’s conf/ directory and add a containerSciFilter attribute to the context tag, as shown below.

Further, if you don’t need WebSockets, you can remove the websocket-api.jar and tomcat-websocket.jar jar files in the Tomcat lib directory to further improve performance.

Turning off JSP support

As with turning off WebSocket, if you don’t need JSP, you can turn off JSP functionality in a similar way, as shown below.

We found that the containerSciFilter attribute is also used to disable the JSP. If you want to disable both WebSocket and JSP, configure it like this:

Disable Servlet annotation scanning

Servlet 3.0 introduced annotated servlets. Tomcat supports this feature by scanning your class files at Web application startup, so if you are not using Servlet annotations, you can tell Tomcat not to scan Servlet annotations. To do this, set it in your Web application’s web. XML file<web-app>Attributes of an elementmetadata-complete="true", like the following.

Configure web-fragment scanning

Servlet 3.0 also introduces the Web-fragment.xml “Web Module Deployment Descriptor fragment”, a deployment description file that completes the configuration of Web.xml. The web-fragment. XML file must be stored in the META-INF directory of JAR files. JAR packages are usually stored in the Web-INF /lib directory, so Tomcat needs to scan JAR files to support this function.

You can configure the web.xml in the<absolute-ordering>The element directly specifies which JAR packages need to be scanned for the Web fragment, if<absolute-ordering/>The element is empty, indicating that no scan is required, as follows.

Random number entropy source optimization

This is a well-known question. Versions of Tomcat 7 and up rely on Java’s SecureRandom class to generate random numbers, such as Session ids. By default, the JVM uses a blocking entropy source (/dev/random), which can cause Tomcat to start slowly in some cases. When blocking for a long time, you’ll see a warning log like this:

<DATE> org.apache.catalina.util.SessionIdGenerator createSecureRandom
INFO: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [8152] milliseconds.
Copy the code

I won’t go into how this works, but you can read more about it. The solution is to set up the JVM to use a non-blocking source of entropy.

We can set JVM parameters:

 -Djava.security.egd=file:/dev/./urandom
Copy the code

Or set up the java.security file under the $JAVA_HOME/jre/lib/security directory:

securerandom.source=file:/dev/./urandom
Copy the code

/dev/./urandom is a Bug in Oracle JRE that has been fixed by the SecureRandom class in Java 8. A blocking source (/dev/random) is more secure than a non-blocking source (/dev/./urandom).

Start multiple Web applications in parallel

When Tomcat is started, Web applications are started one by one by default. Tomcat is started only when all Web applications are started. If you have multiple Web applications under the same Tomcat, to optimize startup speed, you can configure multiple applications to start in parallel by modifying the startStopThreads attribute of the Host element in server.xml. The startStopThreads value indicates how many threads you want to start your Web application. If set to 0, you want to start your Web application in parallel, as follows.Note that this parameter is also configured in the Engine element, which means that if your Tomcat is configured with multiple hosts, Tomcat will start multiple hosts in parallel.