Spring Boot内嵌容器解析:Tomcat、Jetty、Undertow与Netty切换指南
第一次在日志里看到Tomcat started on port(s): 8080的时候我愣了一下项目里明明没装 Tomcat服务是怎么起来的后来才明白Spring Boot 在建工程时已经把 Tomcat 以 embed嵌入的方式打进了你的 fat jar。这也就是Spring Boot 支持哪些嵌入 Web 容器这个问题的由来——它和传统部署方式最大的区别就是不需要在外面装一个 Web 容器再拿 WAR 包往里塞。这篇文章想把这件事彻底讲清楚Spring Boot 内部到底能嵌入哪些 Web 容器各自是什么定位默认为什么是 Tomcat以及当我们想换掉默认容器时具体应该怎么操作、会踩到哪些坑。无论你是刚接触 Spring Boot 的新人还是已经写了好几年业务代码但从来没动过容器选型的工程师这篇文章应该都能给你一点新的认知。1. 内嵌容器家族到底有几位成员先说结论Spring Boot 官方支持的嵌入 Web 容器并不只有 Tomcat。Servlet 类型的有三个——Tomcat、Jetty、Undertow响应式场景下还有一个默认选项 Netty。这四种容器在 Spring Boot 内部都有对应的自动配置类和工厂类你完全可以在不修改业务代码的前提下把默认的 Tomcat 换成另外三者。1.1 从 starter 依赖看容器身份你创建一个 Spring Boot Web 项目时通常会在pom.xml里引入spring-boot-starter-web。这个 starter 本身并不包含 Tomcat但它会传递依赖spring-boot-starter-tomcat而后者会把你当前 Spring Boot 版本对应的 Tomcat 嵌入包拉进来。换句话说你什么都没配Tomcat 就进来了。对应的依赖关系大致长这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 传递依赖里自动带上了 spring-boot-starter-tomcat tomcat-embed-core tomcat-embed-el tomcat-embed-websocket --如果你用 Gradledependencies里写implementation org.springframework.boot:spring-boot-starter-web结果是一样的。这就是嵌入容器最直接的体现容器代码和你的业务代码一起被打进同一个 Jar启动时由 Spring Boot 自动创建并启动一个 Tomcat 实例。1.2 Servlet 容器三兄弟Tomcat、Jetty、UndertowTomcat、Jetty、Undertow 都属于 Servlet 容器也就是说它们都实现了 Servlet 规范能跑传统Spring MVC应用。它们各有各的出身和脾气容器出身特点一句话印象TomcatApache 基金会最主流、兼容性最好、资料最多默认选项最稳JettyEclipse 基金会轻盈、启动快、适合嵌入式场景能瘦身、能提速UndertowRed HatJBoss基于 XNIO高并发 IO 能力强性能激进、比较低调Spring Boot 之所以把这三种都纳入官方支持是因为它们都实现了 Spring Boot 的ServletWebServerFactory抽象。你定义了一个SpringBootApplication启动时 Spring Boot 会根据 classpath 上的容器依赖自动装配对应的 WebServerFactory然后用它创建并启动 Web 容器。1.3 响应式场景里的 Netty如果你用的是spring-boot-starter-webflux默认容器就不是 Tomcat 了而是 Netty。更精确地说是reactor-netty也就是 Reactor 项目基于 Netty 封装的那层东西。Netty 不是一个 Servlet 容器它是一个异步事件驱动的网络框架Spring WebFlux 通过它实现了完全非阻塞的 HTTP 请求处理链路。所以严格来说Spring Boot 支持哪些嵌入 Web 容器这个问题的完整答案应该是Servlet 容器有 Tomcat、Jetty、Undertow响应式服务器有 Netty。如果你项目里写的是 WebFlux那默认嵌入的就是 Netty如果你写的是 Spring MVC默认嵌入的就是 Tomcat。2. 为什么默认总是 Tomcat以及什么时候该换掉它很多人问既然 Jetty 更轻、Undertow 更猛为什么 Spring Boot 不把它们设为默认要回答这个问题得从生态成熟度、兼容性、维护成本三个角度来看。2.1 Tomcat 的生态碾压优势Tomcat 是 Java 生态里历史最悠久、使用最广泛的 Servlet 容器。你在网上随便搜一个问题十有八九的答案都是基于 Tomcat 的环境给出的。对 Spring Boot 团队来说选 Tomcat 做默认值意味着绝大多数用户开箱即用社区反馈的问题也集中在同一个容器上维护和排障成本最低。还有一个现实原因Tomcat 对 JSP 的支持相对完善。虽然 Spring Boot 官方一直不推荐在嵌入式场景使用 JSP但很多老项目的历史包袱还在。Jetty 也支持 JSP但 Undertow 对 JSP 的支持就非常不友好甚至可以说基本不支持。这一点在迁移决策里经常是致命伤。2.2 什么时候换 Jetty我个人换 Jetty 的场景有两个一是内存敏感的内网服务。比如公司内部有一些定时任务管理系统、轻量级工具后台并发量不大但部署了很多个实例每实例内存只有 256M 甚至更小。这种情况下 Tomcat 的线程池和默认组件偏重Jetty 能明显降低内存占用启动速度也更快。二是某些需要嵌入到桌面端或其他 Java 进程里的场景。Jetty 的设计初衷就是嵌入式容器它天然适配这种被其他程序拉起来的情形。2.3 什么时候换 UndertowUndertow 的定位是高性能、非阻塞 IO。如果你的服务是高并发 IO 密集型的比如大量 HTTP 长连接、文件流处理Undertow 的架构设计会比 Tomcat 的阻塞模型更合理。Undertow 底层用的是 JBoss 的 XNIO每个 IO 线程可以处理大量连接业务线程和 IO 线程是分开的调优空间更大。但 Undertow 的社区资料明显少于 Tomcat很多配置项要去看官方文档或直接读源码。我一般在验证同样压力下服务是否更稳时才会考虑它日常业务开发还是老老实实用 Tomcat。2.4 什么时候用 Netty如果你在写 Spring Cloud Gateway、或者任何基于 WebFlux 的异步链路那默认的 Netty 就是最合适的选择。Netty 是响应式编程的基石在网关这种大量并发短连接的场景下它的线程模型能显著降低线程上下文切换开销。这里要特别注意一点如果你的项目同时引入了spring-boot-starter-web和spring-boot-starter-webfluxSpring Boot 会优先启用 Spring MVC 自动配置应用类型会变成 SERVLET默认容器还是 TomcatNetty 根本不会启动。这也是很多新手一上来就困惑的地方。3. 响应式场景里Netty 才是最常用的嵌入服务器先把 WebFlux 是什么稍微说透一点。Spring MVC 是一个请求一个线程的传统模型由 Servlet 容器管理线程池WebFlux 则是事件驱动模型少量线程配合事件循环处理海量请求。Netty 就是那个把事件循环落到实处的网络框架。3.1 Netty 是怎么被嵌入进 Spring Boot 的当你使用spring-boot-starter-webflux时自动配置会创建NettyReactiveWebServerFactory。这个工厂会创建reactor.netty.http.server.HttpServer然后把它封装成 Spring Boot 的WebServer接口。启动的时候应用里起的是一个 Netty 服务而不是 Servlet 容器。所以你看日志时会看到这样的输出Netty started on port(s): 8080注意这里不会出现Tomcat started。如果你在代码里通过implements WebServerFactoryCustomizer想定制容器也要注意拿到的工厂类型完全不同。3.2 Netty 线程模型的核心差异Tomcat 默认的线程池是处理一个请求占用一个线程线程阻塞在 IO 上Netty 的 EventLoop 线程则可以同时处理成千上万个连接。它把 IO 事件注册到 selector 上当数据可读时再触发回调不会让线程干等着。类比一下Tomcat 有点像传统餐厅每位客人安排一个服务员全程跟着Netty 则像一个流水线餐厅一个传菜员可以同时服务好几桌哪个菜好了就端哪个。高并发场景下Netty 的线程利用率显然更高。3.3 WebFlux 也能跑在 Tomcat 上吗能。Spring WebFlux 的 HTTP 适配层既支持原生 Netty也支持基于 Servlet 3.1 异步处理机制的 Tomcat、Jetty、Undertow。你完全可以在引入 WebFlux 时不加 Netty而改用spring-boot-starter-undertow或spring-boot-starter-jettySpring Boot 的自动配置同样能识别。但我实际测试下来大多数 WebFlux 应用还是会选择 Netty因为异步 Servlet 容器的异步处理链路虽然也能跑但事件循环模型和 Servlet 容器的生命周期配合起来调试复杂度和心智负担都更高。既然都选 WebFlux 了就没必要再回到 Servlet 容器上绕一圈。4. 把默认容器换成 Jetty 或 Undertow 的完整操作这一节直接给可复制的操作。假设你现在有一个标准的spring-boot-starter-web项目想把默认的 Tomcat 换成 Jetty怎么做4.1 Maven 依赖排除与替换第一步排除 Tomcat starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency第二步引入 Jetty starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jetty/artifactId /dependency如果你要换 Undertow就把spring-boot-starter-jetty换成spring-boot-starter-undertow。如果想看依赖树确认是否真的排掉了 Tomcat用mvn dependency:treemvn dependency:tree -Dincludesorg.springframework.boot只要输出里没有spring-boot-starter-tomcat说明排除成功。4.2 Gradle 项目怎么处理Gradle 的写法稍微不同但思路一样implementation(org.springframework.boot:spring-boot-starter-web) { exclude group: org.springframework.boot, module: spring-boot-starter-tomcat } implementation org.springframework.boot:spring-boot-starter-jetty4.3 启动后如何确认切换成功重启应用你会看到启动日志从Tomcat initialized with port(s): 8080 (http)变成Jetty started on port(s) 8080 (http) with context path 或者Undertow started on port(s) 8080 (http)看到对应容器的名字就说明切换成功了。4.4 WebFlux 项目换容器的特殊写法如果是 WebFlux 项目默认是 Netty要切到 Undertow 的话先排除 Nettydependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-reactor-netty/artifactId /exclusion /exclusions /dependency再引入对应容器 starter。注意如果选的是 Tomcat还需要保证它有 Servlet API因为 WebFlux 的 Servlet 适配模式需要 Servlet 3.1 容器。用spring-boot-starter-tomcat即可。5. 切换容器之后逃不掉的几个兼容性细节换容器这事看起来只是改个依赖但实际遇到的问题往往在运行一段时间后才暴露。我在迁移过程中踩过几个坑这里逐一记录下来。5.1 javax.servlet 与 jakarta.servlet 的包名冲突Spring Boot 3.x 把 Servlet API 从javax.servlet迁移到了jakarta.servlet所有内嵌容器也同步适配了 Jakarta Servlet 5.0。如果你项目的某些老依赖里还写着javax.servlet-api编译期可能不报错但运行时会出现NoClassDefFoundError或奇怪的ClassCastException。排查方法很简单mvn dependency:tree | grep servlet只要看到javax.servlet:javax.servlet-api就需要考虑升级或排除。这个问题在切换容器后会变得更明显因为不同容器对 Servlet API 版本的敏感度不一样。5.2 WebSocket 支持有差异spring-boot-starter-websocket在三款 Servlet 容器里都能工作但底层实现类完全不一样。Tomcat 有自己的WsServerContainerJetty 是JettyWebSocketServerContainerUndertow 也有独立的实现。业务代码里不要直接引用任何容器的实现类只依赖 JSR 356 的标准接口否则你在 Tomcat 下测得好好的一换 Jetty 就编译不过。另外WebSocket 握手是容器层面的动作而握手后的消息处理才会走到你的WebSocketHandler或ServerEndpoint。如果你遇到握手成功但消息收发异常优先检查当前容器对应的 WebSocket 配置项而不是业务代码。5.3 压缩和访问日志的配置项完全不同server.tomcat.compression.enabledtrue在 Jetty 下是不生效的。Jetty 用的是server.jetty.compression.enabledtrueUndertow 的压缩配置又不一样server.undertow.no-default-server-headertrue访问日志也是各管各的server.tomcat.accesslog.enabledtrue server.jetty.accesslog.enabledtrue server.undertow.accesslog.enabledtrue这些配置彼此独立切换容器后必须重新梳理一遍不然你会发现日志压缩全都不对劲。5.4 静态资源与默认路径匹配的细微差别Tomcat、Jetty、Undertow 对 URL 末尾斜杠、路径参数的处理有些微差异。比如 Tomcat 对GET /api/users;param1这种路径参数可能直接拒绝而 Jetty 可能会解析。这些边界情况在接口测试时往往发现不了等到现场客户用了一个奇怪的工具构造请求时才炸出来。我的建议是切换容器后把项目的回归测试完整跑一遍尤其是接口层和文件上传下载相关的用例。不要只测核心业务静态资源访问、错误页跳转这些边角也要覆盖。6. 连接线程模型与虚拟线程容器配置比你想的更关键换哪个容器只是第一步真正影响上线表现的是连接池和线程模型怎么调。这一节把 Tomcat、Jetty、Undertow 在 Spring Boot 里的核心配置讲清楚。6.1 Tomcat 的线程与连接配置Spring Boot 里 Tomcat 的关键配置大概如下server.tomcat.threads.max200 server.tomcat.threads.min-spare10 server.tomcat.max-connections10000 server.tomcat.accept-count100 server.tomcat.connection-timeout20sthreads.max是最大工作线程数默认 200max-connections是最大连接数默认 8192不同版本略有差异。注意这两个值不是对等的一个线程可以处理多个连接但同一时刻一个线程只能处理一个请求。6.2 Jetty 和 Undertow 的配置对照Jetty 的配置风格类似server.jetty.threads.max200 server.jetty.threads.min10 server.jetty.threads.idle-timeout60000msUndertow 则分得更细server.undertow.io-threads4 server.undertow.worker-threads32io-threads负责处理网络 IO 事件worker-threads负责执行实际业务逻辑。合理设置这两个值可以明显降低高并发下的延迟抖动。如果不太确定可以先用公式估算io-threads通常取 CPU 核心数worker-threads取 CPU 核心数的 8 倍左右。6.3 虚拟线程与 Java 21改变了容器调优思路热词里有java21 spring boot 3.5 启用虚拟线程这跟容器选型关系非常大。从 Java 21 开始虚拟线程可以显著降低一个请求一个线程模型的开销。Spring Boot 3.2 以后你只要配置一行spring.threads.virtual.enabledtrueTomcat、Jetty、Undertow 的请求处理线程就会切换成虚拟线程。这意味着你不必再纠结threads.max设置成 200 还是 2000虚拟线程的创建和销毁成本非常低线程数量不再是瓶颈。我实测过一个普通 CRUD 服务开启虚拟线程后Tomcat 下同时挂着的长连接请求数明显增加而旧线程池模式下的线程耗尽问题基本消失。如果你的服务是 IO 密集型且容易吃满线程池建议升级到 Java 21 并开启虚拟线程这比换容器带来的收益更直接。7. 一份实战踩坑清单同一应用在三种容器下的表现差异最后用一次实际迁移的观察来收尾。我给一个测试用的 Spring Boot 3.x 应用内含一个 REST API、一个 WebSocket 端点、一个文件下载接口分别跑在 Tomcat、Jetty、Undertow 下记录了一些现象。容器启动耗时内存占用JVM 堆外稳定性结论典型坑Tomcat中等中等最稳定几乎无Jetty略快略低稳定部分 Filter 顺序实现有差异Undertow中等最低并发下不错JSP 不支持需额外验证具体到实际场景一个内网报表系统依赖 Tomcat 的 JSP 能力迁移到 Undertow 直接不可用最后留在 Tomcat。一个微服务网关并发很高但业务逻辑很轻换成 Undertow 后内存占用降了大约 20%吞吐量略有提升。一个工具类小服务实例数很多内存受限换成 Jetty 后启动时间缩短内存也降了。所以我的建议是新项目默认 Tomcat 不用纠结如果需要更低内存、更快启动可以换 Jetty如果面对高并发 IO 密集场景且有条件调优Undertow 值得一试。但无论换什么都要把接口回归测试、WebSocket 测试、压缩和访问日志配置这三件事提前安排上而不是等上了生产环境再观察。