“This is the 21st day of my participation in the Gwen Challenge in November. See details: The Last Gwen Challenge in 2021”

Create relationships between Tomcat components

The components in Tomcat and their relationships were described in detail in the last two articles. Here is a simplified diagram:

These components need to be created, assembled, and started in order for Tomcat to provide external services. When the service stops, resources need to be freed and these components destroyed. So Tomcat needs to manage the life cycle of these components dynamically.

These components of Tomcat, the big component manages the widget, the outer component controls the inner component, and the request processing is driven by the outer component.

Thus it is decided that Tomcat should follow this order when creating components:

  • The child component is created, then the parent component is created, and the child component needs to be “injected” into the parent component.
  • The inner component is created first, then the outer component is created, and the inner component needs to be “injected” into the outer component.

Therefore, it is most intuitive to create all the components on the diagram in the order from small to large, from inside to outside, and then assemble them together. I don’t know if you’ve noticed, but there’s something wrong with this idea! Because this will not only cause code logic confusion and component omission, but also not conducive to the later function expansion.

To solve this problem, we wanted to find a common, unified way to manage the life cycle of components, similar to the “one-click start” effect of cars.

LifeCycle interface of Tomcat component

As mentioned above, As Tomcat starts and stops, its components are created and destroyed, and Tomcat is designed to abstract the LifeCycle of a component into a LifeCycle interface. This interface defines methods about the lifecycle:

    void init(a) throws LifecycleException;

    void start(a) throws LifecycleException;

    void stop(a) throws LifecycleException;

    void destroy(a) throws LifecycleException;
Copy the code

These methods are implemented by concrete components.

Of course, in the parent component’s init() method you need to create the child component and call its init() method. Similarly, the start() method of the parent component needs to be called in the start() method of the child component, so the caller can call the init() and start() methods of each component without distinction. This is the use of the composite pattern, and as long as the top-level component is called, This is the Server component’s init() and start() methods, and Tomcat is started.

LifeCycle events

Consider from the scalability of the system, because the specific implementation of the init() and start() methods of each component is complex and changeable. For example, the startup method of the Host container needs to scan the Web application in the Webapps directory and create the corresponding Context container. If you need to add new logic in the future, just change the start() method? This would violate the open and close principle, so how to solve the problem? The open closed principle says that in order to extend the functionality of the system, you cannot directly modify existing classes in the system, but you can define new classes.

A component’s init() and start() calls are triggered by changes in the state of its parent component, initialization of the upper component triggers initialization of the child component, and startup of the upper component triggers startup of the child component. Therefore, we define the life cycle of a component as states, and consider the transition of state as an event. Events have listeners. Some logic can be implemented in listeners, and listeners can be easily added and deleted. This is the typical observer mode.

Specifically, LifeCycle interface will have two methods: add listeners and remove listeners. In addition, we need to define an Enum that represents what states the component has and what events are triggered by being in that state. So LifeCycle interface and LifeCycleState are defined as follows.

public interface Lifecycle {
    String BEFORE_INIT_EVENT = "before_init";
    String AFTER_INIT_EVENT = "after_init";
    String START_EVENT = "start";
    String BEFORE_START_EVENT = "before_start";
    String AFTER_START_EVENT = "after_start";
    String STOP_EVENT = "stop";
    String BEFORE_STOP_EVENT = "before_stop";
    String AFTER_STOP_EVENT = "after_stop";
    String AFTER_DESTROY_EVENT = "after_destroy";
    String BEFORE_DESTROY_EVENT = "before_destroy";
    String PERIODIC_EVENT = "periodic";
    String CONFIGURE_START_EVENT = "configure_start";
    String CONFIGURE_STOP_EVENT = "configure_stop";

    void addLifecycleListener(LifecycleListener var1);

    LifecycleListener[] findLifecycleListeners();

    void removeLifecycleListener(LifecycleListener var1);

    void init(a) throws LifecycleException;

    void start(a) throws LifecycleException;

    void stop(a) throws LifecycleException;

    void destroy(a) throws LifecycleException;

    LifecycleState getState(a);

    String getStateName(a);

    public interface SingleUse {}}Copy the code

AddLifecycleListener and removeLifecycleListener add and remove events and define event names in the interface, which is the string above.

The getStateName method gets the state of the current component at which stage in its life cycle. In the Tomcat design, there is an enumerated class that describes the state of the component:

public enum LifecycleState {
    NEW(false, (String)null),
    INITIALIZING(false."before_init"),
    INITIALIZED(false."after_init"),
    STARTING_PREP(false."before_start"),
    STARTING(true."start"),
    STARTED(true."after_start"),
    STOPPING_PREP(true."before_stop"),
    STOPPING(false."stop"),
    STOPPED(false."after_stop"),
    DESTROYING(false."before_destroy"),
    DESTROYED(false."after_destroy"),
    FAILED(false, (String)null);

