尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot替换内嵌Tomcat版本的完整指南

Spring Boot替换内嵌Tomcat版本的完整指南 先说点实际的。很多人拿到一个Spring Boot项目除了业务代码的调优还会遇到一个既基础又容易卡壳的问题怎么把项目里那个内嵌的Tomcat换成指定版本。这个需求乍一听很小真上手去改的人十个里有六七个会在依赖冲突、版本不生效、启动报错这些坑里转一圈。我见过太多同事改个版本号改到怀疑人生最后又默默改回来。这篇就围绕“替换内嵌Tomcat版本”这件事把原理、操作和坑一次性说清楚。这个话题适合谁看主要是正在维护Spring Boot项目的后端开发尤其是遇到安全通告要求升级Tomcat版本、或者项目要适配公司统一容器版本、又或者线上某个Tomcat行为表现不对劲需要降级/对齐版本的场景。无论你是Maven项目还是Gradle项目都能在这里找到对应的改法和验证思路。1. 为什么非得跟这个内嵌的Tomcat过不去Spring Boot最大的便利之一就是内嵌容器。你引入一个spring-boot-starter-web容器自动就带进来了不用装Tomcat、不用配Server.xml直接java -jar就能跑。这个设计帮大家省了很多事但也带来了一个副作用你平时根本不知道项目里内嵌的Tomcat是什么版本也不关心。直到某一天你不得不开始关心它。我总结一下实际工作中最常见的三个触发场景。第一个是安全通告。运维或安全团队某天甩给你一份报告说某个Tomcat版本存在漏洞你们项目的内嵌Tomcat版本在影响范围之内限期整改。Spring Boot官方当然会跟着升级依赖版本但官方版本节奏未必跟得上你手里的老项目。你不可能为了一个Tomcat小版本升级就把整个Spring Boot版本升上去那样牵连面太大各种兼容性测试都要重做。最稳妥的方案就是单独把Tomcat换成合规版本改完验证没问题先交差再排期升级Boot版本。第二个是兼容性对齐。很多公司在内部有统一的技术基线比如规定Java版本、容器版本、中间件版本。你的项目如果用了默认内嵌Tomcat版本说不定就跟公司的基线不一致。还有一种情况是你的老项目原本跑在外置Tomcat 8.5上后来迁移到Spring Boot内嵌容器一启动就发现某些行为不对劲。这种差异往往是Tomcat小版本更新带来的比如请求编码处理方式、连接器参数默认值的变化。把内嵌Tomcat版本换成和老环境一致的版本能省掉很多排查老怪问题的时间。第三个是特定功能需求。比如新的Tomcat版本才支持的HTTP/2特性、或者某些性能优化选项而项目暂时没办法升级Spring Boot版本只能局部替换Tomcat。这个场景相对少一些但也真实存在。一句话总结这个需求核心就是我的Spring Boot版本暂时不变但内嵌的Tomcat版本必须换成某个指定版本。搞清楚目标之后接下来看具体怎么做。2. 动手前先摸清现状当前版本和版本管理机制替换版本之前第一件事是搞清楚项目当前用的Tomcat版本。千万别凭感觉我见过有人自信满满地说项目用的是Tomcat 9.0.65结果一查依赖树发现是9.0.78。版本都看错了后面的操作自然跟着错。查当前版本最直接的方式是Maven依赖树。在项目根目录执行mvn dependency:tree -Dincludesorg.apache.tomcat.embed输出里会明确列出tomcat-embed-core、tomcat-embed-el、tomcat-embed-websocket这些组件的版本。如果用的是Gradle执行./gradlew dependencyInsight --dependency tomcat-embed-core包发布到服务器之后也可以不看构建工具直接看产物。Spring Boot的可执行Jar本质上是一个压缩包里面有个BOOT-INF/lib目录所有依赖jar都在里面。用解压工具打开看一眼tomcat-embed-core-9.0.xx.jar这样的文件名直接把版本写在脸上。这个方式在生产排查时尤其好用不需要本地环境随便什么机器上都能看。查完当前版本下一步要理解Spring Boot是怎么管理Tomcat版本的。这关系到你的修改能不能生效也关系到后面排查问题时的定位方向。Spring Boot的所有依赖版本都集中在官方BOMBill of Materials里也就是spring-boot-dependencies这个POM文件。在这个BOM中Tomcat各组件的版本并不是硬编码写死的而是通过一个占位符属性来管理的。这个属性名就是tomcat.version。也就是说Spring Boot中内嵌Tomcat的核心版本号其实全部由tomcat.version这一个属性统一控制tomcat-embed-core、tomcat-embed-el、tomcat-embed-jasper、tomcat-embed-websocket都读同一个值。你可能会问为什么不直接改spring-boot-dependencies里的版本号或者在自己项目的pom.xml里重新声明一遍Tomcat依赖这是个好问题。直接改BOM肯定不现实你不可能去改动框架自带的POM重新在pom.xml里声明Tomcat依赖倒是可行但你要同时声明好几个组件漏掉任何一个都可能出问题。而利用tomcat.version这个属性只需要改一处所有Tomcat相关组件会一起跟着变简单又不容易出错。理解了这一层后边的操作其实就一句话的事在项目里把tomcat.version这个属性值改成你想要的版本。但不同构建方式、不同项目形态下具体写法有差别坑也不一样。3. 实际操作三种方式把版本换成你说了算3.1 Maven项目最标准的方式覆盖tomcat.version属性如果你的项目是Maven项目而且继承了Spring Boot的父POM写法非常简单。在项目的pom.xml中找到properties节点加一行properties java.version1.8/java.version !-- 替换内嵌Tomcat版本按实际情况填写具体版本号 -- tomcat.version9.0.76/tomcat.version /properties加上这行之后Maven在解析Spring Boot BOM时会优先使用你在子项目里定义的属性值从而覆盖掉Spring Boot默认的Tomcat版本号。这里有个前提条件容易被人忽视你的项目中spring-boot-starter-parent必须是父POM或者至少通过parent继承了它。为什么因为Maven的属性覆盖规则是“子项目覆盖父项目”。你通过父POM继承了spring-boot-dependencies的依赖管理父级里tomcat.version的默认值就能被子级覆盖掉。改完pom.xml之后记得在IDEA里点击右侧Maven面板的刷新按钮或者执行mvn clean compile然后再用mvn dependency:tree -Dincludesorg.apache.tomcat.embed看一眼确认版本确实变了。这一步是很多人的盲区——改完配置不重新加载依赖跑起来还是旧版本然后就开始怀疑人生。3.2 Gradle项目同样廉价一个ext属性搞定Gradle项目操作起来也不复杂。如果你用了Spring Boot Gradle插件和spring-boot-dependencies的BOM导入在build.gradle里加一行ext[tomcat.version] 9.0.76原理跟Maven的properties一致通过覆盖Spring Boot BOM里定义的属性来影响所有Tomcat组件版本。改完执行./gradlew clean build然后查看依赖验证结果./gradlew dependencyInsight --dependency tomcat-embed-core记住Gradle的系统属性、项目属性、ext属性之间有优先级差异如果只设置系统属性而没设ext可能不会生效。我建议直接写在ext里最直观。3.3 没有继承Spring Boot父POM怎么办还有一种项目形态没有用spring-boot-starter-parent作为父POM而是通过dependencyManagement直接导入spring-boot-dependencies。这种写法在早期非Spring Boot全家桶项目里很常见。如果你在这个项目里直接加tomcat.version属性大概率是不生效的。原因很微妙。Maven在解析BOM时BOM内部自己的属性插值发生在BOM被外部项目引用之前。简单说spring-boot-dependencies这个POM在构建时就已经用自己的默认值把${tomcat.version}插值计算完了外部项目单独定义同名属性无法反向影响BOM内部的结果。而parent继承则不同属性覆盖发生在解析整个继承链的过程中所以子项目定义的属性能覆盖父类默认值。遇到这种情况正确的做法是在自己的dependencyManagement里显式声明要替换的Tomcat组件和版本。举个例子dependencyManagement dependencies dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-core/artifactId version9.0.76/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-el/artifactId version9.0.76/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-websocket/artifactId version9.0.76/version /dependency /dependencies /dependencyManagement注意这里有个坑如果你同时引入了tomcat-embed-jasper也要一并声明版本。因为你手动指定了部分组件的版本之后剩下的组件仍然会被BOM管理它们之间的版本如果对不上很容易出现NoSuchMethodError这类运行期错误。所以要么全部手动声明要么一个都不声明。如果项目里需要JSP支持别忘了一起把tomcat-embed-jasper列进去。3.4 别把外置Tomcat部署和这个搞混还有一种情况经常被人当成“替换指定版本的Tomcat”来讨论把项目部署到外部Tomcat 8.5或9.0上。这种情况其实是用外置Tomcat替代内嵌Tomcat做法是把spring-boot-starter-web里的spring-boot-starter-tomcat改成provided作用域然后把项目打成War包放到外部容器的webapps目录。这不属于“替换内嵌Tomcat版本”的范畴。外部容器的版本由运维或你自己在目标服务器上安装的Tomcat决定跟项目里的依赖管理没有直接关系。很多人问“我改了pom.xml里的tomcat.version怎么部署到外部Tomcat上版本还是不对”本质上是把两种机制搞混了。内嵌容器看的是项目依赖外置容器看的是服务器上的软件。这篇文章讨论的是前者。4. 替换之后怎么确认真的换了改完版本号跑起来就完事不一定要验证。很多人死于“感觉应该生效了”。验证内嵌Tomcat版本有没有替换成功我常用三个手段从快到慢依次排。第一个构建工具依赖树。Maven执行mvn dependency:tree -Dincludesorg.apache.tomcat.embedGradle执行./gradlew dependencyInsight --dependency tomcat-embed-core。如果输出的版本号不是你指定的那个立刻就能发现不用等启动报错。第二个直接看打出来的包。Spring Boot的可执行Jar里BOOT-INF/lib目录下的jar文件名直接体现版本。用jar tf your-app.jar | grep tomcat-embed就能看到文件名里的版本号。这个方法在生产环境最实用不需要本地代码也不需要登录开发机到一个故障服务器上就能查。第三个运行期通过接口拿Tomcat的声明版本信息。这个方法适合做自动化验证比如在巡查平台或监控脚本里定期检查运行中的服务到底用的什么Tomcat版本。Tomcat本身提供了一个类org.apache.catalina.util.ServerInfo在应用里加一个临时接口就能拿到import org.apache.catalina.util.ServerInfo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; RestController public class VersionController { GetMapping(/tomcat-version) public String tomcatVersion() { return ServerInfo.getServerInfo(); } }启动后访问/tomcat-version返回的内容类似Apache Tomcat/9.0.76。这个方法对内部包同样有效因为tomcat-embed-core里就有这个类。不过注意测试完记得删掉这个临时接口免得生产环境暴露版本信息带来安全风险。这里额外提一句Spring Boot启动日志里通常不会直接打印Tomcat的小版本号它只打印“Tomcat started on port(s)”这类运行时信息。如果你盯着日志想确认真实版本很容易看花眼。想要一次确认看依赖树或看jar包名最稳。5. 翻车现场常见问题和排查思路这一节我整理了实际工作中最常遇到的几个坑每一个都是真实经历过的。5.1 版本号改了启动后还是老版本这是最常见的问题。原因一般有三个。第一个Maven或Gradle没有重新加载依赖尤其是IDEA环境下改完pom.xml没有刷新构建工具还拿的是缓存的旧依赖树解决办法是手动触发Reload All Maven Projects然后执行mvn clean。第二个项目没有继承spring-boot-starter-parent像我上面说的直接加tomcat.version属性不生效需要走dependencyManagement手动声明。第三个IDE的缓存问题改了版本号但IDE的运行配置里还缓存了旧classpath重启IDE基本可以解决。5.2 启动报错NoSuchMethodError / ClassNotFoundException这类错误十有八九是版本跨度太大或者Tomcat版本和Spring Boot版本之间兼容性出了问题。最典型的案例是在Spring Boot 2.x项目里强行把Tomcat替换成10.1.x版本。Spring Boot 2.x系列默认的内嵌Tomcat版本是9.0.xAPI基于javax.servlet规范。而从Tomcat 10开始Servlet规范换成了jakarta.servlet包名。你把Tomcat 10塞进Spring Boot 2.x项目里应用代码和Tomcat之间直接对不上一运行就报类找不到或者方法找不到。所以替换版本不是单纯改个数字那么简单。Spring Boot 2.x项目选Tomcat版本请老实待在与9.0.x兼容的范围内最多在同大版本的小版本之间切换。Spring Boot 3.x项目默认使用Tomcat 10.1.x你也没必要为了一个小版本号把Tomcat降到9.x。跨越Servlet规范边界的替换属于“原理上就不该这么干”的操作别硬来。5.3 业务代码里的Tomcat特有配置失效有些项目在代码里对Tomcat做了一些定制配置比如通过TomcatServletWebServerFactory自定义连接器参数、配置静态资源处理器、调整RelaxedPathChars等。换了Tomcat版本之后这个工厂类里用到的一些内部API可能在目标版本里已经被改掉或废弃。遇到这种情况优先去看你用的Spring Boot版本对应的自动配置逻辑。很多时候你不需要直接依赖Tomcat内部API调整配置项就能达到目的。比如连接器参数、线程池线程数、acceptCount这些Spring Boot本身提供了server.tomcat.*系列配置项通过application.yml就能控制根本不用写代码。如果确实需要代码定制那就要根据目标版本来调整调用的方法名和参数这个没有捷径只能看对应版本的API文档。5.4 升级小版本之后某些请求行为悄悄变了这种情况特别隐蔽。Tomcat 9.0.x的小版本升级很多默认行为会随着漏洞修复或规范完善而调整。比如useRelativeRedirects行为、maxPostSize默认值、relaxedQueryChars的处理方式小版本之间确实存在差异。遇到这种情况先复盘一下你升级前后的Tomcat版本中间隔了多少个小版本。如果间隔很多建议仔细阅读Tomcat官方的迁移指南和ChangeLog重点看哪些行为相关的修正会影响你的业务。特别是那些以前能正常访问、升级后突然报400或404的请求大概率是版本行为差异导致的。把关键配置项在application.yml里显式设置成需要的值能有效减少这类问题的规模。5.5tomcat.version写到了别的地方看起来像低级错误但确实发生过。有人在profiles里定义了tomcat.version有人写在了build插件参数里还有人用环境变量硬编码。这些写法有的能生效有的不能反而容易迷惑后来接手的人。建议规则就是一句话Maven项目放propertiesGradle项目放ext里别搞其他花活统一且好维护。5.6 IDE为啥一直用旧依赖这个问题说起来简单排查起来却能耗掉大量时间。IDEA里改了pom.xml但Maven面板显示的依赖树还是旧的。原因通常是IDEA的“自动导入”被关掉了或者Maven的reimport没有触发。此外你本地.m2仓库里的旧版本jar如果被标记为快照也可能出现重复解析。解决办法是刷新Maven项目执行mvn clean必要时删除target目录。Gradle项目的话去~/.gradle/caches看看对应版本的jar是不是被缓存了刷新依赖缓存一般就能解决。5.7 排除内嵌Tomcat却留下了残渣有人想用排除依赖的方式彻底去掉内嵌Tomcat再单独引入指定版本。这个思路可行但操作时必须把tomcat-embed-core、tomcat-embed-el、tomcat-embed-websocket这几个组件都处理干净。我见过有人只排除了tomcat-embed-core结果tomcat-embed-el还在最终运行的时候还是出现版本混杂的诡异错误。如果不是因为外置部署建议优先用tomcat.version属性而不是手动排除依赖能省下不少麻烦。6. 基于实际经验的操作清单和一点心得最后再整理一份可以直接照抄的操作清单帮助你按步骤完成替换少走弯路。第一步确认项目形态。先搞清楚是Maven项目还是Gradle项目有没有继承spring-boot-starter-parent有没有在dependencyManagement里导入spring-boot-dependencies。这一步决定你用哪种方式替换版本。第二步查找当前版本。用依赖树或解压产物jar的方式确认当前内嵌Tomcat的真实版本号记录备用。第三步确认目标版本兼容性。结合Spring Boot主版本来判断Spring Boot 2.x对应Tomcat 9.0.xjavax规范Spring Boot 3.x对应Tomcat 10.1.xjakarta规范。如果跨大版本指定大概率会翻车。第四步按项目类型执行替换。Maven且继承父POM的项目在properties中添加tomcat.versionGradle项目在ext中设置既没有继承父POM又不想改父级的项目在dependencyManagement里显式声明所有Tomcat组件的版本。第五步修改后重新加载依赖。Maven执行mvn clean并刷新IDEA Maven面板Gradle执行./gradlew clean build。第六步用依赖树或解压Jar确认版本。启动前确认版本已经变更不要在启动后才去排查。第七步启动应用。关注启动日志和运行期行为重点留意有没有NoSuchMethodError、ClassNotFoundException以及之前的Tomcat定制配置是否仍然生效。第八步回归测试。这一步最容易忽略。很多替换版本的人跑通了启动流程就直接收工但某些依赖Tomcat内部API的代码路径要等特定请求进来才会触发启动时不一定会暴露问题。建议把涉及文件上传、WebSocket、JSP渲染、会话管理这些和容器强相关的功能都过一遍。就我个人的实际经验来说替换内嵌Tomcat版本这件事本身只有一行代码的工程量真正的成本全在于版本兼容性判断和问题排查。只要遵循“先确认兼容性、再改属性、最后验证依赖树”这个流程绝大多数问题都能在你还没碰到线上事故之前就被暴露出来。另外强烈建议在做这种容器级变更时留一条自动化验证的路径比如上面提到的ServerInfo接口、或者构建脚本里的依赖版本断言。哪怕只是几行简单的脚本也能在未来团队里有人再次改动这个项目的时候帮他快速确认当前状态而不是靠肉眼和记忆。
返回列表