Apache Tomcat 9 核心原理剖析

这几年一直在做大厂的项目,已经很少接触 Spring MVC 了,毕竟大厂基本都在使用 RPC。即便是给前端提供的接口,也大多是 RPC 接口。不过,作为 Java 世界中最常用的 HTTP & Servlet 容器,Tomcat 还是有必要认真学习一下的。

通过 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 服务进行数据交换。它主要包含以下几个组件:

  1. 负责处理网络请求,并将其转换为 Servlet 标准请求的 Coyote 组件;
  2. 负责处理 Servlet 标准请求,并将其转发给各个 Servlet 服务的 Catalina 组件;
  3. 负责处理 JSP 的 Jasper 组件;
  4. 负责处理命名服务的 Naming 组件;
  5. 负责处理日志的 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
  • Catalina 实例:负责解析 server.xml 配置文件,然后根据配置内容创建 Server 实例并管理;
  • Server 实例:负责管理 Servlet Engine、Connector 的生命周期;
  • Service 实例:负责将若干个 Connector 绑定到 Container,一个 Server 可以包含多个 Service;
  • Connector 实例:与 Service 关联的连接器,内部封装了 Coyote ProtocolHandler 实例;
  • Container:容器的抽象接口,其下包含 StandardEngineStandardHostStandardContextStandardWrapper 四个默认实现。且这四个容器实现类之间是有层级关系的(业务层级,非继承关系):
    • 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 实现,包括 StandardContextValveStandardEngineValveStandardHostValveStandardWrapperValve
  • Listener:生命周期事件的监听器。

其 UML 类图如下:

catalina uml

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 &quot;%r&quot; %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 -> STARTINGSTARTED 的转换;
    • 还可以直接调用 stop() 方法,然后直接进入 STOPPED 状态;
  • INITIALIZED 状态的组件通过调用 start() 方法,可以进入启动流程;
  • STARTED 状态的组件可以通过调用 stop() 方法进入 STOPPING_PREP 状态,然后在停止过程中自动完成从 STOPPING_PREP -> STOPPINGSTOPPED 的转换;
  • STOPPED 状态的组件可以通过调用 start() 方法重新进入启动流程;
  • 不论组件处于何种状态,都可以通过调用 destroy() 方法进入 DESTROYING 状态,待资源释放后自动进入 DESTROYED 状态;
  • 任何状态都可以直接转变为 FAILED,处于 FAILED 状态的组件:
    • 可以通过调用 stop() 方法进入停止流程;
    • 也可以通过调用 destroy() 方法进入销毁流程。

Lifecycle 实现原理

Lifecycle 体系结构如下:

lifecycle uml

该体系包含以下两个机制:

  • 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 为例,它会实现 LifecycleinitInternal 模板方法完成初始化操作:

 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
  • 然后调用 ContainerBasestartInternal() 方法启动 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.SocketProcessordoRun() 方法:

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

可见,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 应用程序都设置了一个私有类加载器 WebappClassLoaderParallelWebappClassLoader。这样可以确保每个 Web 应用程序的类都相互隔离,避免了类冲突和版本冲突。当一个 Web 应用程序需要加载一个类时,Tomcat 默认会让这个私有类加载器直接进行加载。

至此,Tomcat 相关的主要内容就介绍完了。

0%