    private final boolean available;
    private final String lifecycleEvent;

    private LifecycleState(boolean available, String lifecycleEvent) {
        this.available = available;
        this.lifecycleEvent = lifecycleEvent;
    }

    public boolean isAvailable(a) {
        return this.available;
    }

    public String getLifecycleEvent(a) {
        return this.lifecycleEvent; }}Copy the code

As can be seen from the above class design, the component’s life cycle includes NEW, INITIALIZING, INITIALIZED, STARTING_PREP, STARTING, STARTED, etc. Once the component reaches the corresponding state, the corresponding event will be triggered. For example, the NEW state indicates that the component has just been instantiated; When the init() method is called, the state changes to INITIALIZING. At this point, the BEFORE_INIT_EVENT event is triggered, and its method is called if a listener is listening to this event.

LifeCycleBase Abstract base classes

Generally speaking, there is more than one implementation class, different classes in the implementation of the interface will often have some of the same logic, if let each subclass to implement again, there will be repeated code. How does a subclass reuse this logic? The idea is to define a base class to implement common logic, and then have subclasses inherit it to achieve reuse. Abstract base classes are also used extensively in many open source software designs.

Abstract methods are often defined in a base class, which means that the base class does not implement these methods, but instead calls them to implement skeleton logic. Abstract methods are left to the individual subclasses to implement, and they must be implemented or they cannot be instantiated.

Tomcat defines a LifeCycleBase class that implements the LifeCycle interface, putting common logic into the base class, such as life-state transitions and maintenance, life-event triggering, listener addition and removal, while subclasses implement their own initialization, start and stop methods. In order to avoid having the same name as the method in the base class, we change the implementation method of the concrete subclass to Internal. We call it initInternal(), startInternal(), etc. Take a look at the class diagram after introducing the base class LifeCycleBase:

As you can see from the diagram, LifeCycleBase implements all the methods in the LifeCycle interface and defines the corresponding abstract methods to be implemented by concrete subclasses, which is a typical template design pattern.

LifeCycleBase init() implementation:

public final synchronized void init(a) throws LifecycleException {
    // Check the status
    if (!this.state.equals(LifecycleState.NEW)) {
        this.invalidTransition("before_init");
    }
    try {
        // Start INITIALIZING event listener
        this.setStateInternal(LifecycleState.INITIALIZING, (Object)null.false);
        // Call the initialization method of the concrete subclass
        this.initInternal();
        // INITIALIZED is a listener to press
        this.setStateInternal(LifecycleState.INITIALIZED, (Object)null.false);
    } catch (Throwable var2) {
        this.handleSubClassException(var2, "lifecycleBase.initFail".this.toString()); }}Copy the code

From the logic of abstracting the base class init method above, we can see that the component initialization is done in four steps:

The first step is to check the validity of the state, such as the current state must be NEW before initialization.

Step 2: Trigger INITIALIZING event listener:

In this setStateInternal method, the listener’s business method is called.

Third, call the initInternal() method, the abstract method implemented by the concrete subclass. As I mentioned earlier, in order to achieve one-click startup, a particular component implements the initInternal() method by calling its child’s init() method.

In the fourth step, after the child component is INITIALIZED, the listener of the INITIALIZED event is fired and the corresponding listener’s business method is invoked.

When and who signed up for the listener?

There are two cases:

  • Tomcat has custom listeners that the parent component registers with the child component during its creation. Such as MemoryLeakTrackingListener listener, used to detect the Context memory leak in the container, the listener is the Host container when create Context container to register in the Context.
  • We can also define our own listeners in server.xml, which Tomcat parses at startup, creating listeners and registering them with container components.

5. Lifecycle Management overall class diagram

StandardServer, StandardService, and so on in the figure are concrete implementation classes for Server and Service components that inherit from LifeCycleBase.

StandardEngine, StandardHost, StandardContext, and StandardWrapper are concrete implementation classes for the corresponding container components, and since they are containers, they inherit the ContainerBase abstract class, ContainerBase implements the Container interface, inherits the LifeCycleBase class, and its lifecycle management interface and function interface are separate, which is consistent with the principle of interface separation in design.

Six, summarized

In order to achieve one-click start and stop and elegant lifecycle management, Tomcat takes extensibility and reusability into account, and gives full play to object-oriented thinking and design pattern, using composite pattern, observer pattern, skeleton abstract class and template method respectively.

If you need to maintain a bunch of entities with parent-child relationships, consider using a composite pattern.

The observer mode, which sounds “superior,” is a series of updates that need to be performed when an event occurs. The traditional approach is to add update logic directly to the event response code. When too much update logic is added, the code becomes bloated, and this approach is tightly coupled and intrusive. The Observer pattern implements a low-coupling, non-intrusive notification and update mechanism.

Template methods are often used in abstract base classes to implement general logic.