1. 项目缘起为什么需要修改Spring Boot内置的Tomcat版本在Spring Boot项目里Tomcat就像你买精装房时开发商预装好的那套卫浴。开箱即用省心省力绝大多数情况下它都能完美工作。但干过几年开发的同行肯定都遇到过这样的场景项目要上线了运维团队跑过来说“咱们生产环境统一用的是Tomcat 9.0.80你们这个Spring Boot 2.7.x默认带的是9.0.68版本不一致有安全策略和性能调优参数对不上得统一一下。” 或者你在集成某个第三方SDK时它明确要求Tomcat版本必须≥9.0.70否则会有类加载或Servlet API兼容性问题。这时候你就不得不面对“修改内置Tomcat版本”这个看似基础实则藏着不少细节的操作。我最初接触这个问题是在一个老项目升级Spring Boot 2.3到2.7的过程中。新版本默认的Tomcat带来了更好的HTTP/2支持和线程池管理但同时也引入了一个老项目里某个Filter不兼容的改动。直接升级Spring Boot大版本风险太高退而求其次只升级其内置的Tomcat小版本就成了一个更稳妥的“手术式”解决方案。这个需求背后其实反映了Spring Boot“约定优于配置”哲学的另一面当“约定”不符合你的“配置”这里指环境或特定需求时你必须有清晰、可靠的手段去覆盖它。从技术角度看Spring Boot通过spring-boot-starter-web依赖将特定版本的Tomcat作为“传递性依赖”打包进来。修改它本质上就是告诉Maven或Gradle“别用你默认带来的那个用我指定的这个。” 听起来简单但实操中版本号怎么写、依赖怎么排除、不同构建工具如何处理、升级后如何验证每一步都有讲究。网上教程很多但往往只给命令不说原理更不提那些只有踩过坑才知道的“副作用”。今天我就结合多次实战经验把这套操作掰开揉碎了讲清楚让你不仅能“做到”更能“懂得”。2. 核心原理Spring Boot是如何管理Tomcat依赖的要修改内置组件首先得知道它从哪来、怎么被管理的。我们直接从项目的依赖树看起。创建一个最简单的Spring Boot Web项目它的pom.xml里通常会有这么一段dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency这个starter-web就像一个“礼包”它自己并不包含代码而是声明了一组依赖。通过Maven的mvn dependency:tree命令我们可以清晰地看到这个礼包拆开后的样子[INFO] com.example:demo:jar:0.0.1-SNAPSHOT [INFO] - org.springframework.boot:spring-boot-starter-web:jar:2.7.18:compile [INFO] | - org.springframework.boot:spring-boot-starter:jar:2.7.18:compile [INFO] | - org.springframework.boot:spring-boot-starter-json:jar:2.7.18:compile [INFO] | - org.springframework:spring-webmvc:jar:5.3.31:compile [INFO] | - org.springframework:spring-web:jar:5.3.31:compile [INFO] | \- org.springframework.boot:spring-boot-starter-tomcat:jar:2.7.18:compile [INFO] | - org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.68:compile [INFO] | - org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.68:compile [INFO] | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.68:compile关键点就在spring-boot-starter-tomcat这个子starter里。它定义了三个核心的嵌入式Tomcat组件tomcat-embed-core: Tomcat的核心引擎。tomcat-embed-el: 表达式语言支持。tomcat-embed-websocket: WebSocket支持。Spring Boot父工程spring-boot-dependencies的BOMBill Of Materials文件里已经为每个Spring Boot版本预定义好了这些Tomcat组件的版本号。例如在Spring Boot 2.7.18的BOM里你可能会找到类似tomcat.version9.0.68/tomcat.version的属性定义。这就是“约定”的来源。所以修改版本的核心思路就明确了我们需要覆盖Spring Boot父工程中预定义的Tomcat版本属性。这通常有两种主流做法属性覆盖推荐在项目的pom.xml中直接定义一个同名的属性如tomcat.versionMaven会优先使用项目中定义的值。依赖排除与显式引入先排除掉starter里传递过来的Tomcat依赖再手动引入指定版本的依赖。第一种方法更简洁是Spring Boot官方推荐的方式。第二种方法则更“暴力”和直接通常在需要引入Tomcat的某个特殊变体比如测试版或者遇到复杂的依赖冲突时使用。我们接下来会详细讲解第一种并补充说明第二种的应用场景。3. 实战操作基于Maven项目的版本修改全流程假设我们的需求是将Tomcat从Spring Boot 2.7.18默认的9.0.68升级到9.0.82。下面是一步一步的操作和背后的思考。3.1 第一步确认当前版本与目标版本动手前先知己知彼。运行mvn dependency:tree | findstr tomcat-embedWindows或mvn dependency:tree | grep tomcat-embedLinux/Mac确认当前真实的Tomcat版本。然后去Apache Tomcat官网或Maven中央仓库确认目标版本如9.0.82是稳定发布版并且与你当前的Spring Boot版本兼容。一个简单的兼容性判断原则是大版本号9必须一致小版本号0.82建议选择比Spring Boot内置版本更高但仍是稳定版的版本。Spring Boot社区通常不会在补丁版本中引入不兼容的Tomcat升级。3.2 第二步在pom.xml中覆盖版本属性这是最核心的一步。在你的项目pom.xml的properties标签内添加Tomcat版本属性。properties java.version11/java.version !-- 覆盖Spring Boot默认的Tomcat版本 -- tomcat.version9.0.82/tomcat.version /properties为什么在这里加properties是Maven定义变量的地方。当Spring Boot的父BOM定义了tomcat.version时你在子项目中重写它Maven在解析依赖时会使用你的定义。这比直接去dependencyManagement里写要简洁得多。添加后强烈建议立即刷新Maven项目在IDE中点击Maven刷新按钮或执行mvn clean compile -U。-U参数强制更新快照依赖能确保版本信息被正确拉取。3.3 第三步验证依赖变更刷新后再次执行mvn dependency:tree | findstr tomcat-embed。你应该能看到输出中的版本号全部变成了9.0.82[INFO] | - org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.82:compile [INFO] | - org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.82:compile [INFO] | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.82:compile这步验证至关重要它能确认你的配置生效了。我见过有同事改了属性但没刷新或者属性名拼写错误比如写成tomcat.version少了个点导致修改无效排查了半天。3.4 第四步启动测试与基础功能验证版本改好了不代表万事大吉。启动应用进行基础功能测试启动日志观察应用启动日志Tomcat初始化信息中应该会显示版本号例如Starting Servlet engine: [Apache Tomcat/9.0.82]。端点检查访问/actuator/env如果开启了Actuator或/actuator/info查看server.tomcat相关的信息确认运行时版本。基础请求写一个简单的RestController接口发送HTTP GET/POST请求确保基本的MVC功能正常。WebSocket测试如果你的项目用到了WebSocket务必进行连接测试因为tomcat-embed-websocket是独立组件。3.5 第五步处理可能出现的依赖冲突这是最容易踩坑的地方。升级了Tomcat可能会“拔出萝卜带出泥”引发其他依赖的版本冲突。常见的有Servlet APITomcat 9对应的是Servlet 4.0规范。如果你项目中其他地方比如某个古老的工具包直接引入了javax.servlet:servlet-api的旧版本如2.5可能会引发NoSuchMethodError或ClassNotFoundException。使用mvn dependency:tree查找所有servlet-api或javax.servlet相关的依赖通过exclusions排除掉旧的或者统一在dependencyManagement中管理版本。EL表达式语言实现tomcat-embed-el是Tomcat自带的EL实现。如果项目里还有org.glassfish:jakarta.el之类的其他EL实现也可能冲突。通常保留Tomcat自带的即可。一个排查依赖冲突的实用命令是mvn dependency:tree -Dverbose。它会显示所有依赖并标注出因为版本冲突而被忽略的依赖显示为omitted for conflict。重点关注这些被忽略的依赖看它们是不是必要的。4. 进阶场景与深度避坑指南上面的标准流程能解决90%的问题。但实际企业级项目更复杂下面这些场景你可能也会遇到。4.1 场景一需要降级Tomcat版本是的有时候不是升级而是降级。比如一个为Tomcat 9.0.60优化的本地缓存组件在9.0.68上出现了性能回退经过评估决定暂时回退到9.0.60。操作方法和升级一模一样只需将tomcat.version属性值设为9.0.60即可。但降级需要格外小心安全风险旧版本可能包含已知的安全漏洞。降级前必须经过安全团队评估并确认是否有其他补偿性安全措施。功能回退确认新版本中你依赖的某个特性或Bug修复在旧版本中不存在是否会影响业务。仔细阅读Tomcat的版本变更日志。Spring Boot兼容性极端情况下Spring Boot的某个自动配置可能依赖了高版本Tomcat的某个特定方法。降级后启动报NoSuchMethodError的概率比升级要大。务必进行全面测试。4.2 场景二使用dependencyManagement进行精细控制对于多模块项目或者项目中有其他库也传递依赖了Tomcat虽然不常见你可能需要在父POM或顶层POM的dependencyManagement节中显式声明Tomcat依赖的版本以确保所有模块统一。dependencyManagement dependencies dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-core/artifactId version9.0.82/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-el/artifactId version9.0.82/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-websocket/artifactId version9.0.82/version /dependency /dependencies /dependencyManagement注意这种方式需要声明所有三个embed组件比只覆盖一个tomcat.version属性要繁琐。它通常用于解决更复杂的、属性覆盖无法解决的依赖冲突。4.3 场景三排除默认依赖并引入指定版本“硬替换”这是最彻底但也最麻烦的方法。在spring-boot-starter-web依赖中排除掉Tomcat starter然后手动引入你想要的Tomcat依赖。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency !-- 手动引入指定版本的Tomcat -- dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-core/artifactId version9.0.82/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-el/artifactId version9.0.82/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-websocket/artifactId version9.0.82/version /dependency什么时候用这个方法当你需要引入的“Tomcat”根本不是官方的tomcat-embed而是某个深度定制的版本或者像网络热词中提到的“国产中间件宝蓝德”这类替换实现时这里仅作技术可能性探讨具体替换需参考对应中间件文档。此时属性覆盖和依赖管理都不起作用必须彻底排除默认实现引入新的jar包。4.4 重大陷阱版本号变量名并非一成不变这是一个超级大坑tomcat.version这个属性名并不是Spring Boot亘古不变的约定。在Spring Boot 3.x版本中由于Tomcat 10的包名从javax.servlet迁移到了jakarta.servlet为了区分Spring Boot 3.x的BOM中可能使用了不同的属性名例如tomcat-embed-core.version。如何确定正确的属性名最可靠的方法查看你所使用的Spring Boot版本对应的官方文档中关于“Build System”或“Dependency Versions”的部分。工程方法在IDE中按住Ctrl键点击你的spring-boot-starter-parent或spring-boot-dependencies的版本号跳转到它的POM文件然后搜索tomcat找到它实际定义的属性名是什么。如果你在Spring Boot 2.7.x的项目里错误地使用了tomcat-embed-core.version你的覆盖是不会生效的。这个细节很多教程都不会提但一旦出错排查起来非常耗时。5. 基于Gradle构建项目的修改策略虽然国内Maven仍是主流但使用Gradle的项目也越来越多。其核心逻辑相通但语法不同。在Gradle的build.gradle或build.gradle.kts文件中你需要通过覆盖ext中的属性或使用resolutionStrategy来修改版本。对于build.gradle(Groovy DSL)ext[tomcat.version] 9.0.82或者更现代的方式是直接在dependencies块中强制指定版本dependencies { implementation(org.springframework.boot:spring-boot-starter-web) { // 如果需要排除可以在这里排除 // exclude group: org.springframework.boot, module: spring-boot-starter-tomcat } // 显式引入指定版本Gradle的高级冲突解决策略会优先使用这个版本 implementation org.apache.tomcat.embed:tomcat-embed-core:9.0.82 implementation org.apache.tomcat.embed:tomcat-embed-el:9.0.82 implementation org.apache.tomcat.embed:tomcat-embed-websocket:9.0.82 }对于build.gradle.kts(Kotlin DSL)extra[tomcat.version] 9.0.82或者dependencies { implementation(org.springframework.boot:spring-boot-starter-web) // 强制所有配置中的Tomcat组件使用指定版本 implementation(org.apache.tomcat.embed:tomcat-embed-core) { version { strictly(9.0.82) } } }Gradle的依赖解析机制比Maven更灵活也稍复杂。修改后运行./gradlew dependencies --configuration runtimeClasspath来查看最终的依赖树确认版本已变更。6. 修改后的全面验证清单版本修改完成并成功启动只是第一步。在生产环境部署前建议执行一个完整的验证清单我称之为“Tomcat版本变更健康检查”基础功能回归测试所有核心业务接口的CRUD操作。会话Session测试如果用了Tomcat的Session管理测试登录态保持、Session超时等。文件上传下载测试特别是大文件测试max-http-post-size等配置是否依然有效。SSL/TLS连接测试如果启用了HTTPS验证证书加载和握手过程。AJP连接器测试如果使用了AJP与前置代理如Nginx通信测试连通性。内存与线程监控通过JMX或/actuator/metrics端点观察线程池tomcat.threads、内存使用等指标与旧版本进行对比确保没有异常增长。压测对比在测试环境进行简单的压力测试如使用Apache Bench对比关键接口的QPS和平均响应时间确保性能没有劣化。查看日志级别确认logging.level.org.apache.tomcat的日志级别没有输出大量不必要的DEBUG或TRACE信息避免生产日志暴涨。7. 从修改版本到理解版本管理最佳实践思考经过上面这一通操作我们成功修改了Tomcat版本。但这件事带给我们的价值不应该止步于“会改”。更深层的收获是建立起对项目依赖管理的敏感度。版本锁定与可重复构建对于企业级项目建议在父POM中不仅锁定Tomcat版本而是通过dependencyManagement锁定所有核心组件的版本形成一份明确的“物料清单”确保任何人在任何时间构建出的产物都是一致的。关注版本差异每次升级或修改版本花10分钟阅读一下两个版本之间的官方Release Notes。重点看Bug Fixes和Security Fixes。这能帮你预判可能的风险并理解这次变更的价值。例如从9.0.68到9.0.82可能修复了某个特定的内存泄漏问题。建立升级流程将版本修改操作标准化。可以是一个简单的Checklist或一个内部Wiki页面记录着“改属性 - 刷新依赖 - 查依赖树 - 启动验证 - 功能测试 - 性能基线对比”的完整步骤避免后续人员重复踩坑。考虑使用Spring Boot的版本管理除非有强制的环境要求或特定的Bug规避否则尽量跟随Spring Boot官方推荐的Tomcat版本。Spring Boot团队在集成测试上投入巨大他们选择的版本组合通常是经过充分兼容性验证的。自行升级小版本是安全的但跨大版本如从Tomcat 9到10则需要同步升级Spring Boot大版本这属于系统性的升级工程。修改一个内置组件的版本就像给一台精密仪器更换一个指定型号的螺丝。你知道要换也知道怎么换但真正的功夫在于清楚这颗螺丝为什么必须是这个型号兼容性知道用什么工具能最顺手的换上Maven/Gradle换完之后如何测试仪器是否运转如初验证清单以及从这次更换中总结出保养整个仪器的方法论依赖管理实践。把这个过程想透、做熟你在应对其他类似问题比如处理Netty、Jetty、数据库驱动版本时就能举一反三游刃有余了。