前言
说到 Tomcat 的启动,我们常需运行“tomcat/bin/startup.sh”脚本,但脚本内容究竟为何?不妨一探究竟。
启动脚本
startup.sh 脚本
该脚本中,两个重要变量
- “PRGDIR”指向脚本所在路径,
- “EXECUTABLE”指向“catalina.sh”脚本名称。其中,最关键代码“exec "EXECUTABLE" start "$@"”执行了“catalina.sh”脚本,并传入“start”参数。
catalina.sh 脚本
接下来,让我们深入探究“catalina.sh”脚本的实现。
该脚本虽然冗长,但我们只需关注“start”参数的处理逻辑。当传入“start”参数时,脚本会执行最后一行代码“org.apache.catalina.startup.Bootstrap "$@" start”,即调用“Bootstrap”类的“main”方法并传递“start”参数。接下来,让我们深入了解“Bootstrap”类的“main”方法是如何实现的。
Bootstrap.main
首先,我们进入“main”方法。
让我们关注“bootstrap.init()”代码段。
代码通过反射机制实例化“Catalina”类,并将实例引用赋值给“catalinaDaemon”。接下来,让我们关注“daemon.load(args);”部分。
Catalina.load
我们发现“daemon.load(args)”实际上是通过反射机制调用“Catalina”类的“load()”方法。接下来,让我们进入“Catalina”类的“load()”方法。
Server 初始化
在“Catalina”类的“load()”方法中,我们发现了“getServer.init()”方法,顾名思义,它是启动“Server”的初始化方法,而“Server”是图中最外层的容器。因此,让我们深入研究“getServer.init()”方法,即“LifecycleBase.init()”方法。该方法是一个模板方法,定义了一个算法框架,将一些细节算法留给子类实现。接下来,我们分析“LifecycleBase.init()”方法。
“Server”的实现类为“StandardServer”,我们来分析一下“StandardServer.initInternal()”方法。该方法用于对“Server”进行初始化,关键部分在于最后对“services”的循环操作,对每个“service”调用“init”方法。
[注:此处只粘贴代码片段]
“StandardServer.initInternal()”
调用“Service”子容器的“init”方法,使“Service”组件完成初始化。需要注意的是,同一个“Server”下面可能存在多个“Service”组件。
Service 初始化
“StandardService”和“StandardServer”都继承自“LifecycleMBeanBase”,因此公共的初始化逻辑相同,这里不做过多介绍。我们直接看“initInternal”方法:
“StandardService.initInternal()”
- 首先,将“StandardService”注册到 JMX 中。
- 然后,初始化“Engine”,而“Engine”在初始化过程中会初始化“Realm”(权限相关的组件)。
- 如果存在“Executor”线程池,还会进行“init”操作,该“Excecutor”是 Tomcat 的接口,继承自“java.util.concurrent.Executor”和“org.apache.catalina.Lifecycle”。
- 最后,初始化“Connector”连接器,默认包含“http1.1”和“ajp”连接器,而“Connector”初始化过程中会对“ProtocolHandler”进行初始化,开启应用端口监听,后面会详细分析。
Engine 初始化
以下是“StandardEngine”初始化的代码:
“StandardEngine”继承自“ContainerBase”,而“ContainerBase”重写了“initInternal()”方法,用于初始化“start”和“stop”线程池,该线程池具有以下特点:
- 核心线程数和最大线程数相等,默认为 1。
- 允许核心线程在超时未获取到任务时退出线程。
- 线程获取任务的超时时间为 10 秒,也就是说所有线程(包括核心线程),超过 10 秒未获取到任务,就会被销毁。
这么做的目的是因为该线程池只需要在容器启动和停止时发挥作用,没有必要时时刻刻处理任务队列。
以下是“ContainerBase”的代码:
“startStopExecutor”线程池的作用是在容器启动和停止时,将子容器的启动和停止操作放入线程池中进行处理。
- 在启动时,如果发现有子容器,则会将子容器的启动操作放入线程池中处理。
- 在停止时,也会将停止操作放入线程池中处理。
在之前的文章中我们介绍了“Container”组件,“StandardEngine”作为顶层容器,它的直接子容器是“StandardHost”。但是,在对“StandardEngine”代码的分析中,我们并没有发现它会对子容器“StandardHost”进行初始化操作。 “StandardEngine”不按套路出牌,而是将初始化过程放在启动阶段。
个人认为,“Host”、“Context”、“Wrapper”这些容器与具体的 Web 应用相关联,初始化过程会更加耗时。因此,在启动阶段使用多线程完成初始化和启动生命周期,否则,像顶层的“Server”、“Service”等组件需要等待“Host”、“Context”、“Wrapper”完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。
“Connector”的初始化将在后面专门的“Connector”文章中讲解。
结束
至此,整个初始化过程便告一段落。整个初始化过程,由父组件控制子组件的初始化,一层层往下传递,直到最后全部初始化完成。下图描述了整体的传递流程。
图片
默认情况下,Server 只有一个 Service 组件,Service 组件先后对 Engine 和 Connector 进行初始化。而 Engine 组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper 容器的初始化是在启动阶段完成的。Tomcat 默认会启用 HTTP1.1 和 AJP 的 Connector 连接器,这两种协议默认使用 Http11NioProtocol 和 AJPNioProtocol 进行处理。