Tomcat一键启停背后的设计

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

一、Tomcat的组件创建关系

上两篇文章中详细的阐述了Tomcat中的组件以及之间的关系,下面通过一张简化的图描述一下:

想要让Tomcat能够对外提供服务,还需要创建、组装并启动这些组件。在服务停止的时候,还需要释放资源,销毁这些组件。所以Tomcat需要动态地管理这些组件地生命周期。

Tomcat的这些组件,大组件管理小组件,外层组件控制内层组件,请求的处理就是由外层组件来驱动的。

因此决定了Tomcat在创建组件时应该遵循这顺序:

  • 先创建子组件,再创建父组件,子组件需要被“注入”到父组件中。
  • 先创建内层组件,再创建外层组件,内层组建需要被“注入”到外层组件。

因此,最直观的做法就是将图上所有的组件按照先小后大、先内后外的顺序创建出来,然后组装在一起。不知道你注意到没有,这个思路其实很有问题!因为这样不仅会造成代码逻辑混乱和组件遗漏,而且也不利于后期的功能扩展。

为了解决这个问题,我们希望找到一种通用的、统一的方法来管理组件的生命周期,就像汽车“一键启动”那样的效果。

二、Tomcat组件的生命周期:LifeCycle接口

上面说到,随着Tomcat的启停,它的组件也会由创建到销毁,所在Tomcat在设计中便把组件的生命周期抽象成LifeCycle接口。该接口定义了关于生命周期的方法:

    void init() throws LifecycleException;

    void start() throws LifecycleException;

    void stop() throws LifecycleException;

    void destroy() throws LifecycleException;
复制代码

这些方法便由具体的组件来实现。

理所当然,在父组件的 init() 方法里需要创建子组件并调用子组件的 init() 方法。同样,在父组件的 start() 方法里也需要调用子组件的 start() 方法,因此调用者可以无差别的调用各组件的 init() 方法和 start() 方法,这就是组合模式的使用,并且只要调用最顶层组件,也就是 Server 组件的 init() 和 start() 方法,整个 Tomcat 就被启动起来了。

三、从可扩展性出发:LifeCycle事件

从系统的可扩展性出发思考,因为各个组件 init() 和 start() 方法的具体实现是复杂多变的,比如在 Host 容器的启动方法里需要扫描 webapps 目录下的 Web 应用,创建相应的 Context 容器,如果将来需要增加新的逻辑,直接修改 start() 方法?这样会违反开闭原则,那如何解决这个问题呢?开闭原则说的是为了扩展系统的功能,你不能直接修改系统中已有的类,但是你可以定义新的类。

组件的 init() 和 start() 调用是由它的父组件的状态变化触发的,上层组件的初始化会触发子组件的初始化,上层组件的启动会触发子组件的启动,因此我们把组件的生命周期定义成一个个状态,把状态的转变看作是一个事件。而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便的添加和删除,这就是典型的观察者模式

具体来说就是在 LifeCycle 接口里加入两个方法:添加监听器和删除监听器。除此之外,我们还需要定义一个 Enum 来表示组件有哪些状态,以及处在什么状态会触发什么样的事件。因此 LifeCycle 接口和 LifeCycleState 就定义成了下面这样。

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() throws LifecycleException;

    void start() throws LifecycleException;

    void stop() throws LifecycleException;

    void destroy() throws LifecycleException;

    LifecycleState getState();

    String getStateName();

    public interface SingleUse {
    }
}
复制代码

addLifecycleListenerremoveLifecycleListener对事件添加和删除,并且在该接口中定义了事件名称,就是上面的字符串。

其中的getStateName方法,是获取当前组件的状态是处于生命周期的哪个阶段。在Tomcat设计中,定了一个枚举类描述了组件的状态:

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() {
        return this.available;
    }

    public String getLifecycleEvent() {
        return this.lifecycleEvent;
    }
}

复制代码

从上面的类设计中可以看到,组件的生命周期有 NEW、INITIALIZING、INITIALIZED、STARTING_PREP、STARTING、STARTED 等,而一旦组件到达相应的状态就触发相应的事件,比如 NEW 状态表示组件刚刚被实例化;而当 init() 方法被调用时,状态就变成 INITIALIZING 状态,这个时候,就会触发 BEFORE_INIT_EVENT 事件,如果有监听器在监听这个事件,它的方法就会被调用。

