对于很多 Java 后端开发者来说,Tomcat 就像是水和空气——每天都在用,但很少有人真正在意它到底是怎么运作的。特别是在 Spring Boot 大行其道的今天,一个 main 方法跑起来,Web 服务就启动了,Tomcat 似乎彻底“隐形”了。
但如果不揭开这层神秘的面纱,我们将永远停留在“会用”的阶段。这篇文章将带你扒开 Tomcat 的外衣,从核心架构、请求处理流程,一路解析到它与 Spring Boot 的“爱恨情仇”。
一、 Tomcat 到底是个什么东西?
在教科书里,Tomcat 被称为 Web 服务器 和 Servlet 容器。
为了理解这个概念,我们可以把 Tomcat 想象成一家“餐厅”:
- Web 服务器(服务生):负责接收客人(浏览器客户端)的 HTTP 请求,比如客人点了一份静态页面(HTML/图片),服务生直接从后厨端出来给客人。
- Servlet 容器(大厨):如果客人点的是动态数据(比如查询某个用户的订单详情),服务生自己处理不了,就会把点单(HTTP 请求)交给后厨的 Servlet 容器。容器会根据规则找到对应的 Servlet(可以理解为具体的某位大厨)来执行 Java 代码,生成动态结果后再交还给服务生,最后响应给客人。
二、 Tomcat 的核心架构:Catalina 与 Coyote
Tomcat 的内部组件设计非常优雅,体现了极其严谨的面向对象思想。它的核心主要由两大部分组成:连接器(Connector) 和 容器(Container)。
1. Connector 连接器(Coyote)
连接器就像是 Tomcat 的“耳朵”和“嘴巴”,专门负责和外界打交道。
- 主要职责:监听网络端口(比如经典的 8080 或你刚才配的 3307 等网络环境),接收客户端的 HTTP 请求。
- 协议转换:它将底层的 Socket 网络字节流,解析、封装成 Tomcat 内部能够理解的
Request对象;同时,把容器处理完的Response对象,转换成字节流通过 Socket 发送回客户端。 - 支持的协议与 IO 模型:支持 HTTP/1.1、HTTP/2、AJP 等协议。在 Tomcat 8 以后,默认采用 NIO(非阻塞 IO)模型,大大提升了高并发下的吞吐量。
2. Container 容器(Catalina)
容器是 Tomcat 的“大脑”,负责处理 Connector 递交过来的 Request,执行具体的业务逻辑。
Tomcat 的容器设计采用了责任链模式和层级嵌套结构,从外到内分为四层:
- Engine(引擎):最外层的容器,负责管理多个虚拟主机。一个 Service 只能包含一个 Engine。
- Host(虚拟主机):代表一个虚拟主机,或者说一个域名(比如
www.baidu.com)。一个 Engine 下可以有多个 Host。 - Context(应用上下文):代表一个具体的 Web 应用程序。在过去打 WAR 包的年代,一个 WAR 包解压后就是一个 Context。
- Wrapper(包装器):最底层的容器,是对实际 Servlet 的包装。它负责管理 Servlet 的生命周期(实例化、init、service、destroy)。
3. Service 与 Server
- Service:将 Connector 和 Container 组装在一起的逻辑组件。一个 Service 通常包含多个 Connector(比如一个处理 HTTP,一个处理 HTTPS)和一个 Engine。
- Server:代表整个 Tomcat 实例。它可以包含多个 Service。
用大白话总结它们的层级关系就是:
一个 Tomcat 实例(Server)包含多个服务(Service)。每个服务有接客的(Connector)和做菜的(Engine)。做菜的部门里分了不同的餐饮品牌(Host),每个品牌下有不同的具体门店(Context),门店里有不同的厨师(Wrapper/Servlet)。
三、 一个 HTTP 请求的奇幻漂流
当我们在浏览器输入 http://localhost:8080/api/user/1 时,Tomcat 内部到底发生了什么?
- 接收请求:请求到达服务器操作系统的网卡,操作系统根据端口号(8080)将请求交给 Tomcat 的 Connector 线程。
- 解析与封装:Connector 采用 NIO 模型读取网络字节流,解析 HTTP 报文,生成内部的
org.apache.catalina.connector.Request对象。 - 分发给引擎:Connector 将 Request 对象传递给它所绑定的 Engine。
- 层层路由(Mapping):
- Engine 根据请求头中的
Host字段,匹配到对应的 Host 容器。 - Host 根据请求 URI 的前缀(比如
/api),匹配到对应的 Context(Web 应用)。 - Context 根据具体的 URL 路径(
/user/1),通过web.xml或注解,匹配到具体的 Wrapper(即目标 Servlet)。
- Engine 根据请求头中的
- 执行过滤器与 Servlet:
- 请求在到达目标 Servlet 之前,会先经过一系列的 Filter(过滤器)(比如处理跨域、权限校验、字符编码转换)。
- 最终调用 Servlet 的
service()方法。如果是 Spring 框架,这里命中的通常是DispatcherServlet,随后进入 Spring MVC 的控制层逻辑(Controller)。
- 响应返回:Servlet 执行完毕后,将结果写入
Response对象。Connector 将其转换成 HTTP 响应报文,通过 Socket 发送给浏览器。
四、 宿命的相遇:Tomcat 与 Spring Boot
在讲 Spring Boot 之前,我们需要回忆一下“上古时期”的 SSM(Spring + SpringMVC + MyBatis)开发模式。
1. 传统时代的痛点:外置 Tomcat
以前开发 Java Web,我们需要:
- 去官网下载一个 Tomcat 压缩包,解压到本地。
- 在 IDEA 里配置 Tomcat 路径。
- 把我们写的代码打成一个
.war包。 - 把 WAR 包扔到 Tomcat 的
webapps目录下。 - 启动 Tomcat 的
startup.bat。
这种方式的痛点非常明显:环境依赖性极强。你刚才部署 Docker 数据库时应该深有体会,环境不一致会导致各种诡异的问题。如果线上服务器的 Tomcat 版本和本地不一致,或者 server.xml 配置少了一行,程序直接崩溃。
2. Spring Boot 的革命:内嵌 Tomcat
Spring Boot 的核心思想是 “约定优于配置” 和 “独立运行”。它做了一个极其大胆且成功的决定:不再让我们的代码去适配 Tomcat,而是把 Tomcat 变成一个普通的依赖(Jar 包),塞进我们的代码里!
当你引入 spring-boot-starter-web 依赖时,Spring Boot 会自动拉取 tomcat-embed-core 等核心 Jar 包。
它是怎么做到的?
Spring Boot 巧妙地利用了 Servlet 3.0 规范中的 SPI 机制和自己的自动装配特性:
- 自动装配:Spring Boot 启动时,
ServletWebServerFactoryAutoConfiguration配置类会生效。 - 工厂模式:它会向 Spring 容器中注入一个
TomcatServletWebServerFactory(Tomcat Web 服务器工厂)的 Bean。 - 编程式启动 Tomcat:在 Spring 的
ApplicationContext刷新阶段(onRefresh()方法中),Spring Boot 会调用这个工厂对象。工厂会在内部直接通过 Java 代码new Tomcat(),然后编程式地配置 Connector、Context,最后调用tomcat.start()。 - 注册 DispatcherServlet:Spring Boot 会将 Spring MVC 的核心
DispatcherServlet注册到这个内嵌的 Tomcat 的 Context 中。
一句话总结:以前是把做好的菜(WAR包)端进餐厅(Tomcat),现在 Spring Boot 直接自带了一个移动厨房(内嵌 Tomcat),哪里有 Java 环境,哪里就能开伙做饭。
五、 实战:Spring Boot 中的 Tomcat 性能调优
作为后端开发,绝对不能满足于“能跑就行”。在应对外卖秒杀、打车早高峰等高并发场景时,修改 Spring Boot 中的 Tomcat 默认参数是必修课。
在 application.yml 中,我们可以直接对内嵌 Tomcat 进行精准调优:
YAML
server:
port: 8080
tomcat:
# 核心线程数:Tomcat 初始化时创建的线程数,即使空闲也会保留,默认 10
min-spare-threads: 20
# 最大线程数:Tomcat 能够创建来处理请求的最大线程数,默认 200。
# 如果接口耗时较长(比如复杂的数据库查询),可以适当调大。
max-threads: 500
# 最大连接数:Tomcat 允许客户端同时建立的最大连接数。NIO 模式下默认 8192。
max-connections: 10000
# 全连接队列长度:当所有工作线程都在忙,且连接数达到 max-connections 时,
# 新来的请求会被放入操作系统的等待队列。这个值就是队列的最大长度,默认 100。
# 超过这个长度的请求会被直接拒绝(Connection Refused)。
accept-count: 200
调优核心逻辑总结:
- 如果你的系统属于 CPU 密集型(比如大量的加密解密、复杂算法),把
max-threads调小一点,减少线程上下文切换的开销。 - 如果你的系统属于 IO 密集型(比如大量的数据库查询、第三方 API 调用),把
max-threads调大一点,让更多线程去等待 IO,压榨 CPU 吞吐量。
六、 结语
从底层的 Socket 网络通信,到 Coyote 连接器的协议解析;从 Catalina 容器的责任链路由,再到 Spring Boot 巧妙的内嵌化设计。Tomcat 见证了 Java Web 二十多年的发展史。
理解了这套机制,以后在项目中看到 java.net.SocketTimeoutException 或者是 Tomcat Max Connections Reached 时,你脑海中浮现的不再是死板的报错代码,而是 Tomcat 内部那套精密齿轮卡住的生动画面。这就是高级工程师与普通码农的根本区别。
Comments NOTHING