深入拆解Tomcat学习与实践 | 07

07 | Tomcat 如何实现一键式启停

下面的是一张简化的 Tomcat 类图,图中虚线表示一个请求在 Tomcat 中流转的过程。

上面这张图描述了组件之间的静态关系,如果让一个系统能对外 提供服务,我们需要创建、组装并启动这些组件;

在服务停止的时候,我们还需要释放资源,销毁这些组件。因此这是一个动态的过程

Tomcat 需要动态地管理这些组件的生命周期。

在实际工作中,如果需要设计一个比较大的系统或者框架时,同样也需要考虑这几个问题:

  • 如何统一管理组件的创建、初始化、启动、停止和销毁

  • 如何做到代码逻辑清晰。

  • 如何方便地添加或者删除组件。

  • 如何做到组件启动和停止不遗漏、不重复。

分析这些组件,它们之间具有两层关系
  • 第一层是组件分大小,大组件管理小组件。比如 Server 管理 Service,Service管理连接器和容器。
  • 第二层关系是组件有内有外,外层组件控制内层组件,比如连接器是外层组件,负责对外交流,外层组件调用内层组件完成业务功能。也就是说,请求的处理过程由外层组件驱动。
这两层关系决定了系统在创建组件时应该遵循一定的顺序。
  • 第一个原则是先创建子组件,再创建父组件,子组件需要被”注入”到父组件中。
  • 第二个原则是先创建内层组件,再创建外层组件,内层组件需要备”注入”到外层组件。

因此最直观的方法是将图上的所有组件按先小后大,先内后外的顺序创建出来,然后组装在一起。 但是这个思路有问题,会造成代码逻辑混乱和组件遗漏,也不利于后期功能扩展。【为什么会造成这个问题,是组件的数量会太多吗?】

为了解决这个问题,Tomcat 设计了一个统一的接口来管理组件的生命周期。

一键式启停:Lifecycle 接口

前面说过,设计系统就是找到变化点和不变点。

这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程,这些状态以及状态的转化是不变的

变化点是每个组件的初始化方法是不一样的

于是,将不变点抽象出来成为接口,这个接口与生命周期相关,叫做 Lifecycle。 接口里应该定义这么几个方法:init、start、stop、destroy 这样的常见生命周期方法。每个具体的组件去实现这些方法。

理所当然,父组件的 init 方法里需要创建子组件并调用 子组件的init 方法。 同样在父组件的 start 方法里也要调用子组件的 start方法,因此调用者可以无差别的调用各组件的 init 和 start 方法。 这又是一个组合模式的使用。

并且只要调用最顶层组件,也就是 Server 组件的 init 和 start 方法,整个 Tomcat 就被启动起来了。

下面是Lifecycle的方法列表

可扩展性:Lifecyle 与 事件

考虑另一个问题,系统的扩展性。各个组件的 init 和 start 方法的具体实现是复杂多变的,比如在 Host 容器的启动方法里需要扫描webapps 目录下的 Web 应用,创建对应的 Context 容器,如果将来需要增加新的逻辑,那么就需要修改 start 方法。但是这样就违背了开闭原则。【开闭原则:代码对扩展开放,对修改关闭。不能修改以前的类,但是可以定义新的类。】

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

具体实现是在 Lifecycle 里加入两个方法,添加监听器和删除监听器。

1
2
3
4
5
6
7
8
9
10
11

/**
* Add a LifecycleEvent listener to this component.
*
* @param listener The listener to add
*/
public void addLifecycleListener(LifecycleListener listener);



public void removeLifecycleListener(LifecycleListener listener);

除此之外,还定义了一个枚举类来标识组件有哪些状态,以及处在什么状态会触发什么样的事件。

LifcycleState:

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

核心就是状态变更与事件的监听

有了接口,就有具体的接口实现类。一般来说实现类不止一个,不同的实现类往往有部分相同的逻辑,如果每个子类都实现一遍就会出现重复代码。 子类如何重用这部分相同的逻辑呢?面向对象的核心之一继承,定义一个基类来实现共同逻辑(一般是抽象类)然后让子类继承它。

基类中往往会定义一些抽象方法,抽象方法基类不会去实现,而是调用这些方法来实现骨架逻辑,抽象方法留给子类去做自己独有的逻辑实现。

比如两辆车车的底盘和骨架相同,但是其发动机和内饰配套不同。那么底盘和骨架就是基类,这两辆车具体的品牌就是子类。每个品牌会在底盘和骨架上留出对应的安装接口,比如发动机,座椅,这些就是抽象方法。不同品牌或者不同配置的车发动机与座椅不同,对应的就是具体子类对抽象方法的不同实现。

Tomcat 定义了一个抽象基类 LifecycleBasse 实现 Lifecycle 接口。

将一些公共逻辑放入基类中。比如生命周期状态的转变与维护、生命事件的触发以及监听器的添加和删除等。而子类负责实现自己的初始化、启动和停止等方法。为了避免跟基类中的方法同名,我们把子类的实现方法改了个名字,在后面加上 Internal, 如 initInternal,startInternal 等。

LifecycleBase 实现了 Lifecycle 的 start 和 init, 定义了自己的 startInternal 和 initInternal 让子类实现。
这是典型的模板方法设计模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@Override
public final synchronized void init() throws LifecycleException {
//1. 状态检查
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}

try {
//2.触发INITIALIZING事件的监听器
setStateInternal(LifecycleState.INITIALIZING, null, false);

//3.调用具体子类的初始化方法
initInternal();

//4. 触发INITIALIZED事件的监听器
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
...
}
}

这个方法逻辑清晰,主要完成了四步:

  1. 检查状态是否为NEW。
  2. 触发 INITIALIZING 事件监听器,在setStateInternal 方法里会调用监听器的业务方法。

  1. 调用具体子类实现的抽象方法 initInternal 方法。 之前讲,为了实现一键式启动,这里使用了组合模式,具体组件在实现 initInternal 方法时又会调用它子组件的 init 方法。
  2. 子组件初始化后,触发 INITIALIZED 事件监听器,对应监听器的业务方法被调用。

总之,LifecycleBase 调用了抽象方法来实现骨架逻辑,负责触发事件,调用监听方法。

那么是什么时候,又是谁来完成注册监听器的工作呢?

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

生命周期管理总类图:

这里注意,StandardServer,StandardService 等是 Server 和 Service 组件的具体实现类,它们都继承了 LifecycleBase。

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

分享到:
0%