《深入拆解Tomcat&Jetty》总结三:Tomcat的启停

Tomcat&Jetty 专栏收录该内容
8 篇文章 1 订阅

4 Tomcat实现一键式启停

回顾一下总体架构和一个请求的流转过程:
在这里插入图片描述
想让一个系统能够对外提供服务,需要创建、组装并启动这些组件;
在服务停止的时候,还需要释放资源,销毁这些组件,因此这是一个动态的过程。
也就是说,Tomcat需要动态地管理这些组件的生命周期。

4.1 一键式启停:LifeCycle接口

最直观的做法就是将图上所有的组件按照先小后大、先内后外的顺序创建出来,然后组装在一起。
但是这样不仅会造成代码逻辑混乱和组件遗漏,而且也不利于后期的功能扩展。

设计就是要找到系统的变化点和不变点。
这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程,这些状态以及状态的转化是不变的。
而变化点是每个具体组件的初始化方法,也就是启动方法是不一样的。

因此把不变点抽象出来成为一个接口,这个接口跟生命周期有关,叫作LifeCycle。
LifeCycle接口里应该定义这么几个方法:init()、start()、stop()和destroy(),每个具体的组件去实现这些方法。

而具体实现就是:

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

4.2 可扩展性:LifeCycle事件

各个组件init()和start()方法的具体实现是复杂多变的,比如在Host容器的启动方法里需要扫描webapps目录下的Web应用,创建相应的Context容器,如果将来需要增加新的逻辑,需要直接修改start()方法,这样会违反开闭原则。

那如何解决这个问题呢?开闭原则说的是为了扩展系统的功能,你不能直接修改系统中已有的类,但是你可以定义新的类。
注意,组件的init()和start()调用是由它的父组件的状态变化触发的,上层组件的初始化会触发子组件的初始化,上层组件的启动会触发子组件的启动,因此可以把组件的生命周期定义成一个个状态,把状态的转变看作是一个事件,而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便的添加和删除,这就是典型的观察者模式
具体实现就是:

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

4.3 重用性:LifeCycleBase抽象基类(骨架抽象类)

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

LifeCycleBase实现了LifeCycle接口中所有的方法,还定义了相应的抽象方法交给具体子类去实现,这是典型的模板设计模式,如其init():

@Override
public final synchronized void init() throws LifecycleException {
    //1. 状态检查,init方法中,当前状态必须是new才能进行初始化
    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) {
    	//...
    }
}

LifeCycleBase负责触发事件,并调用监听器的方法,那是什么时候、谁把监听器注册进来的呢?

1 Tomcat自定义了一些监听器,这些监听器是父组件在创建子组件的过程中注册到子组件的。
比如MemoryLeakTrackingListener监听器,用来检测Context容器中的内存泄漏,这个监听器是Host容器在创建Context容器时注册到Context中的。

2 可以在server.xml中定义自己的监听器,Tomcat在启动时会解析server.xml,创建监听器并注册到
容器组件。

4.4 总结

Tomcat组件生命周期的管理总体类图:
在这里插入图片描述
StandardServer、StandardService等是Server和Service组件的具体实现类,它们都继承了LifeCycleBase。

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

5 Tomcat启动过程

通过Tomcat的/bin目录下的脚本startup.sh来启动Tomcat,其流程为:
在这里插入图片描述

1.Tomcat本质上是一个Java程序,因此startup.sh脚本会启动一个JVM来运行Tomcat的启动类Bootstrap。
2.Bootstrap的主要任务是初始化Tomcat的类加载器,并且创建Catalina。
3.Catalina是一个启动类,它通过解析server.xml、创建相应的组件(如四大容器),并调用Server的start方法。
4.Server组件的职责就是管理Service组件,它会负责调用Service的start方法。
5.Service组件的职责就是管理连接器和顶层容器Engine,因此它会调用连接器和Engine的start方法。

5.1 Catalina

主要任务:

解析server.xml创建组件,然后调用Server组件的init方法和start方法,这样整个Tomcat就启动起来了

最后要处理异常,如通过“Ctrl +C”关闭Tomcat时,如何优雅地停止并且清理资源呢?通过”关闭钩子“
Catalina在JVM中注册一个“关闭钩子”