四、从重用性出发:LifeCycleBase抽象基类

一般来说实现类不止一个,不同的类在实现接口时往往会有一些相同的逻辑,如果让各个子类都去实现一遍,就会有重复代码。那子类如何重用这部分逻辑呢?其实就是定义一个基类来实现共同的逻辑,然后让各个子类去继承它,就达到了重用的目的。抽象基类在很多的开源软件的设计中应用地也是十分的广泛。

基类中往往会定义一些抽象方法,所谓的抽象方法就是说基类不会去实现这些方法,而是调用这些方法来实现骨架逻辑。抽象方法是留给各个子类去实现的,并且子类必须实现,否则无法实例化。

Tomcat 定义一个基类 LifeCycleBase 来实现 LifeCycle 接口,把一些公共的逻辑放到基类中去,比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等,而子类就负责实现自己的初始化、启动和停止等方法。为了避免跟基类中的方法同名,我们把具体子类的实现方法改个名字,在后面加上 Internal,叫 initInternal()、startInternal() 等。看一下引入了基类 LifeCycleBase 后的类图:

从图上可以看到,LifeCycleBase 实现了 LifeCycle 接口中所有的方法,还定义了相应的抽象方法交给具体子类去实现,这是典型的模板设计模式

LifeCycleBase中init()的实现:

public final synchronized void init() throws LifecycleException {
    // 状态检查
    if (!this.state.equals(LifecycleState.NEW)) {
        this.invalidTransition("before_init");
    }
    try {
        // 出发INITIALIZING事件的监听器
        this.setStateInternal(LifecycleState.INITIALIZING, (Object)null, false);
        // 调用具体子类的初始化方法
        this.initInternal();
        // 出发INITIALIZED是按的监听器
        this.setStateInternal(LifecycleState.INITIALIZED, (Object)null, false);
    } catch (Throwable var2) {
        this.handleSubClassException(var2, "lifecycleBase.initFail", this.toString());
    }
}
复制代码

从上面抽象基类的init方法的逻辑可以知道组件的初始化主要完成了四步:

第一步,检查状态的合法性,比如当前状态必须是 NEW 然后才能进行初始化。

第二步,触发 INITIALIZING 事件的监听器:

在这个 setStateInternal 方法里,会调用监听器的业务方法。

第三步,调用具体子类实现的抽象方法 initInternal() 方法。我在前面提到过,为了实现一键式启动,具体组件在实现 initInternal() 方法时,又会调用它的子组件的 init() 方法。

第四步,子组件初始化后,触发 INITIALIZED 事件的监听器,相应监听器的业务方法就会被调用。

监听器又是什么时候,又是谁注册进去的?

分为两种情况:

  • Tomcat 自定义了一些监听器,这些监听器是父组件在创建子组件的过程中注册到子组件的。比如 MemoryLeakTrackingListener 监听器,用来检测 Context 容器中的内存泄漏,这个监听器是 Host 容器在创建 Context 容器时注册到 Context 中的。
  • 我们还可以在 server.xml 中定义自己的监听器,Tomcat 在启动时会解析 server.xml,创建监听器并注册到容器组件。

五、生周期管理总体类图

图中的 StandardServer、StandardService 等是 Server 和 Service 组件的具体实现类,它们都继承了 LifeCycleBase。

StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相应容器组件的具体实现类,因为它们都是容器,所以继承了 ContainerBase 抽象基类,而 ContainerBase 实现了 Container 接口,也继承了 LifeCycleBase 类,它们的生命周期管理接口和功能接口是分开的,这也符合设计中接口分离的原则

六、总结

Tomcat 为了实现一键式启停以及优雅的生命周期管理,并考虑到了可扩展性和可重用性,将面向对象思想和设计模式发挥到了极致,分别运用了组合模式、观察者模式、骨架抽象类和模板方法

如果你需要维护一堆具有父子关系的实体,可以考虑使用组合模式。

观察者模式听起来“高大上”,其实就是当一个事件发生后,需要执行一连串更新操作。传统的实现方式是在事件响应代码里直接加更新逻辑,当更新逻辑加多了之后,代码会变得臃肿,并且这种方式是紧耦合的、侵入式的。而观察者模式实现了低耦合、非侵入式的通知与更新机制。

而模板方法在抽象基类中经常用到,用来实现通用逻辑。

Guess you like

Origin juejin.im/post/7033779252587659272