这些年一直在做大厂项目,已经很少接触 Spring MVC 了,毕竟大厂基本都在使用 RPC,即便是给前端提供的接口。不过,作为 Java 世界中最常用的 HTTP 和 Servlet 容器,Tomcat 还是值得认真学习的。因此,本文将基于 tomcat-9.0.88 源码,详细剖析该软件的核心原理。
通过 IDEA 2021 编译 Tomcat 9
Tomcat 官方通过 ant 编译工程,由于大部分国内开发者对 Maven 更熟悉,因此这里我们选择修改源码,以便通过 Maven 进行编译。编译过程中有以下几点需要注意:
- 该项目需要使用 JDK 17 或更高版本。
经不完全测试,开发工具均为 IntelliJ IDEA 2021.3.3 (Ultimate Edition) 的情况下,Windows 10 平台需要使用 JDK 17,Windows 11 平台需要使用 JDK 22(语言级别选择 X)。
- 为了方便测试,我们可以在
org.apache.catalina.startup.ContextConfig
中启用 JSP 功能。 - 为了通过编译,需要注释掉
org.apache.jasper.compiler.JDTCompiler
中不存在的常量。 - 启动
org.apache.catalina.startup.Bootstrap
时,需要添加以下 JVM 参数:1
2
3
4
5
| -Dcatalina.home=catalina-home
-Dcatalina.base=catalina-home
-Djava.io.tmpdir=catalina-home\temp
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=catalina-home\conf\logging.properties
|
- catalina-home 是我自己创建的,内部是原先根目录中的 conf 与 webapps 文件夹。
我已经将该项目的工程文件上传到了 Github:LoongmaSpirit/tomcat-9.0.88,直接下载即可使用。
Tomcat 系统架构
Tomcat 的本质是一个结合了网关和 Servlet 容器功能的服务器,其主要职责是处理来自网络的请求与响应,并在此过程中与内部的 Servlet 服务进行数据交换。它主要包含以下几个组件:
- 负责处理网络请求,并将其转换为 Servlet 标准请求的 Coyote 组件;
- 负责处理 Servlet 标准请求,并将其转发给各个 Servlet 服务的 Catalina 组件;
- 负责处理 JSP 的 Jasper 组件;
- 负责处理命名服务的 Naming 组件;
- 负责处理日志的 Juli 组件。
1
2
3
4
5
6
7
8
9
10
11
| +--------------------------------------+
| +--------------+ +--------------+ |
| | Coyote | | Catalina | |
| | ------------ | | ------------ | |
| | http & AJP | | Servlet... | |
| +--------------+ +--------------+ |
| |
| +--------+ +--------+ +------+ +---+ |
| | Jasper | | Naming | | Juli | |...| |
| +--------+ +--------+ +------+ +---+ |
+--------------------------------------+
|
这些组件中的核心是 Coyote 和 Catalina,其工作流程大致如下:
1
2
3
4
5
6
7
8
| +---------------------------------------------------------------+
| Coyote Catalina |
| +----------+ ServletRequest +-----------------+ |
| net req -->| transfer |----------------->| --------------+ | |
| | | | do something | | |
| net resp <--| transfer |<-----------------| <-------------+ | |
| +----------+ ServletResponse +-----------------+ |
+---------------------------------------------------------------+
|
接下来我们详细介绍这两个组件。
Coyote ProtocolHandler
Coyote ProtocolHandler 是 Tomcat 中处理网络协议的组件,没有它我们的 Spring MVC 将无法接收请求。其支持的协议和模型如下表所示:
- 应用层:
协议 | 说明 |
---|
HTTP/1.1 | 目前兼容性较强的协议,正在被淘汰 |
HTTP/2 | 目前的主流协议,支持二进制数据流传输,性能更高 |
AJP | 用于和 Apache 集成,以实现对静态资源的优化以及集群部署 |
- 传输层:
模型 | 说明 |
---|
NIO | 多路复用 I/O 模型,Tomcat 默认模型 |
NIO2 | 异步 I/O 模型,由于在 Linux 平台上没有性能优势,所以一般不用 |
APR | 将 I/O 操作委托给下层 Apache 的 APR C++ 库,性能也很强,但需要额外安装 |
为了降低组件之间的耦合度,Coyote 内部又设计了 EndPoint、Processor 和 Adapter 三个组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| +------------------------------------------------------------------------------+
| Coyote ProtocolHandler |
| +----------+ +-----------+ |
| req --> | EndPoint | | Processor | Request +---------+ ServletRequest |
| | -------- | -> | --------- | -------->| Adapter | --------------+ |
| | TCP/IP | | HTTP/AJP | +---------+ O |
| +----------+ +-----------+ | |
| Servlet |
| +----------+ +-----------+ | |
| | EndPoint | | Processor | Response +---------+ O |
| | -------- | <- | --------- | <------- | Adapter | <-------------+ |
| resq <-- | TCP/IP | | HTTP/AJP | +---------+ ServletResponse |
| +----------+ +-----------+ |
+------------------------------------------------------------------------------+
|
- EndPoint:传输层通信端点,基于各种 I/O 模型实现对传输层数据的处理;
- Processor:网络层处理器,用于处理 HTTP/1.1、HTTP/2 等网络层协议数据;
- Adapter:适配器,用于解耦 Coyote 与 Servlet 容器。Processor 组件生成的 Request 并不能直接与 Servlet 容器交互,因此需要通过该适配层做转换。
Catalina 组件
Catalina 是 Tomcat 中统筹全局的组件,它负责解析 server.xml 并管理文件中配置的所有 Servlet 容器和 Connector。其整体架构如下:
![catalina](/blog/posts/core-principles-of-apache-tomcat-9/images/catalina.webp)
- Catalina 实例:负责解析 server.xml 配置文件,然后根据配置内容创建 Server 实例并管理;
- Server 实例:负责管理 Servlet Engine、Connector 的生命周期;
- Service 实例:负责将若干个 Connector 绑定到 Container,一个 Server 可以包含多个 Service;
- Connector 实例:与 Service 关联的连接器,内部封装了 Coyote ProtocolHandler 实例;
- Container:容器的抽象接口,其下包含
StandardEngine
、StandardHost
、StandardContext
、StandardWrapper
四个默认实现。且这四个容器实现类之间是有层级关系的(业务层级,非继承关系):- Engine:表示 Servlet 引擎,默认实现为
StandardEngine
。负责将请求转发给目标 Host,一个 Container 中只有一个 Engine,一个 Engine 内可以有多个 Host;- Host:表示虚拟主机,默认实现为
StandardHost
。Tomcat 支持部署多个虚拟主机,且每个虚拟主机下可以包含多个 Context;- Context:表示一个 Web 应用,默认实现为
StandardContext
;- Wrapper:Servlet 包装器,默认实现为
StandardWrapper
,用于包装 War 包等地方的 Servlet;
- Pipeline:容器内的管道,默认实现为
StandardPipeline
,用于将不同层级的容器串联起来,构成一个“责任链”; - Valve:管道阀门,即 Pipeline 责任链上的处理器。每种容器都有一个默认的 Valve 实现,包括
StandardContextValve
、StandardEngineValve
、StandardHostValve
和 StandardWrapperValve
;
- Listener:生命周期事件的监听器。
其 UML 类图如下:
![catalina uml](/blog/posts/core-principles-of-apache-tomcat-9/images/catalina-uml.webp)
Tomcat 核心配置文件
接下来我们通过 tomcat 源码中的 server.xml,分析 Tomcat 的核心配置项:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| <?xml version="1.0" encoding="UTF-8"?>
<!-- 根标签,每个 Catalina 内部只有一个 Server -->
<Server port="8005" shutdown="SHUTDOWN">
<!-- 输出各种版本信息到日志 -->
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- 管理 APR 库,找不到就输出个日志 -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!-- 防止特定的 java/javax API 导致内存泄露 -->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<!-- 管理全局命名服务 -->
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<!-- 在 Context 停止时重建 Executor,避免 ThreadLocal 相关的内存泄露 -->
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- 全局命名服务配置,主要配置数据库连接池、JNDI 资源等 -->
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<!-- 创建一个名为 Catalina 的 Service 实例,默认使用 org.apache.catalina.core.StandardService-->
<Service name="Catalina">
<!-- 供 Connector 使用的共享线程池 -->
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
<!-- 配置 Connector 连接器,可以配置多个,监听多个端口 -->
<Connector executor="tomcatThreadPool"
port="80" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxParameterCount="1000"/>
<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true"
maxParameterCount="1000">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/localhost-rsa.jks" type="RSA" />
</SSLHostConfig>
</Connector>
<!-- 配置名称为 Catalina 的 Servlet 引擎,当客户端指向的 host 无效时交给 defaultHost 处理 -->
<Engine name="Catalina" defaultHost="localhost">
<!--认证配置 -->
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<!-- 配置虚拟主机 -->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
|
可见,该配置的层级与 Tomcat 架构层级基本一致:
1
2
3
4
5
6
7
8
9
10
11
| Server
├── Listener
├── GlobalNamingResources
│ └─ Resource
└── Service
├── Executor
├── Connector
└── Engine
├── Realm
└── Host
└── Valve
|
了解了以上基础内容,接下来我们开始剖析 Tomcat 源码。
Lifecycle 生命周期管理
Tomcat 内部有大量的组件需要管理,因此其抽象出了一个 Lifecycle
接口,来优雅的管理这些组件的生命周期:
方法 | 说明 |
---|
void init() | 初始化组件 |
void start() | 启动组件 |
void stop() | 停止组件 |
void destroy() | 销毁组件 |
Lifecycle 有限状态机
与这四个方法相关联的是一个有限状态机,其状态流转过程如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| start()
-----------------------------
| |
| init() |
NEW -»-- INITIALIZING |
| | | | ------------------«-----------------------
| | |auto | | |
| | \|/ start() \|/ \|/ auto auto stop() |
| | INITIALIZED --»-- STARTING_PREP --»- STARTING --»- STARTED --»--- |
| | | | |
| |destroy()| | |
| --»-----«-- ------------------------«-------------------------------- ^
| | | |
| | \|/ auto auto start() |
| | STOPPING_PREP ----»---- STOPPING ------»----- STOPPED -----»-----
| \|/ ^ | ^
| | stop() | | |
| | -------------------------- | |
| | | | |
| | | destroy() destroy() | |
| | FAILED ----»------ DESTROYING ---«----------------- |
| | ^ | |
| | destroy() | |auto |
| --------»----------------- \|/ |
| DESTROYED |
| |
| stop() |
----»-----------------------------»------------------------------
|
- 组件的初始状态为
NEW
,此时:- 调用
init()
方法,组件状态将由 NEW
转换为 INITIALIZING
,待初始化完成时自动转换为 INITIALIZED
; - 也可以直接调用
start()
方法,Tomcat 会自动先调用 init()
,然后进入 STARTING_PREP
状态,最后在启动过程中自动完成从 STARTING_PREP
-> STARTING
到 STARTED
的转换; - 还可以直接调用
stop()
方法,然后直接进入 STOPPED
状态;
INITIALIZED
状态的组件通过调用 start()
方法,可以进入启动流程;STARTED
状态的组件可以通过调用 stop()
方法进入 STOPPING_PREP
状态,然后在停止过程中自动完成从 STOPPING_PREP
-> STOPPING
到 STOPPED
的转换;STOPPED
状态的组件可以通过调用 start()
方法重新进入启动流程;- 不论组件处于何种状态,都可以通过调用
destroy()
方法进入 DESTROYING
状态,待资源释放后自动进入 DESTROYED
状态; - 任何状态都可以直接转变为
FAILED
,处于 FAILED
状态的组件:- 可以通过调用
stop()
方法进入停止流程; - 也可以通过调用
destroy()
方法进入销毁流程。
Lifecycle 实现原理
Lifecycle 体系结构如下:
![lifecycle uml](/blog/posts/core-principles-of-apache-tomcat-9/images/lifecycle.webp)
该体系包含以下两个机制:
- JMX (Java Management Extensions) MBean 管理机制:用于注册容器与组件;
- Lifecycle 管理机制:完成容器或组件状态的修改,并发送相应的事件给监听者。
这里我们重点关注 Lifecycle 机制,以 init 过程为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // org.apache.catalina.util.LifecycleBase#init()
@Override
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(BEFORE_INIT_EVENT);
}
try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}
|
忽略状态校验,该方法首先将对象的状态设置为 INITIALIZING,然后执行模板方法(由组件或容器实现)完成真正的初始化操作,最终将状态设置为 INITIALIZED,与前文介绍的状态机完全一致。这里我们重点关注状态更新的方法 setStateInternal()
:
1
2
3
4
5
6
7
8
9
10
11
| // org.apache.catalina.util.LifecycleBase#setStateInternal
private synchronized void setStateInternal(LifecycleState state, Object data, boolean check) throws LifecycleException {
...
// 设置状态,LifecycleState 就是个状态枚举
this.state = state;
String lifecycleEvent = state.getLifecycleEvent();
if (lifecycleEvent != null) {
// 发送事件
fireLifecycleEvent(lifecycleEvent, data);
}
}
|
该方法首先将状态缓存,然后调用 fireLifecycleEvent()
发送状态变更事件:
1
2
3
4
5
6
7
8
9
10
| // org.apache.catalina.util.LifecycleBase:
private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
|
fireLifecycleEvent
方法再遍历所有监听了该对象的 Listener,依次触发预先注册的回调,以响应事件。
对于容器或组件来说,这些监听器在关键时刻能够发挥重要作用。例如,下文将介绍的 HostConfig
,它就是通过监听 Host 的 START 事件来加载 WEB-INF 配置文件的。
Tomcat 启动流程解析
警告
本文只列出关键的代码段,不影响主线的代码将直接省略。
从 bin/catalina.sh 启动脚本可知,Tomcat 程序的启动入口是 org.apache.catalina.startup.Bootstrap
:
1
| org.apache.catalina.startup.Bootstrap "$@" start
|
Bootstrap
会在 main()
方法中完成 Tomcat 初始化与加载流程:
1
2
3
4
5
6
7
8
9
10
11
12
| public static void main(String args[]) {
Bootstrap bootstrap = new Bootstrap();
// 初始化 Bootstrap,内部会创建 Catalina 实例
bootstrap.init();
daemon = bootstrap;
...
// 然后调用 load 方法初始化 Catalina 以及其下实例
daemon.load(args);
// 最后启动 Catalina 以及其下实例
daemon.start();
}
|
Bootstrap#init
首先我们来看初始化过程,它会构造加载过程中用到的 Common Classloader、Server Classloader 和 Shared Classloader(实际上指向了同一个 URLClassLoader
),并创建 Catalina 组件:
1
2
3
4
5
6
7
8
9
10
11
12
| // org.apache.catalina.startup.Bootstrap#init()
public void init() throws Exception {
// 初始化类加载器
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
...
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
// 创建一个 Catalina 实例
Object startupInstance = startupClass.getConstructor().newInstance();
...
}
|
类加载器相关的内容,将在 下文 讲解。
Bootstrap#load
然后就是用于初始化 Catalina 的 org.apache.catalina.startup.Bootstrap#load()
方法:
1
2
3
4
5
6
7
| private void load(String[] arguments) throws Exception {
String methodName = "load";
...
Method method = catalinaDaemon.getClass().getMethod(methodName, paramTypes);
...
method.invoke(catalinaDaemon, param);
}
|
Bootstrap.load
方法通过反射调用了 Catalina 内部的 load()
方法。
Catalina#load
Catalina#load
内部首先完成了对核心配置文件 server.xml 的解析,然后又调用了 Server 的 init 方法:
1
2
3
4
5
6
7
| // org.apache.catalina.startup.Catalina#load()
public void load() {
// 解析 server.xml
parseServerXml(true);
// 初始化 server 实例
getServer().init();
}
|
Server#initInternal
从 Server 这一层开始,初始化过程就交给 Lifecycle 管理了,以标准实现 StandardServer
为例,它会实现 Lifecycle
的 initInternal
模板方法完成初始化操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // org.apache.catalina.core.StandardServer#initInternal
@Override
protected void initInternal() throws LifecycleException {
// 将自己注册到 JMX
super.initInternal();
onameStringCache = register(new StringCache(), "type=StringCache");
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
onameMBeanFactory = register(factory, "type=MBeanFactory");
// 全局命名空间初始化
globalNamingResources.init();
// 加载扩展验证程序
if (getCatalina() != null) {
ClassLoader cl = getCatalina().getParentClassLoader();
//while ...
ExtensionValidator.addSystemResource(f);
//...
}
// 初始化 Service
for (Service service : findServices()) {
service.init();
}
}
|
Server 是管理各容器和组件的核心,负责完成其下所有 Service 的初始化。
Service#initInternal
然后 Service 又完成了 Engine 容器和 Connector 等组件的初始化,以标准实现 StandardService
为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // org.apache.catalina.core.StandardService#initInternal
@Override
protected void initInternal() throws LifecycleException {
// 注册到 JMX MBean 管理器
super.initInternal();
// 然后完成下层引擎的初始化
if (engine != null) {
engine.init();
}
// 完成 Executor、Connector 的初始化
...
for (Connector connector : findConnectors()) {
connector.init();
}
}
|
Engine#initInternal
Engine 的初始化相对简单,主要是完成 Realm 的设置,并将自身注册到 JMX:
1
2
3
4
5
6
| // org.apache.catalina.core.StandardEngine#initInternal
@Override
protected void initInternal() throws LifecycleException {
getRealm();
super.initInternal();
}
|
Catalina 大模组中与 Servlet 容器有关的初始化过程,到了 Engine 这一层其实就停止了!接下来我们再关注用于管理 Coyote 组件的 Connector 的启动流程。
Connector#initInternal
Connector 组件在初始化过程中,首先将自身注册到 JMX,然后完成了 Coyote ProtocolHandler 的初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // org.apache.catalina.connector.Connector#initInternal
@Override
protected void initInternal() throws LifecycleException {
// 1. 将自己注册到 JMX
super.initInternal();
...
// 2. 创建 Coyote Adapter,将 Connector 存入字段
adapter = new CoyoteAdapter(this);
// protocolHandler 在 Connector 构造方法中这创建
protocolHandler.setAdapter(adapter);
...
try {
// 3. 初始化 protocolHandler,内部完成 Endpoint 的创建
protocolHandler.init();
} ...
}
|
其中:
CoyoteAdapter
是 Connector 组件与 Coyote 交互的适配器,是 org.apache.coyote.Adapter
的实现;为了不让 Coyote 依赖 Servlet,这里将 Adapter 适配层的实现放到了 Catalina 的 Connector 组件中,并交由给 Coyote 使用。
- ProtocolHandler 即前文介绍的 Coyote 组件,其
init()
方法中完成了 Endpoint 组件的初始化:1
2
3
4
5
6
7
| // org.apache.coyote.AbstractProtocol#init
// JMX 注册。..
// 传输层处理器初始化
String endpointName = getName();
endpoint.setName(endpointName.substring(1, endpointName.length() - 1));
endpoint.setDomain(domain);
endpoint.init();
|
最终,Endpoint 在其 init()
方法中完成了端口绑定,并将自己注册到了 JMX:
1
2
3
4
5
| // org.apache.tomcat.util.net.AbstractEndpoint#init
// 默认调用 nio 模型的 org.apache.tomcat.util.net.NioEndpoint#initServerSocket
bindWithCleanup();
// 注册到 JMX
...
|
至此,整个 bootstrap.load
流程就结束了,此过程中涉及的所有组件的状态都变成了 INITIALIZED。
bootstrap.start
初始化完成后,继续回到 Bootstrap
完成后续的启动流程。Bootstrap#start
首先通过反射调用了 Catalina
实例的 start()
方法:
1
2
3
4
5
6
7
8
9
| // org.apache.catalina.startup.Bootstrap#start
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
// 这里调用的是 Catalina 的 start
Method method = catalinaDaemon.getClass().getMethod("start", (Class[]) null);
method.invoke(catalinaDaemon, (Object[]) null);
}
|
Catalina#start
紧接着,Catalina 完成了 Server 的启动:
1
2
3
| public void start() {
getServer().start();
}
|
Server#startInternal
从这一层开始,启动过程就交给 Lifecycle
管理了,以标准实现 StandardServer
为例,它会实现 Lifecycle 的 startInternal
模板方法完成启动过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // org.apache.catalina.core.StandardServer#startInternal
@Override
protected void startInternal() throws LifecycleException {
// 发送启动事件
fireLifecycleEvent(CONFIGURE_START_EVENT, null);
// 修改 Server 状态为“启动中”
setState(LifecycleState.STARTING);
...
// 启动所有 Service
for (Service service : findServices()) {
service.start();
}
...
}
|
Server 首先修改了自身的状态,然后启动了其下所有的 Service。
Service#startInternal
Service 又完成了内部的 Engine、Executor 和 Connector 的启动:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // org.apache.catalina.core.StandardService#startInternal
@Override
protected void startInternal() throws LifecycleException {
// 启动 Servlet 引擎
if (engine != null) {
engine.start();
}
// 启动通用执行器
for (Executor executor : findExecutors()) {
executor.start();
}
// 启动连接器
for (Connector connector : findConnectors()) {
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
}
}
|
Engine#startInternal
首先关注 Engine 启动流程,以默认实现 StandardEngine
为例:
1
2
3
4
| @Override
protected void startInternal() throws LifecycleException {
super.startInternal();
}
|
StandardEngine
调用了其父类中的 org.apache.catalina.core.ContainerBase#startInternal()
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Override
protected void startInternal() throws LifecycleException {
// 查找 Engine 的子容器,也就是 StandardEngine[Catalina].StandardHost[localhost]
Container[] children = findChildren();
List<Future<Void>> results = new ArrayList<>(children.length);
// 提交给执行器去启动
for (Container child : children) {
results.add(startStopExecutor.submit(new StartChild(child)));
}
// 同步等待启动结果
for (Future<Void> result : results) {
result.get();
}
}
|
该方法通过 StartChild()
调用,完成了 Host 容器的初始化与启动过程。
Host#startInternal
Host 的初始化过程就是将自己注册到 JMX,不再赘述。这里我们重点关注其启动过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // org.apache.catalina.core.StandardHost#startInternal
@Override
protected void startInternal() throws LifecycleException {
// 判断用户是否配置了 ErrorReportValve
String errorValve = getErrorReportValveClass();
if ((errorValve != null) && (!errorValve.equals(""))) {
try {
boolean found = false;
// 获取 StandardPipe 上配置的阀门
Valve[] valves = getPipeline().getValves();
for (Valve valve : valves) {
if (errorValve.equals(valve.getClass().getName())) {
// 找到了就 ok 了
found = true;
break;
}
}
if (!found) {
// 找不到就新建一个
Valve valve = ErrorReportValve.class.getName().equals(errorValve) ? new ErrorReportValve() :
(Valve) Class.forName(errorValve).getConstructor().newInstance();
getPipeline().addValve(valve);
}
} //catch...
}
// 然后完成 Host 启动
super.startInternal();
}
|
- 该方法首先判断用户是否配置了
ErrorReportValve
,如果没有配置就新建一个追加到 StandardPipe
; - 然后调用
ContainerBase
的 startInternal()
方法启动 Host 容器。
HostConfig 监听 Host 启动
Host 在 ContainerBase#startInternal
方法中仅启动了自己,因为与 Context 相关的配置并不在核心配置文件中,而是位于 appdir/META-INF/context.xml
中。因此,Tomcat 使用 HostConfig
监听 StandardHost
的生命周期,当 Host 处于 STARTING
状态时,触发回调解析 webapps 目录下所有的 WAR 包或目录中的 context.xml
,并启动 Context。
以解析目录中的应用程序为例,其核心实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
| // org.apache.catalina.startup.HostConfig#deployDirectories
protected void deployDirectories(File appBase, String[] files) {
for (String file : files) {
File dir = new File(appBase, file);
if (dir.isDirectory()) {
// 创建 Context 名称,即目录路径最后一层
ContextName cn = new ContextName(file, false);
// 将解析任务移交给执行器
results.add(es.submit(new DeployDirectory(this, cn, dir)));
}
}
}
|
其中,解析应用目录的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // org.apache.catalina.startup.HostConfig#deployDirectory
// cn: centext name,例如 master
// dir: app 所在目录
protected void deployDirectory(ContextName cn, File dir) {
// context 句柄
Context context = null;
File xml = new File(dir, Constants.ApplicationContextXml);
// 解析应用下的 META-INF 下的 context.xml,并实例化
context = (Context) digester.parse(xml);
// 向 Context 插入生命周期监听器
Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);
// 将 context 关联至所属的 Host,并调用 context 的 start 方法
host.addChild(context);
}
|
这里的流程包含大量回调,跟踪起来比较麻烦。我们只需知道它解析了 context.xml,并在 host.addChild(context)
方法中调用了 Context#startInternal
即可。
Context#startInternal
Context 负责完成 Servlet 容器的初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // org.apache.catalina.core.StandardContext#startInternal
@Override
protected void startInternal() throws LifecycleException {
getServletContext().setAttribute(Globals.RESOURCES_ATTR, getResources());
...
// 加载私有的 ClassLoader
Loader loader = getLoader();
if (loader.getClassLoader() instanceof WebappClassLoaderBase) {...}
...
// 初始化 Servlet 容器
entry.getKey().onStartup(entry.getValue(), getServletContext());
...
// 通过 Wrapper 包装器启动 Servlet
if (!loadOnStartup(findChildren())) {...}
}
|
Context 首先为每个 Web APP 创建了私有的 ClassLoader,然后调用 Servlet 标准的容器初始化工作,最后通过 Wrapper 包装器启动了 Servlet:
1
2
3
4
5
6
7
| public boolean loadOnStartup(Container children[]) {
...
for (Wrapper wrapper : list) {
try {
wrapper.load();
...
}
|
Connector#startInternal
了解了 Servlet 启动流程,我们再回到 Connector 的启动流程:
1
2
3
4
5
| // org.apache.catalina.connector.Connector#startInternal
@Override
protected void startInternal() throws LifecycleException {
protocolHandler.start();
}
|
Connector 主要负责 ProtocolHandler 的启动,而 ProtocolHandler 又主要负责 Endpoint 的启动:
1
2
3
4
5
| // org.apache.coyote.AbstractProtocol#start
@Override
public void start() throws Exception {
endpoint.start();
}
|
以默认的 NioEndpoint
为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // org.apache.tomcat.util.net.NioEndpoint#startInternal
@Override
public void startInternal() throws Exception {
// 创建供 Worker 使用的执行器
if (getExecutor() == null) {
createExecutor();
}
// 启动 Worker 循环
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-Poller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
// 启动 Acceptor 循环
startAcceptorThread();
}
|
最终,NioEndpoint
启动了一个处理连接请求的 Acceptor
线程和一个处理数据读写的 Worker 线程。因此我们可以得出结论,Tomcat 在传输层使用的是 主从 Reactor 多线程模型,理论上具有很高的吞吐量。
至此,bootstrap.start 流程就结束了,此过程中涉及的所有组件的状态都变成了 STARTED。
Tomcat 请求处理流程解析
接下来,我们以一次 GET 请求为例,详细介绍 Tomcat 处理 HTTP 请求的流程。
Acceptor 处理连接请求
上文讲过,NioEndpoint
有一个单独的 Reactor 线程不断地处理连接请求:
1
2
3
4
5
| // org.apache.tomcat.util.net.Acceptor#run
// 1. 获取连接的 SocketChannel
socket = endpoint.serverSocketAccept();
// 2. 创建 Buffer 并绑定给 Socket
endpoint.setSocketOptions(socket)
|
当有请求到达时,Acceptor 会调用 org.apache.tomcat.util.net.NioEndpoint#setSocketOptions
创建用于读写的数据缓冲区,并构造 SocketWrapper
包装类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| protected Map<U, SocketWrapperBase<S>> connections = new ConcurrentHashMap<>();
@Override
protected boolean setSocketOptions(SocketChannel socket) {
...
NioSocketWrapper socketWrapper = null;
NioChannel channel = null;
// 创建用于读写的 ByteBuffer
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
// 利用 Buffer 创建 NioChannel
channel = new NioChannel(bufhandler);
// 构造包装类,包装类内部缓存了 nioChannels、poller、socketBufferHandler 等
NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);
channel.reset(socket, newWrapper);
// 映射 SocketChannel 与 Buffer 包装类
connections.put(socket, newWrapper);
socketWrapper = newWrapper;
// 监听 socket 的可读事件
poller.register(socketWrapper);
}
|
最终通过 Selector 监听 Socket 的可读事件,等待客户端请求到达:
1
2
3
4
5
| public void register(final NioSocketWrapper socketWrapper) {
socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
PollerEvent pollerEvent = createPollerEvent(socketWrapper, OP_REGISTER);
addEvent(pollerEvent);
}
|
NioEndpoint 拉取事件
连接建立成功后,不久将会触发可读事件,随后由 org.apache.tomcat.util.net.NioEndpoint.Poller#run
进行处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @Override
public void run() {
while (true) {
// 等待事件到达
keyCount = selector.select(selectorTimeout);
// 遍历 Select key
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
// Attachment may be null if another thread has called
// cancelledKey()
if (socketWrapper != null) {
// 读取并解析数据
processKey(sk, socketWrapper);
}
}
}
}
|
run
方法会不断拉取事件,当事件就绪时,调用 processKey
-> processSocket
解析数据:
1
2
3
4
5
6
7
8
| // org.apache.tomcat.util.net.AbstractEndpoint#processSocket
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
sc = createSocketProcessor(socketWrapper, event);
Executor executor = getExecutor();
// 通常交给执行器处理 I/O 任务
executor.execute(sc);
}
|
执行器执行的 run 方法最终会调用内部类 org.apache.tomcat.util.net.NioEndpoint.SocketProcessor
的 doRun()
方法:
1
2
3
4
5
6
| @Override
protected void doRun() {
...
state = getHandler().process(socketWrapper, event);
...
}
|
最后,该方法调用 org.apache.coyote.AbstractProtocol.ConnectionHandler#process
完成了数据的读取,并转交给 Coyote Processor 组件解析:
1
2
3
4
5
6
7
8
9
10
11
| @Override
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
...
// 创建应用层协议的解析器,即 processor 组件
processor = getProtocol().createProcessor();
do {
// 执行解析
state = processor.process(wrapper, status);
}
...
}
|
该方法首先创建了用于解析应用层协议的 Processor 组件(上文介绍的 Coyote 内部三组件之一),然后调用其 process
方法完成应用层数据解析。
Processor 解析应用层协议
以 Http11Processor
为例,它的 process
方法位于父类 AbstractProcessorLight
中:
1
2
3
4
5
| // org.apache.coyote.AbstractProcessorLight#process
@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status) throws IOException {
state = service(socketWrapper);
}
|
AbstractProcessorLight
调用了 org.apache.coyote.http11.Http11Processor#service
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @Override
public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException {
// 读取并封装 Request、Response
while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null &&
sendfileState == SendfileState.DONE && !protocol.isPaused()) {
try {
if (!inputBuffer.parseRequestLine(keptAlive, protocol.getConnectionTimeout(),
protocol.getKeepAliveTimeout())) {
if (inputBuffer.getParsingRequestLinePhase() == -1) {
return SocketState.UPGRADING;
} else if (handleIncompleteRequestLineRead()) {
break;
}
}
}
...
}
// 解析 HTTP 请求内容,例如 url 等
prepareRequest();
// 核心!!!交给适配层往 Servlet 容器转移
getAdapter().service(request, response);
}
|
走到这里我们终于看到了关键的核心,Http11Processor
将 HTTP 类型的 request 和 response 移交给了 Adapter
适配器。
CoyoteAdapter 转发请求
默认的 Adapter
实现为 CoyoteAdapter
:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // org.apache.catalina.connector.CoyoteAdapter#service
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
// 取出 Servlet 类型的请求和响应
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
...
// 解析和设置 Catalina 和配置特定的请求参数
postParseSuccess = postParseRequest(req, request, res, response);
...
// 交给部署在 Pipeline 上的 Valve 处理
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
|
这里的重点是 postParseRequest
方法,该方法通过 URL 找到对应的 Engine、Host、Context 和 Servlet 实例,并将其缓存到 Request 中:
1
2
3
4
5
6
7
8
9
10
11
| // org.apache.catalina.connector.CoyoteAdapter#postParseRequest
protected boolean postParseRequest(org.apache.coyote.Request req, Request request, org.apache.coyote.Response res,
Response response) throws IOException, ServletException {
...
while (mapRequired) {
// 通过 Mapper 映射器,找到 url 对应的容器,并存到 request 的 MappingData 中
connector.getService().getMapper()
.map(serverName, decodedURI, version, request.getMappingData());
...
}
}
|
最终 Valve 会逐层调用内层容器的 Valve,直到将请求发送至 Servlet:
StandardEngineValve#invoke
1
2
| Host host = request.getHost();
host.getPipeline().getFirst().invoke(request, response);
|
StandardHostValve#invoke
1
2
| Context context = request.getContext();
context.getPipeline().getFirst().invoke(request, response);
|
StandardContextValve#invoke
1
2
3
4
5
6
7
8
9
10
| // 安全策略:禁止直接访问敏感目录
MessageBytes requestPathMB = request.getRequestPathMB();
if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0)) || (requestPathMB.equalsIgnoreCase("/META-INF")) ||
(requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0)) || (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 从 Mapper 映射器中取出包装的 Web APP
Wrapper wrapper = request.getWrapper();
wrapper.getPipeline().getFirst().invoke(request, response);
|
StandardWrapperValve
1
2
3
4
5
6
7
8
9
10
11
12
| // 从 Valve 中取出这个 Wrapper
StandardWrapper wrapper = (StandardWrapper) getContainer();
// 取出 Servlet,如果第一次请求,就构造一个
servlet = wrapper.allocate();
// 构造过滤器链
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// 处理 servlet 请求
filterChain.doFilter(request.getRequest(), response.getResponse());
// 销毁过滤器链
filterChain.release();
// 修改引用计数,或将 servlet 放回实例池
wrapper.deallocate(servlet);
|
这里的核心是 filterChain.doFilter
调用,该方法内部最终会进入 org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
中,并在最后调用 servlet.service(request, response)
正式进入我们的 Servlet service 标准方法中:
1
2
3
4
5
6
| private void internalDoFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
...
servlet.service(request, response);
...
}
|
至此,一次完整的请求处理流程已经介绍完毕。接下来,我们还需要分析上述过程中最重要的一个问题,那就是 StandardContextValve
是如何通过 Mapper 映射器将请求与对应的 Servlet 关联起来的。
Mapper 映射机制
Catalina 通过 Mapper 组件管理 URL 与各级容器之间的映射关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| +---------------------------------------------------+
| http://localhost:80/web/app |
|+------------------+ | | | |
|| Engine | | | | |
|| +---------------+| | | | |
|| | Host ||<-------------+ | | |
|| | +------------+|| Mapper | | |
|| | | Context |||<-----------------------+ | |
|| | | +---------+||| | |
|| | | | Wrapper ||||<---------------------------+ |
|| | | +---------+||| |
|| | +------------+|| |
|| +---------------+| |
|+------------------+ |
+---------------------------------------------------+
|
其体系结构如下:
![catalina mapper](/blog/posts/core-principles-of-apache-tomcat-9/images/mapper.webp)
可见,Mapper 通过内部类管理了 URL 与 Host、Context、Wrapper 这三层容器之间的映射关系。
MapElement 内部类
MapElement
是保存映射关系的几类,它通过两个字段,构成了一个 Key-Value 形式的键值对:
1
2
3
4
5
6
7
8
9
| protected abstract static class MapElement<T> {
public final String name;
public final T object;
public MapElement(String name, T object) {
this.name = name;
this.object = object;
}
}
|
name
:url 中的某一部分;object
: 某一层的容器对象。
MappedHost 内部类
MappedHost
继承自 MapElement
,它不仅维护了 URL 与 Host 容器的关系,还维护了与 Host 下级的所有 Context 的关系:
1
2
3
4
5
| protected static final class MappedHost extends MapElement<Host> {
public volatile ContextList contextList;
private final MappedHost realHost;
...
}
|
MappedContext 内部类
MappedContext
也继承自 MapElement
,它负责管理与 URL 路径关联的所有 Wrapper,不过是通过另一个中间内部类 ContextVersion
管理的:
1
2
3
4
5
6
7
8
9
| protected static final class MappedContext extends MapElement<Void> {
public volatile ContextVersion[] versions;
public MappedContext(String name, ContextVersion firstVersion) {
super(name, null);
// 插入第一个版本
this.versions = new ContextVersion[] { firstVersion };
}
}
|
ContextVersion
内部管理了与 URL 路径关联的所有 MappedWrapper
,并且支持版本管理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| protected static final class ContextVersion extends MapElement<Context> {
// url 路径
public final String path;
// 目录层级
public final int slashCount;
// 根目录入口资源
public final WebResourceRoot resources;
...
//
public MappedWrapper defaultWrapper = null;
// 精确 Wrapper 集合
public MappedWrapper[] exactWrappers = new MappedWrapper[0];
// 模糊 Wrapper 集合
public MappedWrapper[] wildcardWrappers = new MappedWrapper[0];
// 扩展 Wrapper 集合
public MappedWrapper[] extensionWrappers = new MappedWrapper[0];
public ContextVersion(String version, String path, int slashCount, Context context, WebResourceRoot resources,
String[] welcomeResources) {
super(version, context);
this.path = path;
this.slashCount = slashCount;
this.resources = resources;
this.welcomeResources = welcomeResources;
}
}
|
MappedWrapper 内部类
最终,MappedWrapper
保存了最末一级路径与 Servlet 包装器之间的映射关系:
1
2
3
4
5
6
7
8
9
10
11
| protected static class MappedWrapper extends MapElement<Wrapper> {
public final boolean jspWildCard;
public final boolean resourceOnly;
public MappedWrapper(String name, Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) {
super(name, wrapper);
this.jspWildCard = jspWildCard;
this.resourceOnly = resourceOnly;
}
}
|
Tomcat 类加载机制
JVM 类加载机制简介
所谓的 JVM 类加载机制,就是将 .class 文件中的字节码加载到 JVM 内存中的机制,而这一过程中最重要的组件就是 ClassLoader
。
ClassLoader
本质上也是一个类,JVM 在启动时需要先将 ClassLoader
加载到内存,再由它去加载其它类(例如各种 jar 包中的字节码文件)。
JVM 内置了以下几种类加载器:引导类加载器、扩展类加载器、系统类加载器。它们之间通过 parent
属性形成父子关系,最终形成了以下结构:
1
2
3
4
5
6
7
| Bootstrap Classloader
^
|
Extension Classloader
^
|
System Classloader (App Classloader)
|
类加载器 | 说明 |
---|
Bootstrap Class Loader | 由 C++ 编写,负责加载 java 核心库(例如 rt.jar 中的类)、构造 Ext Class Loader 和 System Class Loader |
Extension Classloader | 由 Java 编写,用于加载扩展库中的类,例如 JAVA_HOME/lib/ext 目录下的 jar 包中的类、javax.* 包中的类或者 java.ext.dir 指定位置中的类 |
System Classloader(又称 App Classloader) | 默认类加载器,用于加载非扩展库 (classpath) 中的类 |
如果我们对类加载过程有特殊需求(例如加载特别的私有路径等),也可以自定义类加载器。要自定义类加载器,只需让它继承 System Classloader 即可。
当使用自定义类加载器加载字节码文件时,首先会逐级向父加载器提交加载请求。最上层的 Bootstrap Classloader 会先在核心库路径中尝试加载目标类,如果找不到,再移交给 Extension Classloader 查找扩展库。如果仍然找不到,再移交给 System Classloader 查找 classpath。若以上所有加载器都无法找到目标类,最后再由自定义的类加载器来完成加载。这就是所谓的双亲委派机制。
双亲委派机制
双亲委派机制是指,当一个 ClassLoader 需要加载某个 .class 文件时,首先将任务交给它的父加载器,依次递归。如果所有父加载器都无法完成加载,才由自身处理。
该机制有以下几个作用:
- 安全性:通过双亲委派机制,可以确保核心类库(如 java.lang.* 等)由 Bootstrap Classloader 加载,避免了核心类库被覆盖或篡改的风险。
- 避免重复加载:双亲委派机制可以防止类的重复加载。上级类加载器会缓存已加载的类,如果下级类加载器重复加载同一个类,上级类加载器将会拦截该加载请求。
Tomcat 类加载特性
Tomcat 的类加载器并没有严格遵循双亲委派机制,主要是因为 Tomcat 需要满足多版本部署和动态更新等需求。假如有两个版本的 app 需要部署,app-01.jar 中的代码实现与 app-02.jar 中的并不相同,但它们的类的全限定名却相同。如果严格遵循双亲委派机制,就无法实现同时加载这两个类(会被视为重复加载)。因此,Tomcat 选择在自定义类加载器中直接加载这些类。
具体的表现为:
- 通常情况下,Tomcat 的类加载器会遵循双亲委派机制。
- 然后,Tomcat 为每个 Web 应用程序都设置了一个私有类加载器
WebappClassLoader
或 ParallelWebappClassLoader
。这样可以确保每个 Web 应用程序的类都相互隔离,避免了类冲突和版本冲突。当一个 Web 应用程序需要加载一个类时,Tomcat 默认会让这个私有类加载器直接进行加载。
至此,Tomcat 相关的主要内容就介绍完了。