public void start() {
    //1. 如果持有的Server实例为空,就解析server.xml创建出来
    if (getServer() == null) {
    	load();
    }
    //2. 如果创建失败,报错退出
    if (getServer() == null) {
        log.fatal(sm.getString("catalina.noServer"));
        return;
    }
    //3.启动Server
    try {
    	getServer().start();
    } catch (LifecycleException e) {
    	return;
    }
    //创建并注册关闭钩⼦
    if (useShutdownHook) {
        if (shutdownHook == null) {
        	shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
    //⽤await⽅法阻塞监听停⽌请求
    if (await) {
        await();
        stop();
    }
}

“关闭钩子”其实就是一个线程,JVM在停止之前会尝试执行这个线程的run方法
CatalinaShutdownHook:执行了Server的stop方法来释放和清理所有的资源。

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

5.2 Server组件

具体实现类StandServer:
1 在启动时调用Service组件的启动方法,在停止时调用它们的停止方法。
2 启动一个Socket来监听停止端口

Server在内部维护了若干Service组件,它是以数组来保存的,在添加的过程中动态地扩展数组长度,即当添加一个新的Service实例时,会创建一个新数组并把原来数组内容复制到新数组,目的是为了节省内存空间,虽然时间变久了点,但是值得,因为Service并不会很多,没必要预分配,源码中是0个,可以预分配一个(issue);
添加过程中,如果成功添加,则会启动添加的Service,最后触发监听器

5.3 Service组件

具体实现类StandardService:

public class StandardService extends LifecycleBase implements Service {
    //名字
    private String name = null;
    //Server实例
    private Server server = null;
    //连接器数组
    protected Connector connectors[] = new Connector[0];
    private final Object connectorsLock = new Object();
    //对应的Engine容器
    private Engine engine = null;
    //映射器及其监听器
    protected final Mapper mapper = new Mapper();
    //Tomcat支持热部署,当Web应用的部署发生变化时,Mapper中的映射信息也要跟着变化,MapperListener就是一个监听器,它监听容器的变化,并把信息更新到Mapper中,这是典型的观察者模式。
    protected final MapperListener mapperListener = new MapperListener(this);

启动时需要注意顺序:Service先启动了Engine组件,再启动Mapper监听器,最后才是启动连接器

5.4 Engine组件

具体实现类StandardEngine

Engine的子容器是Host,所以它持有了一个Host容器的数组,这些功能都被抽象到了ContainerBase中:

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

ContainerBase用HashMap保存了它的子容器,并且ContainerBase还实现了子容器的“增删改查”,甚至
连子组件的启动和停止都提供了默认实现,比如ContainerBase会用专门的线程池来启动子容器。

Engine自己解决了对请求的“处理”,就是把请求转发给某一个Host子容器来处理,具体是通过Valve来实现的;
其基础阀BasicValve定义如下:

final class StandardEngineValve extends ValveBase {
    public final void invoke(Request request, Response response) throws IOException, ServletException {
        //拿到请求中的Host容器
        Host host = request.getHost();
        if (host == null) {
            return;
        }
        // 调⽤Host容器中的Pipeline中的第⼀个Valve
        host.getPipeline().getFirst().invoke(request, response);
    }
}

请求对象中为什么会有Host容器呢?
因为请求到达Engine容器中之前,Mapper组件已经对请求进行了路由处理,Mapper组件通过请求的URL定位了相应的容器,并且把容器对象保存到了请求对象中。

问题:Server组件的在启动连接器和容器时,都分别加了锁,这是为什么呢?

加锁通常的场景是存在多个线程并发操作不安全的数据结构。

不安全的数据结构:
Server本身包含多个Service,内部使用数组来存储这些Service,数组的并发操作(包含缩容,扩容)是不
安全的。所以,在并发操作(添加/修改/删除/遍历等)services数组时,需要进行加锁处理。

可能存在并发操作的场景:
Tomcat提供MBean的机制对管理的对象进行并发操作,如添加/删除某个service。

  • 2
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

木棉上的光

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值