Tomcat 工作机制
Tomcat 是什么:HTTP 服务器与 Servlet 容器的组合
Tomcat 是一个开源的、由 Java 实现的 HTTP 服务器 + Servlet 容器。它对外承担两件事:一是作为 HTTP 服务器监听端口、处理 TCP 连接与 HTTP 协议;二是作为 Servlet 容器,把解析后的请求按规则路由到具体的 Servlet 并管理其生命周期。
一句话概括:Connector 处理 I/O,Catalina 负责路由。 Connector 负责把字节流变成结构化的 Request/Response 对象,Catalina(Tomcat 的 Servlet 容器实现)负责把这些对象分发给正确的业务代码。
为什么存在:Servlet 容器接管了”通信”这件脏活
在没有 Servlet 容器之前,用 Java 写 Web 应用意味着自己处理 socket、解析 HTTP 报文头、维护连接池——这些代码和业务逻辑毫无关系却极其繁琐。Servlet 容器存在的根本动机是:让 Web 开发者专注于业务逻辑,由容器处理一切与网络通信相关的细节。
一个合格的 Servlet 容器通常承担以下职责:
- 网络通信:监听端口、接收 TCP 连接、解析 HTTP 请求报文、序列化 HTTP 响应。
- 请求分发:根据 URL 与映射规则(
web.xml或注解)把请求路由到对应的 Servlet。 - 生命周期管理:控制 Servlet 的实例化、初始化、服务、销毁。
- 线程管理:为每个请求分配工作线程,处理并发。
- 类加载隔离:让部署在同一容器内的多个 Web 应用互不干扰。
- 资源管理:会话(Session)、JNDI 命名资源、静态资源等。
容器接手之后,开发者只需要继承 HttpServlet,拿到已经解析好的 HttpServletRequest 与 HttpServletResponse 即可:
Context: 容器把字节流封装成可编程对象,业务代码只关心 request/response。
public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("text/plain;charset=utf-8"); resp.getWriter().write("Hello, " + req.getParameter("name")); }}Why this way: 业务方法只依赖 Servlet 规范定义的 API,与具体容器实现解耦——同一个 Servlet 可以跑在 Tomcat、Jetty、Undertow 上。
Key params:
HttpServletRequest— 已解析的请求(头、参数、体、Cookie)HttpServletResponse— 容器提供的响应写入通道doGet/doPost— 容器根据 HTTP 方法自动路由
工作原理:从启动到一次请求的完整链路
组件架构:自上而下的树形结构
Tomcat 的运行时由一棵组件树构成,根节点是 Server,逐层向下:
Server // 整个 Tomcat 实例(全局唯一)└── Service // 把多个 Connector 绑定到一个 Engine ├── Connector (NIO/HTTP) // 处理 I/O 与协议解析 └── Engine // 整个 Servlet 引擎 └── Host // 虚拟主机(如 localhost) └── Context // 一个 Web 应用(对应一个 WAR/目录) └── Wrapper // 一个 ServletWhy this way: 自上而下的分层让每一层只关心自己的职责——Engine 负责选虚拟主机,Context 负责选 Web 应用,Wrapper 负责选具体 Servlet。这种层级正好对应一次 HTTP 请求从”到哪台主机”到”调哪个方法”的逐级收敛。
Key params:
Connector— 可以有多个(同时监听 HTTP/AJP 等不同协议)Context— 类加载隔离与热部署的基本单位Wrapper— 最内层,直接持有 Servlet 实例
启动流程:解析 server.xml,按 Lifecycle 逐层启动
Context: Tomcat 启动时需要把这棵组件树从配置文件构建出来,再自上而下激活。
# 入口:Catalina 提供的引导类(startup.sh 实际执行的命令)java org.apache.catalina.startup.Bootstrap start启动分为两步:先用 Digester(基于 SAX 的 XML→Java 对象映射工具)把 server.xml 解析成上述组件树,然后通过 Lifecycle 接口逐层调用 start()。
Why this way: 每个组件都实现 org.apache.catalina.Lifecycle,暴露统一的状态机(NEW → INITIALIZING → INITIALIZED → STARTING → STARTED …)和事件通知机制。父组件启动时会自动触发子组件启动,从而用一套递归逻辑完成整棵树的初始化。
Key params:
server.xml— 声明组件树的配置文件(位于conf/目录下)Lifecycle— 组件生命周期的统一接口,支持事件监听Digester— 把 XML 元素映射成 Java 对象与方法的解析器
请求处理:Pipeline 与 Valve 组成的责任链
一次 HTTP 请求在组件树中的流转路径如下:
Connector(NIO) 接收 TCP 连接、解析 HTTP │ 构造 Request / Response 对象 ▼Engine Pipeline ──► Host Pipeline ──► Context Pipeline ──► Wrapper Pipeline (选 Host) (选 Context) (选 Wrapper) (调 Servlet.service())每个容器(Engine/Host/Context/Wrapper)内部都有一条 Pipeline(管道),由若干 Valve(阀门) 串联而成,链的末尾是该容器的基础 Valve。请求从最外层进入、逐层向下,每一层先经过自定义 Valve(可做日志、鉴权、限流等切面),再由基础 Valve 决定下一层的路由目标:
StandardEngineValve→ 根据主机名选出 HostStandardHostValve→ 根据应用路径选出 ContextStandardContextValve→ 根据映射规则选出 WrapperStandardWrapperValve→ 加载并调用 Servlet 的service()
Why this way: Pipeline/Valve 本质是一条责任链,把”路由”与”切面增强”解耦。新增鉴权、日志、访问控制不需要改动核心分发逻辑,只需往某层 Pipeline 里插入一个 Valve。
Key params:
Pipeline— 每个容器持有一条阀门链Valve— 链上的处理节点,可自定义StandardWrapperValve— 最终调用 Servlet 的阀门
线程模型:NIO 下的 Acceptor / Poller / Worker
Tomcat 默认使用一个 java.util.concurrent.ThreadPoolExecutor,最大线程数默认 200。NIO 模式下,线程被分为三种角色,符合 Reactor 模式:
Acceptor ──accept()──► 注册到 Selector ──► Poller ──I/O 就绪──► Worker 线程池 (接收连接) (监听读事件) (检测) (执行 Servlet)- Acceptor:阻塞调用
ServerSocket.accept(),拿到新的 Socket 连接后把它交给 Poller。 - Poller:维护一个
Selector,轮询哪些连接的数据已就绪可读。少量 Poller 线程就能管理大量连接。 - Worker:从线程池取出工作线程,执行真正的 Servlet 业务逻辑。
Why this way: I/O 等待(数据还没到达)不占用工作线程。Poller 用 Selector 的事件驱动机制,让少数线程监管海量连接;只有当数据真正就绪、需要执行业务时才消耗宝贵的 Worker 线程。这与传统 BIO 模式”一连接一线程”相比,大幅提升了并发承载能力。
Key params:
maxThreads(默认 200)— Worker 线程池上限acceptCount— 连接已满时的等待队列长度Selector— NIO 多路复用器,Poller 监听就绪事件的核心
类加载机制:打破双亲委派以实现隔离与热部署
Tomcat 的类加载器层次与标准 JDK 略有不同:
Bootstrap ClassLoader // JVM 内置 └── System (AppClassLoader) // 加载启动类、catalina.properties └── Common // lib/ 下,所有应用共享 ├── Catalina // Tomcat 自身的类 └── Webapp // 每个 Context 独立:WEB-INF/classes + WEB-INF/lib关键差异在于 Webapp 类加载器会先尝试自己加载,再委托父加载器(JDK 核心类除外),这与标准双亲委派恰好相反。
Why this way: 先本地加载保证了不同 Web 应用之间的类互相隔离——A 应用的 commons-xxx.jar 与 B 应用即使版本不同也能共存。而热部署之所以可行,正是因为一个 Context 的全部类都由它自己的 Webapp 类加载器持有;重新加载应用时只需丢弃旧加载器、创建新的,旧类随之被 GC,从而在不重启整个 Tomcat 的情况下更新应用。
Key params:
Common—lib/目录,全局共享库Webapp— 每个 Web 应用独立,是隔离与热部署的单位- 双亲委派”反转” — 先本地后父类,实现应用隔离
Servlet 生命周期:单实例多线程
Servlet 在容器中的生命周期由 Context 驱动:
Context 启动扫描配置(web.xml / 注解)注册 Servlet 定义 → 首次请求到达时懒加载实例化 → init(ServletConfig) // 初始化一次 → service() (并发) // 每个请求一个线程 → destroy() // Context 卸载时Why this way: 默认懒加载是为了加快启动速度——只有被请求到的 Servlet 才真正创建。init() 只在实例创建后调用一次,之后所有请求共享同一个实例、由多个工作线程并发调用 service()。这正是 Servlet 编程的核心约束:不要用实例变量保存请求级状态,否则会出现线程安全问题。
Key params:
load-on-startup— 大于 0 时改为启动即加载(而非懒加载)init()— 仅执行一次的初始化钩子service()— 每请求一线程,按方法分发到doGet/doPost
与替代方案的差异:Servlet 兼容性与版本断点
“Servlet 兼容性”指同一个 Web 应用能否在不同版本、不同品牌的 Servlet 容器上正常运行。它本质上由 Servlet 规范版本 决定。不同版本的 Tomcat 对应不同的规范版本和最低 Java 版本:
Servlet 4.0 → Tomcat 9.x → Java 8+ → 包名 javax.servletServlet 5.0 → Tomcat 10.0 → Java 8+ → 包名 jakarta.servletServlet 6.0 → Tomcat 10.1 → Java 11+ → 包名 jakarta.servletServlet 6.1 → Tomcat 11.0 → Java 21+ → 包名 jakarta.servletWhy this way: 这里最关键的断点是 Tomcat 9 → 10。Servlet 规范从 Java EE 迁移到 Jakarta EE(由 Eclipse 基金会接管)时,因为 Oracle 不允许继续使用 javax 命名空间,API 包名被强制从 javax.servlet 改为 jakarta.servlet。
这意味着为 Tomcat 9 编译的 WAR 无法直接运行在 Tomcat 10 上——所有 import javax.servlet.* 都会失败,必须重新编译,或借助迁移工具批量改写 import。
与其他容器的横向对比:
- Jetty:同样是 Servlet 容器,更轻量,嵌入式场景友好。
- Undertow:性能导向,是 WildFly 的默认 Web 服务器。
- WildFly / GlassFish / Liberty:完整的 Jakarta EE 应用服务器,除 Servlet 外还提供 EJB、JPA、JMS 等全栈能力。
如果只需要 Servlet 容器能力,选 Tomcat 即可;需要完整 Jakarta EE 全栈时才考虑后者。
何时选择 Tomcat
- 运行基于 Servlet 规范的 Java Web 应用 → Tomcat 是首选。
- 只需 Servlet 容器、不需要完整 Jakarta EE 全栈 → 选 Tomcat 而非 WildFly/GlassFly,更轻量。
- 微服务 / 嵌入式场景 → Spring Boot 默认内嵌 Tomcat,开箱即用。
- 高并发静态资源 / 反向代理 → 前面挂一层 Nginx 处理静态与负载均衡,Tomcat 只负责动态请求。
理解了”Connector 处理 I/O、Catalina 负责路由”这一主线,再回看启动流程、请求链路、线程模型与类加载,就都能串成一条完整的因果链。