This article focuses on how Tomcat uses design patterns in its design to improve code’s ability to cope with change. There will be several levels of discussion: 1. What is the application scenario here? 2. What kind of changes will this application scenario bring about and what kind of scalability is needed 3. Why are certain design patterns used here, and how do they relate?
// TODO: Continue to update some of the common design patterns and refine the scenario descriptions
Understand the application scenarios of Tomcat
Tomcat needs to generate a Request object based on the Request information in HTTP or AJP Request packets
- Request line for the current request –
RequestLine
Includes request methods such as:GET, POST, HEAD
, urls of requested resources such as static pages, dynamic handlers (servlets), and so on; - Information in the request header carried by the current request, such as
Cookie
,content-type
Information such as…
Put this information into the Request object, and through static resources or dynamic handlers to fill the return object Response object returned to the Request client; This is how Tomcat handles a request at the macro level.
So let’s move onTomcat
Some challenges encountered in the design and analysisTomcat
How designers circumvent it.
Generates an event-observer pattern based on changes in server state
The Observer mode can be used when there is a one-to-many relationship between objects and multiple objects (listeners or observers) are concerned about the state change of the observed object (the condition of event generation) and perform corresponding actions according to the state change.
In Tomcat, the server has various states, including Init, Starting, Started, Stoped, etc. If we need to make some corresponding extensions during the process of the server state change, such as the need to create objects when the state change, need to record key logs, need to control resources, We can set up multiple observers to extend when the observed state changes.
StandardServer is the class of the corresponding server in Tomcat, and when our server state changes from Init to Start, StandardServer notifies the current server startup event to the observer object on the Listeners [] list using the Dependent Class FireLifeCycleEvent() in LifeCycleSupport, and the observer object responds to the behavior of the event.
Let’s look at the actual code:
/** * The lifecycle Event support for this component. * LifecycleSupport encapsulates The implementation of The observer pattern -> 1. LifecycleSupport lifecycle = new LifecycleSupport(this); lifecycle = new LifecycleSupport(this); * It sends a LifecycleEvent of type START_EVENT to any registered listeners. */ public void start() Throws LifecycleException {// Notify our interested LifecycleListeners // Trigger the Before Start event of the Server component lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); / / Start events trigger Server components, Start the component lifecycle. FireLifecycleEvent (START_EVENT, null); started = true; Synchronized (Services) {for (int I = 0; synchronized (Services) {for (int I = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) ((Lifecycle) services[i]).start(); }} // Notify our interested LifecycleListeners // Trigger the Server component After Start event lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }Copy the code
Public Final class LifecycleSupport {/** * The set of registered LifecycleListeners for event notifications. * Observer list */ private LifecycleListener listeners[] = new LifecycleListener[0]; Public void fireLifecycleEvent(String type, Object data) {// Build the event Object, LifeCycle is the Server object LifecycleEvent event = new LifecycleEvent(LifeCycle, type, data); LifecycleListener interested[] = null; synchronized (listeners) { interested = (LifecycleListener[]) listeners.clone(); } for (int i = 0; i < interested.length; [I]. LifecycleEvent (event); }}Copy the code
public class ServerLifecycleListener implements LifecycleListener { /** * Primary entry point for startup and shutdown Events. * @param event The event that has occurred Triggered by LifecycleSupport. FireLifecycleEvent () * / public void lifecycleEvent (lifecycleEvent event) {Lifecycle Lifecycle = event.getLifecycle(); -start_event if (Lifecycle.start_event.equals (event.getType())) {if (Lifecycle instanceof Server) {// Create the related object createMBeans() in the EJB; } // We are embedded. if( lifecycle instanceof Service ) { MBeanFactory factory = new MBeanFactory(); createMBeans(factory); createMBeans((Service)lifecycle); }}}}Copy the code
Based on the above code, Tomcat realizes the monitoring and response of multiple observers to the event of service startup through observer mode, so that the code meets three design principles:
- Encapsulate changes – Identify areas of the application that may need to change and encapsulate them so that the rest of the application is not affected
- Use composition more than inheritance
- Loose coupling of interactions between interacting objects;
The changes during Tomcat startup are as follows: 1. The service status increases and decreases. For example, In later iterations, Tomcat adds BEFORE_START_EVENT and AFTER_START_EVENT before and after START_EVENT. 2. Add or subtract a Listener for each service state change, such as resource control.
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
Copy the code
Of course, if there is an interdependency between the execution of observer events, the observer pattern is not a good choice, and the chain of responsibility is a better choice.
Multiple network communication modes and application layer protocol-policy mode
Communication modes include BIO, NIO, NIO2, etc., and application layer protocols include HTTP1.1, HTTP2, AJP, etc. Users can use them in combination, so there are a variety of strategies in Tomcat to achieve network communication functions. Such as NIO + HTTP1.1 Strategy, NIO + AJP Strategy, so at the network communication level, Tomcat needs to organize the code structure to support the dynamic change of communication protocols (such as the addition of a protocol).
We can start by following OO design principles.
-
Encapsulation change. The core change of the code in this scenario is the change of network communication mode, so we need to separate this function from other functions.
-
Programming for an interface, not an implementation, so we need to define an Interface ProtocolHandler and have different protocols implement that interface;
-
Multi-purpose composite supercomponents delegate interfaces defined above to network communication functions implemented using different protocols.
According to the above ideas, we can first draw a general class diagram as follows:
The Connector (Tomcat Connector component) relies on the ProtocolHandler to implement different types of network communication. Therefore, when a Protocol is added, we only need to add or modify the corresponding Protocol implementation class without modifying the logic in the Connector.
Then we look at the definition of a policy pattern: A policy pattern defines clusters of algorithms that are packaged separately so that they can be replaced with each other, and this pattern allows changes in the algorithm to be independent of the customers using the algorithm.
It can be seen that the use of strategic pattern design is consistent with following the corresponding design principles, so design patterns can help us to use design principles faster and better.
Then the most core is that we need to support dynamic setting of different protocol implementation classes in Connector. In Tomcat, the corresponding protocol implementation classes are put in Connector through if-else instantiation (new) according to the protocol type used by users.
Public Connector(String protocol) {// Set different protocol implementation classes if ("HTTP/1.1". Equals (protocol)) { setProtocolHandlerClassName("org.apache.coyote.http11.Http11Protocol"); } the if (" AJP / 1.3 ". The equals (protocol)) {setProtocolHandlerClassName (" org. Apache. Jk. Server. JkCoyoteHandler "); } // Instantiate Class clazz = class.forName (protocolHandlerClassName); this.protocolHandler = (ProtocolHandler) clazz.newInstance(); }Copy the code
We can also compound the factory pattern to isolate instantiations of clusters of objects that implement the same functionality from the Connector class.
PS: Because the logic of Dealing with network communication in Tomcat is complicated, the interface design is not described.
Intermodule invocation-adapter pattern
A server processing a request is divided into three parts: establishing a connection, parsing a request according to the protocol, and processing a request. Therefore, Tomcat divides it into two modules, Connector and Engine respectively. In order to facilitate the decoupling of the two modules, adapter mode is implemented to achieve the decoupling of the two modules.
Section logic – Filter chain mode
The filter chain pattern applies when we want to give one or more objects a chance to be able to handle a request.
As shown in the figure above, in Tomcat, when the request actually arrives at the Servlet, we need to dynamically add some aspect logic, such as Log and Auth, which can be handled using the filter chain mode to separate the aspect code from the core business code.
For example, if we want to add a snapshot log of incoming and outgoing requests at the Engine level for each request to the server, we can do the following configuration in server.xml:
<! -- The request dumper valve dumps useful debugging information about the request and response data received and sent by Tomcat. Documentation at: /docs/config/valve.html --> <Valve className="org.apache.catalina.valves.RequestDumperValve"/>Copy the code
Tomcat can then use Digester (XML parser) to read the configuration file and generate events to configure the filter chain.
The class diagram in Tomcat is designed as follows:
In a filter chain, the most important ones are the First Valve, Basic Valve and Next Valve. Through these three information, we can traverse a filter chain, starting from the head of the chain and passing the request through invoke().
conclusion
At this point, the main general image of Tomcat is built, and of course there are many design patterns used in Tomcat, such as factories, singletons, facades, and so on.