Tomcat 启动流程源码分析
概述
执行 catalina.sh start 后,Tomcat 内部经历了一条清晰的启动链:Bootstrap → Catalina → StandardServer → Service → Engine/Host/Context。本文沿着这条链逐层读源码,弄清三个核心问题:入口如何加载主类、server.xml 如何被解析成组件树、容器如何逐级完成生命周期启动。
一、启动入口:Bootstrap
1.1 调用链起点
catalina.sh start
└── java -Dcatalina.base=... -classpath bootstrap.jar org.apache.catalina.startup.Bootstrap start
└── Bootstrap.main()
├── new Bootstrap()
├── bootstrap.init() ← 创建类加载器、加载 Catalina 类
└── bootstrap.start() ← 反射调用 Catalina.start()catalina.sh 组装 classpath 后,把控制权交给 Bootstrap 这个引导类。它不直接 new Catalina,而是通过反射加载——这样 Catalina 的类加载器由 Tomcat 自己控制(兼容老的 CATALINA_HOME/server 目录结构)。
1.2 init:类加载器与反射加载
// org.apache.catalina.startup.Bootstrap.init()
public void init() throws Exception {
// 1. 创建 Common 类加载器
ClassLoader commonLoader = null;
try {
commonLoader = createClassLoader("common", null);
} catch (Throwable t) {
// 兜底:使用系统类加载器
}
// 2. 创建 Catalina 类加载器(默认与 Common 相同)
catalinaLoader = createClassLoader("catalina", commonLoader);
...
// 3. 反射加载 Catalina 类(解除编译期依赖)
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
catalinaDaemon = startupInstance;
}要点:
- 类加载器先行:
createClassLoader读取catalina.properties的common.loader等路径,构造加载器层次 - 反射解耦:
catalinaDaemon是 Object 引用,start()通过getMethod("start")调用,保证 Catalina 的实现细节对引导层透明
1.3 start:反射调用
public void start() throws Exception {
if (catalinaDaemon == null) init();
// 反射调用 Catalina 实例的 load() 与 start()
Method method = catalinaDaemon.getClass().getMethod("start");
method.invoke(catalinaDaemon);
}二、Catalina:加载与启动
2.1 load():解析 server.xml
Catalina.load() 是启动的核心:它用 Digester(Apache Commons Digester)把 conf/server.xml 解析成一棵组件对象树。
// org.apache.catalina.startup.Catalina.load()
public void load() {
// 1. 初始化 JNDI 资源(GlobalNamingResources)
initNaming();
// 2. 创建 Digester 并注册解析规则
Digester digester = createStartDigester();
...
// 3. 解析 server.xml
try (InputStream inputStream = getConfigFileStream()) {
digester.push(this); // 把 Catalina 压入栈顶
digester.parse(inputStream); // 边解析边构建组件对象
}
// 4. 获取 Server 并绑定生命周期
getServer().setCatalina(this);
...
// 5. 注册 JMX
registerJMX();
}2.2 Digester 的解析规则
Digester 采用栈式对象图构建:XML 元素与 Java 对象创建规则一一映射。核心规则注册:
// createStartDigester() 中的规则片段
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer", "className");
digester.addSetProperties("Server");
digester.addSetNext("Server", "setServer", "org.apache.catalina.Server");
digester.addObjectCreate("Server/Service",
"org.apache.catalina.core.StandardService", "className");
digester.addSetNext("Server/Service", "addService", "org.apache.catalina.Service");
digester.addObjectCreate("Server/Service/Connector",
"org.apache.catalina.connector.Connector", "className");
digester.addSetNext("Server/Service/Connector", "addConnector",
"org.apache.catalina.connector.Connector");
digester.addObjectCreate("Server/Service/Engine",
"org.apache.catalina.core.StandardEngine", "className");
digester.addSetNext("Server/Service/Engine", "setContainer", "org.apache.catalina.Engine");规则含义:遇到 Server/Service/Connector 元素 → 创建 Connector 对象 → 调用上一级(Service)的 addConnector 加入容器。XML 结构直接决定对象树结构,这就是 server.xml 与组件树一一对应的原因。
2.3 组件对象树的构建结果
Catalina
└── StandardServer
└── StandardService (Catalina)
├── Connector (8080 HTTP/1.1)
├── Connector (8009 AJP/1.3)
└── StandardEngine (Catalina)
└── StandardHost (localhost)
└── (部署时动态创建) StandardContext三、start():递归生命周期启动
3.1 Catalina.start()
// Catalina.start()
public void start() {
if (getServer() == null) {
load(); // 尚未加载则先 load
}
...
// 启动 Server —— 触发整棵组件树的递归 start
getServer().start();
...
}3.2 Server → 容器的递归启动
StandardServer.start() 的内部会依次启动其子组件。这里体现 Lifecycle 接口的状态机(详见"生命周期与容器架构源码分析"篇):
// StandardServer.startInternal() 的核心顺序
// 1. 启动 Service(Connector + Engine 是 Service 的子组件)
for (Service service : services) {
service.start();
}
// 2. 启动全局 JNDI 资源
// 3. 注册关闭钩子 / 启动非守护线程(await 线程等待 SHUTDOWN 命令)StandardService.startInternal() 中 Connector 先于 Engine 启动:
// StandardService.startInternal()
protected void startInternal() throws LifecycleException {
// 1. 先启动 Engine(容器),后启动 Connector?
// 实际顺序:先 Engine 再 Connector
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
...
for (Connector connector : connectors) {
connector.start(); // Connector 此时才监听端口
}
}启动顺序的讲究:Engine(容器)先于 Connector(端口监听)启动——先让容器就绪,再开放对外端口,避免端口已监听但容器未就绪时请求进来报错。
3.3 容器启动的递归
StandardEngine.start() → StandardHost.start() → 部署 Context。Context 的启动包含:
StandardContext.startInternal()
├── 创建 WebappClassLoader
├── 解析 web.xml(WebXml / ServletContext 初始化)
├── 加载并初始化 Servlet(懒加载:默认 init 延后到首次请求)
├── 启动后台线程(Session 过期扫描、JSP 检查等)
└── 触发 STARTED 事件3.4 await:等待关闭命令
Catalina.await() 让主线程阻塞,监听关闭端口(默认 8005):
// Catalina.await() —— 简化逻辑
while (true) {
int command = serverSocket.getInputStream().read();
if (command == SHUTDOWN_COMMAND[0]) { // 读到 'S'(SHUTDOWN 首字符)
break; // 触发退出流程
}
}shutdown.sh 的本质:向 8005 端口发送 SHUTDOWN 字符串 → await 线程收到 → 走 Catalina.stop() 优雅停止。
四、启动时序总览
Bootstrap.main
├─ init() 创建 Common/Catalina 类加载器,反射加载 Catalina
└─ start()
└─ Catalina.start()
├─ (首次) load():Digester 解析 server.xml → 构建组件树
├─ Server.start()
│ ├─ Service.start()
│ │ ├─ Engine.start()
│ │ │ └─ Host.start()
│ │ │ └─ Context.start()(部署应用,创建 WebappClassLoader)
│ │ └─ Connector.start() ← 端口开始监听
│ ├─ 启动后台线程
│ └─ 注册关闭钩子
├─ await() 阻塞等待 SHUTDOWN
└─ stop() 收到命令后递归停止五、启动日志的对应关系
| 日志 | 对应源码阶段 |
|---|---|
Initializing ProtocolHandler [http-nio-8080] | Connector 初始化 |
Starting service [Catalina] | Service.start() |
Starting Servlet engine: [Apache Tomcat/10.1.x] | Engine.start() |
Deploying web application archive [order.war] | Host 部署 Context |
Server startup in [1234] milliseconds | 启动完成,await 前 |
看懂日志顺序,就能快速判断启动卡在哪一步——是类加载器创建、端口占用还是应用部署。
六、启动慢的常见原因
| 原因 | 定位 |
|---|---|
| 随机数熵不足 | 启动停滞在安全类初始化 → 加 -Djava.security.egd=file:/dev/./urandom |
| DNS 反查 | Host 解析慢 → enableLookups="false" |
| 应用部署慢 | Context 启动日志显示各阶段耗时 → 检查数据源初始化/监听器 |
| 端口被占用 | 报 BindException → netstat 定位 |
七、小结
启动流程的核心设计:
- 引导与实现分离:Bootstrap 反射加载 Catalina,降低耦合
- 配置即对象树:Digester 把 XML 解析规则与对象创建绑定,server.xml 就是组件树声明
- 递归生命周期:Server 启动触发整树递归,Engine 先于 Connector,保证"先就绪、后开放"
- await + 关闭端口:优雅停机与强制退出的统一入口
参考链接: