
本文为 AtomGit 码动四季·开源同行征稿活动参与文章摘要Spring Boot 默认内嵌 Tomcat换成 Undertow 只需排除 引入两步 pom 操作但能跑和跑得干净之间隔着三处暗坑传递依赖把 Tomcat 悄悄拉回 classpath、Tomcat 表达式语言的组件冲突、server.tomcat.*配置静默失效。本文基于 ArticlePilot 实仓的容器化提交2026-09-17逐坑拆解给出依赖树终检 容器红线写进 AGENTS.md 配置逐项翻译三道防线——替换影响面被锁定在 pom 与配置层业务代码零改动回退只需还原两处依赖。文章目录一、替换的两步标准操作二、三个暗坑与解法1传递依赖把 Tomcat 拉回来双容器共存的假象2Tomcat 表达式语言的传递组件冲突3按 Tomcat 行为写的配置悄悄失效三、验证与回退四、总结Spring Boot 默认内嵌 Tomcat多数项目够用。但 ArticlePilot 是容器化部署的单机管理台内存 footprint 和连接处理的轻量性比默认更值钱于是在 9 月 17 日的容器化改造里把 Web 容器换成了 Undertow。替换本身只要两步 pom 操作真正花时间的是随后冒出来的三处暗坑——都与某条传递依赖悄悄把 Tomcat 拉回来或Tomcat 专属行为被默认依赖有关。这篇把这些坑一次讲完照着做可以少走弯路。先交代证据这是当天容器化改造的原始提交后面所有的坑都发生在这个提交的前后。一、替换的两步标准操作替换的核心思路排除 starter 里的 Tomcat再引入 Undertow starter。在spring-boot-starter-web上做排除!-- pom.xml排除默认 Tomcat引入 Undertow --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactIdexclusionsexclusiongroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-tomcat/artifactId/exclusion/exclusions/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-undertow/artifactId/dependency这一步做完启动就是 Undertow 了。但能跑和跑得干净之间隔着下面三个坑。exclude是否仍是 Tomcatstarter-web移除 starter-tomcat引入 starter-undertow启动容器是Undertow 吗第 1 步完成传递依赖把Tomcat 拉回来了二、三个暗坑与解法1传递依赖把 Tomcat 拉回来双容器共存的假象现象排除后启动日志确实打了 Undertow但运行行为怪异——某次引入一个第三方 starter 后Tomcat 的类又出现在 classpath 里端口偶尔被双容器抢占。根因其他 starter 或业务依赖可能传递引用tomcat-embed-*排除只对starter-web这一条路径生效防不住别的路径。解决替换完成后用依赖树做一次终检确认tomcat-embed只允许出现在websocket等明确豁免的路径里其余一律在对应依赖上补 exclusion# 依赖树终检确认没有旁路把 Tomcat 拉回来mvn dependency:tree-Dincludesorg.apache.tomcat.embed效果终检跑完后依赖树里org.apache.tomcat.embed零意外命中双容器抢占端口的问题此后未再复现。感受排除是点防御依赖树是面验收——单点排除永远防不住全图的传递路径终检这一步不能省。教训容器替换的验收标准是依赖树里没有多余容器不是启动日志换了名字。2Tomcat 表达式语言的传递组件冲突现象启动期报 EL 相关的类冲突——项目里某个依赖携带了 Tomcat 的表达式语言实现与 Undertow 环境下的 JSP/EL 期望不一致。根因个别老组件默认按 Tomcat 环境打包把tomcat-embed-el当成必备依赖传递进来。解决在该依赖上显式排除 Tomcat 表达式语言EL 统一走标准实现这条约束后来写进了 AGENTS.md 的安全红线“后端 Web 容器是 Undertow不要引入依赖 Tomcat 的传递组件”。pom 里的实际排除块长这样——starter-tomcat和tomcat-embed-el是两处独立的排除缺一不可效果双排除落地后 EL 冲突消失容器红线写进 AGENTS.md 至今同类问题零复发——包括 AI 编程助手生成的新依赖引入代码也会遵守这条约定。感受一次排查的成本换一条长期生效的红线是这次替换里性价比最高的动作。教训给仓库写一条容器红线比每次事后排查传递依赖便宜得多——AI 编程助手和后来的协作者都会读这条约定。3按 Tomcat 行为写的配置悄悄失效现象替换后个别连接参数不生效。逐项核对才发现server.tomcat.*前缀的配置项在 Undertow 下是死配置Undertow 有自己的一套server.undertow.*。根因Spring Boot 的配置前缀与容器绑定换容器等于换配置命名空间旧配置不报错、只是静默不生效。解决按server.undertow.*重写容器配置工作线程、IO 线程、请求超时、访问日志并对替换做一次显式验证——启动日志确认容器名 压测观察参数是否生效# server.undertow.* 替代 server.tomcat.*Boot 3.2.x 实际键名 server.undertow.threads.io8 server.undertow.threads.worker64 server.undertow.no-request-timeout30s server.undertow.accesslog.enabledtrue常用配置项的迁移对照替换前逐项过一遍用途Tomcat 配置项Undertow 配置项IO 线程数server.tomcat.threads.min-spare近似server.undertow.threads.io工作线程数server.tomcat.threads.maxserver.undertow.threads.worker请求超时server.tomcat.connection-timeoutserver.undertow.no-request-timeout访问日志server.tomcat.accesslog.enabledserver.undertow.accesslog.enabled效果配置逐项翻译后线程与超时参数在压测下按预期生效没有配置了但没反应的残留项。感受这张对照表是整个替换里最费时间也最值得的部分——静默失效比报错可怕得多报错会逼你修静默失效只在生产上等你。教训换容器不是依赖替换是配置命名空间的迁移server.tomcat.*要逐项翻译而不是留着当摆设。三、验证与回退三个坑对应三道防线收拢成一张图替换前照着设防验收动作按固定顺序走一遍任何一步不达标就退回上一步排查替换的验证三件套mvn dependency:tree终检依赖路径、启动日志确认容器、冒烟测试覆盖核心接口。回退方式也很简单——pom 里恢复 starter-tomcat 依赖并删掉 Undertow starter 即可数据层与业务代码不感知容器差异这也是容器层替换值得做的原因影响面被 Spring Boot 的抽象锁在很小的范围里。四、总结Tomcat 到 Undertow 的替换动手只要五分钟做干净要半天。三处暗坑的共同规律是替换的边界不是你改的那个依赖而是整个依赖图里所有隐式依赖 Tomcat 的路径。把依赖树终检 容器红线写进 AGENTS.md 配置前缀逐项翻译三件事做完这次替换才真正结束。真实性声明本文操作与坑例来自 ArticlePilot 仓库 2026-09-17 提交feat(infra): 容器 Undertow 化与 pom.xml 实际配置可对照复核环境基准为 Spring Boot 3.2.5 / Java 17。开源仓库地址ArticlePilotpom.xml 排除配置与 AGENTS.md 容器红线可直接对照https://atomgit.com/dickeryang/articlepilot参考资源Undertow 官方文档Spring Boot 官方文档Web Server 配置专栏导航上一篇ArticlePilot 从 0 到开源的迭代复盘下一篇稿件并发编辑的乐观锁保护如果本文对你有帮助欢迎点赞、收藏、转发。有任何问题或建议请在评论区留言交流。行文仓促定有不足之处欢迎各位朋友在评论区批评指正不胜感激。