1. 引言:为什么还要讨论 Tomcat 与 Jetty
在 Spring Boot 内嵌容器、云原生与虚拟线程(Virtual Threads)快速演进的今天,Servlet 容器似乎成了"默认就够用"的基础设施。但一旦遇到高并发长连接、WebSocket 推送、内存受限的边缘部署、或者需要精细控制 HTTP 解析行为的场景,选 Tomcat 还是 Jetty 就会真实地影响交付质量与运维成本。
本文基于 Apache Tomcat 与 Eclipse Jetty 的官方文档、官方 Wiki/FAQ、Release Notes,以及业界公开的工程实践与踩坑记录,系统梳理两者的设计取向、优缺点、适用场景、生态关系、资料丰富度与学习曲线,并给出新项目交付时的选型建议。
说明:本文结论基于两个项目长期公开的架构文档与社区实践总结,具体版本行为请以对应版本的官方文档为准。
2. 两者的定位与架构取向
2.1 Apache Tomcat:Servlet 规范的"参考实现"
Tomcat 由 Apache 软件基金会维护,长期作为 Jakarta Servlet、JSP、WebSocket 等规范的参考实现(Reference Implementation)之一。它的设计目标非常明确:完整、稳定、规范兼容性优先。
- 核心组件:
Server→Service→Connector+Engine→Host→Context→Wrapper,这套容器层级模型几乎成了 Java Web 容器的教科书结构。 - 连接器:经典的阻塞式
BIO(已移除)、NIO、NIO2、APR(基于 Tomcat Native/OpenSSL)。 - 配置方式:
server.xml、context.xml、web.xml三层配置,配合 JNDI、Realm、Valve 等扩展点。
2.2 Eclipse Jetty:可嵌入的 HTTP 服务器与 Servlet 容器
Jetty 由 Eclipse 基金会维护,定位更偏向可嵌入的、模块化的 HTTP 服务器 + Servlet 容器。它的设计哲学是"小而可组合":
- 核心抽象是
Server→Connector→Handler,Handler链式组合,Servlet 只是其中一种Handler实现。 - 高度模块化:
jetty-server、jetty-servlet、jetty-http2、jetty-websocket、jetty-alpn等按需引入,依赖体积可控。 - 内嵌友好:几行代码即可启动一个带 Servlet 或纯 Handler 的服务,天然适合作为库被其他框架嵌入。
一句话概括取向差异:Tomcat 是"把规范实现到极致"的容器,Jetty 是"把 HTTP 服务做成积木"的工具箱。
3. 架构与运行模型对比
| 维度 | Tomcat | Jetty |
|---|---|---|
| 维护组织 | Apache 软件基金会 | Eclipse 基金会 |
| 核心模型 | 容器层级(Engine/Host/Context/Wrapper) | Handler 链 + Connector |
| I/O 模型 | NIO / NIO2 / APR | NIO(Jetty 9+ 统一为 NIO,支持 ALPN) |
| 线程模型 | 连接器线程池 + 容器线程池分离 | 基于 QueuedThreadPool 的统一线程池 + 可配置策略 |
| 协议支持 | HTTP/1.1、HTTP/2(需配置)、AJP、WebSocket | HTTP/1.1、HTTP/2、HTTP/3(Jetty 12 起)、WebSocket、ALPN |
| 配置风格 | XML 为主,编程式为辅 | 编程式/代码优先,XML 可选 |
| 热部署 | 支持(autoDeploy、reloadable) |
支持(DeploymentManager、ContextProvider) |
| 模块化 | 单体发行包 + 可选模块 | 细粒度 Maven 模块,按需组合 |
3.1 线程模型差异的实际影响
Tomcat 的连接器线程(Acceptor/Poller)与业务线程池(Executor)职责分离清晰,默认配置下对突发流量有较好的缓冲能力;Jetty 的 QueuedThreadPool 采用更激进的线程复用策略,在大量空闲长连接场景下内存与线程开销通常更小,这也是 Jetty 常被用于 WebSocket 推送、SSE、长轮询服务的原因之一。
3.2 协议演进节奏
Jetty 在 HTTP/2、HTTP/3(QUIC)等新协议上的跟进通常更早,Jetty 12 已提供 HTTP/3 支持;Tomcat 对 HTTP/2 的支持成熟稳定,但新协议引入相对保守。若项目对前沿协议有明确需求,Jetty 的迭代节奏更友好。
4. Tomcat 的优点与缺点
4.1 优点
- 规范兼容性最强:作为 Servlet/JSP/WebSocket 规范的参考实现,行为最贴近规范文本,迁移与兼容风险低。
- 生态与文档极其丰富:官方文档、FAQ、Wiki、迁移指南齐全;Stack Overflow、中文社区、各类教程与书籍数量远超 Jetty,遇到问题几乎都能搜到答案。
- 运维与监控成熟:JMX 指标、Manager/Host Manager 应用、访问日志、Valve 机制完善,与各类 APM、监控体系集成经验丰富。
- 企业级特性完整:Realm 认证、JNDI 资源、集群会话复制、AJP 与前端 Web 服务器集成等开箱可用。
- 人才供给充足:绝大多数 Java 后端开发者默认熟悉 Tomcat,团队接手成本低。
- Spring Boot 默认容器:默认依赖、默认配置、默认调优资料都围绕 Tomcat 展开,出问题时的"默认路径"最短。
4.2 缺点
- 体积与启动开销偏大:完整发行包与容器层级初始化较重,冷启动与内存占用在资源受限环境下不占优势。
- 内嵌体验一般:虽然支持编程式启动,但 API 设计以"部署 Web 应用"为中心,作为库嵌入其他进程时不如 Jetty 自然。
- 配置偏重 XML :
server.xml的层级配置对新手不够直观,调优参数分散在连接器、Executor、Valve 多处。 - 长连接场景资源效率一般:海量空闲连接下的线程与内存开销通常高于 Jetty。
- 新协议跟进较保守:HTTP/3 等前沿协议支持节奏慢于 Jetty。
5. Jetty 的优点与缺点
5.1 优点
- 极致的内嵌与模块化:按需引入模块,依赖体积小,几行代码即可启动服务,非常适合中间件、工具、测试框架、IDE 插件等"把 HTTP 服务塞进进程"的场景。
- Handler 模型灵活:不依赖 Servlet 也能构建 HTTP 服务,路由与过滤器可用 Handler 组合实现,控制粒度细。
- 长连接与高并发连接效率高:线程池与连接管理策略对 WebSocket、SSE、长轮询友好,连接数上限表现优秀。
- 新协议跟进快:HTTP/2、ALPN、HTTP/3(Jetty 12)支持积极。
- 代码优先、可编程性强:配置即代码,便于在容器化、动态编排环境中做程序化定制。
- 与云原生/嵌入式场景契合:被大量框架与工具选作内嵌服务器(如部分构建工具、测试容器、微服务框架)。
5.2 缺点
- 文档与社区资料相对少:官方文档质量高但偏"参考手册"风格,缺少大量面向业务的教程;中文资料明显少于 Tomcat,遇到冷门问题排查成本更高。
- 版本演进带来迁移成本 :Jetty 9 → 10 → 11 → 12 的包名(
javax.*→jakarta.*)、模块拆分与 API 调整较大,跨大版本升级需要认真评估。 - 默认配置偏"轻":不少企业级能力(会话持久化、安全策略、监控)需要自行组合模块,开箱即用的完整度不如 Tomcat。
- 运维经验积累较少:团队若没有 Jetty 运维经验,监控指标含义、线程池调优、连接泄漏排查的学习成本更高。
- 人才储备较少:熟悉 Jetty 内部机制的开发者比例明显低于 Tomcat。
6. 适用场景对比
| 场景 | 推荐 | 理由 |
|---|---|---|
| 传统单体 Web 应用 / 企业后台 | Tomcat | 规范完整、JSP 支持、运维成熟、资料丰富 |
| Spring Boot 常规业务服务 | Tomcat(默认) | 默认路径最短,调优资料最多 |
| 高并发 WebSocket / SSE 推送服务 | Jetty | 长连接资源效率高,Handler 模型灵活 |
| 中间件、SDK、测试框架内嵌 HTTP | Jetty | 体积小、可嵌入、模块化 |
| 资源受限的边缘/嵌入式部署 | Jetty | 启动快、内存占用低 |
| 需要 HTTP/3 或前沿协议 | Jetty | 协议跟进更快 |
| 需要 AJP 对接 Apache/Nginx 前端 | Tomcat | AJP 支持成熟 |
| 需要集群会话复制、JNDI、Realm | Tomcat | 企业级特性开箱可用 |
| 需要精细控制 HTTP 解析与路由 | Jetty | Handler 链控制粒度细 |
| 团队以 Java Web 传统栈为主 | Tomcat | 学习曲线平缓,人才易得 |
7. 与周边生态的关系
7.1 与 Spring 生态
- Spring Boot 默认使用 Tomcat,
spring-boot-starter-web直接引入tomcat-embed-core;切换到 Jetty 只需排除 Tomcat 并引入spring-boot-starter-jetty。 - 两者都通过
ServletWebServerFactory抽象接入,Spring 层的差异很小,真正的差异体现在容器层行为与调优参数上。 - Spring Boot 3 起全面转向
jakarta.*命名空间,Tomcat 10+ 与 Jetty 11+ 才兼容,选型时需与 Spring Boot 大版本对齐。
7.1.1 依赖切换:pom.xml 中排除 Tomcat、引入 Jetty
Spring Boot 3 的 spring-boot-starter-web 默认传递引入 spring-boot-starter-tomcat,切换容器时必须显式排除,否则两个容器会同时出现在 classpath 上,Spring Boot 会按优先级选中 Tomcat,导致"改了依赖却没生效"的经典问题。
xml
<dependencies>
<!-- Web starter:排除默认的 Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 Jetty(Spring Boot 3 对应 Jetty 11/12,jakarta.* 命名空间) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
</dependencies>
提示:如果项目还引入了
spring-boot-starter-websocket、spring-boot-starter-security等间接依赖 Tomcat 的 starter,同样需要检查mvn dependency:tree输出,确认tomcat-embed-*已被彻底排除。
7.1.2 配置对照:server.tomcat.* 与 server.jetty.*
两者的调优参数语义并不一一对应,下表只列出最常调整的几组,切勿直接照搬数值:
| 关注点 | Tomcat 配置 | Jetty 配置 | 说明 |
|---|---|---|---|
| 最大工作线程数 | server.tomcat.threads.max |
server.jetty.threads.max |
语义相近,但 Jetty 线程池同时承担连接器与业务处理 |
| 最小空闲线程数 | server.tomcat.threads.min-spare |
server.jetty.threads.min |
Jetty 的 min 是线程池下限,不是"预留空闲线程" |
| 最大连接数 | server.tomcat.max-connections |
server.jetty.max-connections(Jetty 12 起) |
老版本 Jetty 需通过 Connector 编程式设置 |
| 请求头大小 | server.tomcat.max-http-form-post-size / max-swallow-size |
server.jetty.max-http-form-post-size |
表单与请求体上限,需按业务调整 |
| 空闲超时 | server.tomcat.connection-timeout |
server.jetty.connection-idle-timeout |
单位均为 Duration,注意写法如 30s |
| 访问日志 | server.tomcat.accesslog.enabled |
server.jetty.accesslog.enabled |
两者都支持,但日志格式字段名不同 |
yaml
# application.yml ------ 以 Jetty 为例
server:
port: 8080
jetty:
threads:
max: 200 # 工作线程上限
min: 20 # 线程池下限
max-connections: 10000
connection-idle-timeout: 30s
accesslog:
enabled: true
filename: access
retention-period: 7
# 若切回 Tomcat,对应写法如下(注意参数名不同)
# server:
# tomcat:
# threads:
# max: 200
# min-spare: 20
# max-connections: 10000
# connection-timeout: 30s
# accesslog:
# enabled: true
# prefix: access
# suffix: .log
7.1.3 编程式定制:WebServerFactoryCustomizer
当 YAML 无法覆盖某些容器特有参数(例如 Jetty 的 QueuedThreadPool 名称、Tomcat 的 Connector 属性)时,可以用 WebServerFactoryCustomizer 做类型安全的编程式定制。下面的写法同时兼容两种容器,切换时无需改代码:
java
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ContainerCustomizerConfig {
@Bean
public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> containerCustomizer() {
return factory -> {
factory.setPort(8080);
factory.setContextPath("/api");
if (factory instanceof JettyServletWebServerFactory jetty) {
// Jetty:定制 QueuedThreadPool
jetty.addServerCustomizers(server -> {
org.eclipse.jetty.util.thread.QueuedThreadPool pool =
(org.eclipse.jetty.util.thread.QueuedThreadPool) server.getThreadPool();
pool.setName("jetty-worker");
pool.setMaxThreads(200);
pool.setMinThreads(20);
pool.setIdleTimeout(60_000);
});
} else if (factory instanceof TomcatServletWebServerFactory tomcat) {
// Tomcat:定制 Connector
tomcat.addConnectorCustomizers(connector -> {
connector.setProperty("maxThreads", "200");
connector.setProperty("acceptCount", "100");
connector.setProperty("connectionTimeout", "30000");
});
}
};
}
}
注意:
addServerCustomizers/addConnectorCustomizers属于容器特有 API,务必放在instanceof分支内,避免切换容器后编译或运行期报错。
7.1.4 切换后必须重点回归验证的 3 个行为差异点
- 线程池与并发行为 :Tomcat 的
maxThreads与 Jetty 的QueuedThreadPool.maxThreads语义不同,Jetty 的线程池同时服务连接器与业务逻辑。切换后需压测确认:高并发下是否出现请求排队、线程数是否被正确限制、ThreadPoolExecutor相关监控指标是否仍可采集。 - 会话(Session)与 Cookie 行为 :两者的 Session ID 生成策略、Cookie 属性(
SameSite、HttpOnly、Secure)默认值存在差异,集群会话复制方案也不通用。切换后需验证登录态保持、多实例会话共享、以及前端跨域场景下的 Cookie 传递是否正常。 - HTTP 解析与错误处理细节 :请求头大小上限、URL 编码规则、非法请求的响应码(如 400/431)、
multipart上传的临时文件策略在两个容器中并不完全一致。切换后需回归大文件上传、超长 Header、特殊字符路径、以及全局异常处理器对容器级错误的兜底逻辑。
7.2 与反向代理和网关
- Tomcat 的 AJP 连接器在与 Apache HTTP Server 集成时成熟稳定;Jetty 更常以纯 HTTP/1.1 或 HTTP/2 方式置于 Nginx、Envoy、HAProxy 之后。
- 在 Service Mesh 场景下,两者通常都退化为"纯 HTTP 服务端",此时 Jetty 的轻量优势更明显。
7.3 与可观测性体系
- Tomcat 的 JMX MBean 体系成熟,Micrometer 等指标库对其线程池、连接数、请求处理指标支持完善。
- Jetty 同样提供 JMX 与统计 Handler,但指标命名与含义需要团队自行熟悉,接入成本略高。
7.4 与云原生部署
- 容器化部署下,Jetty 更小的镜像体积与更快的启动速度对弹性伸缩更友好。
- Tomcat 在 Kubernetes 中的运维经验、探针配置、JVM 参数模板更丰富,社区案例更多。
8. 官方文档与业界资料的丰富程度
| 维度 | Tomcat | Jetty |
|---|---|---|
| 官方文档 | 完整,含配置参考、FAQ、Wiki、迁移指南 | 完整但偏参考手册,教程性内容较少 |
| 官方示例 | 较多,覆盖集群、安全、连接器 | 有,但更偏模块化示例 |
| 社区问答 | Stack Overflow 与中文社区海量 | 数量明显较少,冷门问题需读源码 |
| 书籍与课程 | 大量中文书籍与课程 | 较少,多为英文资料 |
| 踩坑记录 | 极其丰富 | 相对零散 |
| 版本迁移资料 | 有官方迁移指南 | 有,但跨大版本变化大,需仔细阅读 |
结论:Tomcat 在"资料密度"上明显占优,Jetty 在"官方文档准确性"上不落下风,但需要读者具备更强的自主排查能力。
9. 开发者学习曲线
- 入门阶段:两者都只需理解 Servlet 生命周期即可跑通 Hello World,差异不大。
- 进阶阶段:Tomcat 的学习路径是"配置项 + 容器层级 + 运维工具",知识点多但资料全;Jetty 的学习路径是"Handler 链 + 模块组合 + 线程池策略",概念更抽象但体系更简洁。
- 精通阶段 :Tomcat 需要理解连接器内部实现、Valve 管道、类加载机制;Jetty 需要理解
Handler组合语义、Connector与 ALPN 协商、QueuedThreadPool调度策略。 - 团队视角:Tomcat 的"平均上手成本"更低,因为默认配置与默认资料都在帮你;Jetty 的"精通成本"不低,但一旦掌握,定制能力更强。
10. 新项目交付时的选型建议
10.1 决策流程
#mermaid-svg-39adSuKcQBUl0DbP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-39adSuKcQBUl0DbP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-39adSuKcQBUl0DbP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-39adSuKcQBUl0DbP .error-icon{fill:#552222;}#mermaid-svg-39adSuKcQBUl0DbP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-39adSuKcQBUl0DbP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-39adSuKcQBUl0DbP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-39adSuKcQBUl0DbP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-39adSuKcQBUl0DbP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-39adSuKcQBUl0DbP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-39adSuKcQBUl0DbP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-39adSuKcQBUl0DbP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-39adSuKcQBUl0DbP .marker.cross{stroke:#333333;}#mermaid-svg-39adSuKcQBUl0DbP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-39adSuKcQBUl0DbP p{margin:0;}#mermaid-svg-39adSuKcQBUl0DbP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-39adSuKcQBUl0DbP .cluster-label text{fill:#333;}#mermaid-svg-39adSuKcQBUl0DbP .cluster-label span{color:#333;}#mermaid-svg-39adSuKcQBUl0DbP .cluster-label span p{background-color:transparent;}#mermaid-svg-39adSuKcQBUl0DbP .label text,#mermaid-svg-39adSuKcQBUl0DbP span{fill:#333;color:#333;}#mermaid-svg-39adSuKcQBUl0DbP .node rect,#mermaid-svg-39adSuKcQBUl0DbP .node circle,#mermaid-svg-39adSuKcQBUl0DbP .node ellipse,#mermaid-svg-39adSuKcQBUl0DbP .node polygon,#mermaid-svg-39adSuKcQBUl0DbP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-39adSuKcQBUl0DbP .rough-node .label text,#mermaid-svg-39adSuKcQBUl0DbP .node .label text,#mermaid-svg-39adSuKcQBUl0DbP .image-shape .label,#mermaid-svg-39adSuKcQBUl0DbP .icon-shape .label{text-anchor:middle;}#mermaid-svg-39adSuKcQBUl0DbP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-39adSuKcQBUl0DbP .rough-node .label,#mermaid-svg-39adSuKcQBUl0DbP .node .label,#mermaid-svg-39adSuKcQBUl0DbP .image-shape .label,#mermaid-svg-39adSuKcQBUl0DbP .icon-shape .label{text-align:center;}#mermaid-svg-39adSuKcQBUl0DbP .node.clickable{cursor:pointer;}#mermaid-svg-39adSuKcQBUl0DbP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-39adSuKcQBUl0DbP .arrowheadPath{fill:#333333;}#mermaid-svg-39adSuKcQBUl0DbP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-39adSuKcQBUl0DbP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-39adSuKcQBUl0DbP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-39adSuKcQBUl0DbP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-39adSuKcQBUl0DbP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-39adSuKcQBUl0DbP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-39adSuKcQBUl0DbP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-39adSuKcQBUl0DbP .cluster text{fill:#333;}#mermaid-svg-39adSuKcQBUl0DbP .cluster span{color:#333;}#mermaid-svg-39adSuKcQBUl0DbP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-39adSuKcQBUl0DbP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-39adSuKcQBUl0DbP rect.text{fill:none;stroke-width:0;}#mermaid-svg-39adSuKcQBUl0DbP .icon-shape,#mermaid-svg-39adSuKcQBUl0DbP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-39adSuKcQBUl0DbP .icon-shape p,#mermaid-svg-39adSuKcQBUl0DbP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-39adSuKcQBUl0DbP .icon-shape .label rect,#mermaid-svg-39adSuKcQBUl0DbP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-39adSuKcQBUl0DbP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-39adSuKcQBUl0DbP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-39adSuKcQBUl0DbP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
Tomcat 经验为主
Jetty 经验为主
均无
新项目启动
是否需要内嵌 HTTP 服务或极小体积?
优先 Jetty
是否有高并发长连接/WebSocket 需求?
是否需要 JSP/AJP/集群会话等企业级特性?
优先 Tomcat
团队是否已有成熟容器运维经验?
默认 Tomcat,降低交付风险
10.2 具体建议
- 默认选 Tomcat:除非有明确理由,Spring Boot 项目保持默认容器是最稳妥的交付选择,调优资料、监控接入、故障排查路径都最短。
- 以下情况优先 Jetty:需要把 HTTP 服务嵌入到库或工具中;WebSocket/SSE 连接数极大;镜像体积与启动速度是硬指标;需要 HTTP/3 等前沿协议。
- 不要为了"性能"盲目切换:在常规业务负载下,两者的吞吐差异往往小于业务代码与数据库带来的差异,切换前应做压测验证。
- 关注命名空间对齐:Spring Boot 3 / Jakarta EE 9+ 场景下,Tomcat 需 10+、Jetty 需 11+,版本错配是最常见的踩坑点。
- 把容器当作可替换组件 :通过 Spring 的
ServletWebServerFactory抽象与统一的可观测性埋点,让容器切换成本可控,避免业务代码直接依赖容器 API。 - 团队能力优先于技术参数:容器是长期运维对象,团队熟悉度往往比纸面性能更重要。
10.3 常见经验教训
- 线程池参数照搬会出事 :Tomcat 的
maxThreads与 Jetty 的QueuedThreadPool.maxThreads语义不同,直接照搬数值容易导致排队或线程爆炸。 - 长连接要单独评估:WebSocket 场景下连接数与线程数的关系在两个容器中差异明显,需按实际连接规模压测。
- 升级大版本前先读迁移指南 :Jetty 的模块拆分与包名变化、Tomcat 的
javax→jakarta迁移,都是高频踩坑点。 - 监控指标要提前对齐:不同容器的指标命名不同,接入 APM 前先确认关键指标(活跃连接、排队请求、线程池饱和度)是否可采集。
11. 总结
Tomcat 与 Jetty 并非"谁替代谁"的关系,而是两种设计取向的代表:
- Tomcat:规范完整、生态繁荣、运维成熟,是绝大多数 Java Web 项目的稳妥默认选择。
- Jetty:轻量、可嵌入、长连接友好、协议跟进快,是嵌入式与高并发连接场景的优选。
选型的核心不是比较纸面性能,而是回答三个问题:我的部署形态是什么、我的连接模型是什么、我的团队最熟悉什么。 想清楚这三点,答案通常就很清晰了。