ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot 3.4升级踩坑指南:版本、配置与自动装配实战

Spring Boot 3.4升级踩坑指南:版本、配置与自动装配实战 上个季度我把一个攒了两年业务代码的内部中台从 Spring Boot 3.2.5 一次性升到 3.4.1过程称不上丝滑。启动倒是能启动但测试挂了三十多个两块配置完全没生效其中一块还导致测试环境连错了数据库。事后复盘3.4.x 表面上是 3.3.x 的下一个“小版本”实际上底层框架、依赖管理和配置绑定都动了刀子照着老经验升级就是在给自己埋坑。这篇算是我自己的踩坑清单后面遇到新问题会持续补录。如果你也正准备把手里的老项目迁到 SpringBoot 3.4.x或者已经在升级路上被各种报错折磨这几个模块的经验应该能帮你省下不少时间。1. 升级前的版本摸底先别急着改代码1.1 Spring Boot 3.4.x 的版本链路我见过太多人升级第一步就是直接把spring-boot-starter-parent的版本号从 3.3.x 改成 3.4.x然后mvn clean package报错就懵了。真正稳妥的做法是先搞清楚 3.4.x 带来的一整条底层层级变化。Spring Boot 3.4.x 走的是 Spring Framework 6.2.x最低要求 Java 17。如果你项目之前还在用 Java 11那这步就不是改版本号的问题了整个编译环境、CI 镜像、线上 JDK 都得跟着动。我用的是 Java 21升级之后除了几个依赖需要同步编译层面没有明显问题。但是版本链路不仅仅是 Java 和 Spring Framework。Spring Boot 作为一套自动配置框架它本身不直接实现功能而是把 Tomcat、Jackson、Hibernate、Micrometer 等一大堆第三方库的版本全部拉在一起管理。升级到 3.4.x 后这些间接依赖的版本也会整体往前跳一大截。比如嵌入式 Tomcat 不再是老的 10.1.x 早期版本Jackson 也可能跟着升了大版本。老项目里如果用了某些针对特定库版本的“偏方”这时候就很容易炸。所以升级前的第一件事不是改代码而是跑一次完整的依赖树梳理把骨架摸清楚。我的做法是这样的mvn clean verify -DskipTests mvn dependency:tree -Dincludesorg.springframework.* mvn dependency:tree -Dincludesorg.apache.tomcat.*第一条命令可能跑不过因为编译阶段就会暴露一部分问题没关系先看报错。后面两条是为了了解 Spring 和 Tomcat 的实际解析版本确认没有发生依赖冲突。1.2 Parent 与 BOM 的两种管理差异项目里如果直接用了spring-boot-starter-parent升级是最省事的只要改一个版本号插件版本、依赖管理全部跟着走parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.1/version relativePath/ /parent但还有一种项目为了统一管理多个子模块没有用 parent而是通过dependencyManagement导入spring-boot-dependencies这个 BOM。这种写法升级的时候容易漏掉插件版本比如spring-boot-maven-plugin还停在 3.3.x结果打出来的 jar 包没有主类清单部署时直接报 “no main manifest attribute”。如果你用的是 BOM 方式升级时记得把三样东西一起查一遍spring-boot-dependencies的版本、spring-boot-maven-plugin的版本、以及maven.compiler.release目标。不能只改第一个否则 Spring Boot 的类是新版插件打包逻辑还是老的很容易出现一些看起来毫无关联的诡异问题。1.3 第三方 starter 的版本声明也要同步看很多公司内部会维护一个统一的父 POM里面声明了 MyBatis、Flyway、Spring Cloud 等所有第三方依赖的版本。Spring Boot 3.4.x 升级后这些组件不能继续用旧版本硬撑。原因很简单Spring Boot 的自动配置类会直接引用第三方库里的类如果第三方库版本太旧类里的方法签名对不上就会出现NoSuchMethodError或ClassNotFoundException。我这次最先翻车的是 MyBatis Starter当时项目里还锁着 3.0.3启动时一直报一个和SqlSessionFactory有关的循环依赖错误。查了半天才发现不是代码问题是版本匹配问题。后面把 MyBatis Starter 也升到对应支持 Boot 3.4 的新版本后错误直接消失了。这里给个建议升级前先列一张项目里的关键依赖清单逐个去 Maven 仓库或组件官网确认是否已经声明支持 Spring Boot 3.4。没适配的先不要贸然升级否则你会在“代码问题”和“版本问题”之间反复横跳非常浪费时间。2. 配置项静默失效启动不报错线上行为却变了2.1 现象配置读了但是没生效升级过程中最让人难受的一类问题不是启动失败而是启动一切正常业务行为却变了。我这边遇到过一个典型场景项目里某个模块依赖了一段自定义的ConfigurationProperties前缀是myapp.cache配置在application.yml里写得好好的。升级到 3.4.1 之后这段配置里的值全部变成了 null导致缓存连接不上。为什么启动没报错因为 Spring Boot 对配置的绑定是“宽松绑定”很多情况下找不到匹配的属性时它不会抛异常只会把字段留成默认值。如果你的代码里没有做空值校验这个隐患就会一直埋伏到线上才爆发。关于这一点Spring Boot 3.4 对配置元数据生成的规则更严了加上 IDE 对配置提示的判定也变了。以前在 IDEA 里写配置项时打个字母就有补全提示升级后如果配置项没有被元数据正确识别IDEA 会直接划黄线。很多同事不太在意这个黄线结果就是配置没绑定上页面还看不出异常。2.2 Configuration Properties 的正确打开方式我这边把自定义配置类重新调整了一下改成不可变风格的构造器绑定优先推荐用 record 或者带构造方法的类ConfigurationProperties(prefix myapp.cache) public record CacheProperties( String host, int port, Duration timeout ) { }配合使用Configuration EnableConfigurationProperties(CacheProperties.class) public class CacheConfig { Bean public CacheClient cacheClient(CacheProperties props) { return new CacheClient(props.host(), props.port(), props.timeout()); } }注意两点。第一Duration类型的属性配置里不要写纯数字Spring Boot 对时长解析要求带单位推荐写成3s、500ms这种格式。第二如果你的项目里把多个配置类放在不同包里不要同时混用ConfigurationPropertiesScan和EnableConfigurationProperties否则升级后可能出现部分配置绑定成功、部分没绑定的情况。统一用一种方式我这边后来全部改成ConfigurationPropertiesScan扫描根包反而稳定很多。2.3 用 Actuator 做配置漂移对比处理这类“配置没生效但又不报错”的问题最有效的办法不是盯日志而是做配置快照对比。Spring Boot 升级前先导出一次配置的完整状态升级后再导一次然后用 diff 工具对比所有值的差异一目了然。开启 Actuator 后配置属性可以通过接口暴露management.endpoints.web.exposure.includeconfigprops,conditions,beans,env然后导出快照curl -s localhost:8080/actuator/configprops | jq . configprops-before.json curl -s localhost:8080/actuator/configprops | jq . configprops-after.jsonconfigprops返回的是所有ConfigurationProperties类当前绑定的实际值。对比的时候重点看“我配置了值但绑定结果还是默认值”的那些 key。这种方法比人肉看配置文档高效得多尤其适合那种几十个模块、上百个配置项的中大型项目。2.4 启动时打开 Debug确认条件评估结果另外升级后第一次启动时建议带上--debug参数让 Spring Boot 打印完整的自动配置报告。这个报告里有一项叫CONDITIONS EVALUATION REPORT会列出每一个自动配置类匹配成功还是不匹配以及为什么不匹配。很多“配置没生效”的根因就在这里比如某个匹配条件因为新版本的前缀规则调整失效了自动配置类直接跳过了你后面配置再多也没用。mvn spring-boot:run -Dspring-boot.run.arguments--debug日志会比较多建议直接重定向到文件里再搜索 Positive matches 和 Negative matches。这个动作不需要每次都做但升级后的第一次启动强烈建议全程开着。3. 自动装配与条件注解自定义 Starter 在 3.4 下的兼容性调整3.1 自定义自动配置类的注册方式没变但顺序敏感了很多团队内部都有自己的公共 Starter比如统一日志、统一数据源、统一 Redis 操作封装。升级 Spring Boot 后最容易出的问题集中在自动装配的注册文件里。Spring Boot 3.4 沿用了 3.0 以来的自动配置注册机制文件路径是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports如果你是从 Spring Boot 2.x 一路升上来的老项目一定要检查是否还在用老掉牙的spring.factories文件注册自动配置。Spring Boot 3.4 对spring.factories里的自动配置声明已经不再支持了留着这个文件不会生效而且容易被忽略。我这里踩到的坑是更隐蔽的自动配置类的执行顺序变了。项目里有一个MetricsDataSourceConfig希望接管数据源给业务埋点提供单独的数据连接。升级之前这段配置一直工作正常但升级到 3.4.1 后它创建的数据源被后面执行的DataSourceAutoConfiguration覆盖了导致 MyBatis 连到了错误地址。3.2 用 AutoConfiguration 的 before/after 显式排顺序问题根因是自动配置类的加载顺序没有显式声明。老版本里可能因为类名排序或某个依赖的消失碰巧顺序是对的。新版本调整了部分自动配置的排序逻辑隐藏问题就暴露了。解决办法是不要依赖运气显式声明顺序AutoConfiguration( before DataSourceAutoConfiguration.class ) ConditionalOnClass(DataSource.class) ConditionalOnProperty(prefix metrics.datasource, name enabled, havingValue true) public class MetricsDataSourceAutoConfiguration { Bean ConditionalOnMissingBean public DataSource metricsDataSource() { // 创建独立的埋点数据源 } }这里的关键点是before DataSourceAutoConfiguration.class。声明之后Spring Boot 会优先执行这个配置类再执行数据源自动配置并且在执行后面的自动配置时因为已经存在自定义的DataSourceBeanConditionalOnMissingBean就会让内置自动配置退出不至于覆盖。3.3 条件注解的失效场景要单独排查另一个容易踩的是ConditionalOnClass的误判。Spring Boot 评估这个条件时看的是Class.forName能否成功而不是“代码里是否真的用到”。如果你在Bean方法里返回某个自定义类型但方法签名里没有直接引用它而仅仅是在方法内部new了一个那ConditionalOnClass的评估时机和类加载时机可能不同步就会出现“条件匹配了但 Bean 创建失败”的怪事。我的经验是自定义 Starter 里的条件注解尽量保持简单能不用条件注解就不用能用ConditionalOnProperty控制开关的就别用复杂的类条件。尤其如果你在 Starter 里大量使用ConditionalOnMissingBean升级后一定要逐个核对顺序因为这个注解只有在自动配置类的加载顺序确定时才有意义顺序一旦变化结果会完全相反。给个排查思路启动后查看actuator/conditions接口找到对应的自动配置类看 Positive 和 Negative 的具体原因。如果它的顺序和你预期不一致优先调整AutoConfiguration里的before和after而不是在代码里加一些奇怪的DependsOn去强行约束。4. 虚拟线程、TaskScheduler 与并发行为定时任务先踩雷4.1 开启虚拟线程后Tomcat 行为变了Spring Boot 3.2 开始支持虚拟线程3.4.x 里配置方式还是一样启用路径在配置文件里spring: threads: virtual: enabled: true升级之后如果你把这个开关打开了需要知道它到底改变了什么。它会让嵌入式 Tomcat 在接受请求时使用虚拟线程承载任务而不是传统的平台线程池。对于 IO 密集型应用这通常能大幅提升并发能力而且线程资源占用更少。听起来是好事但虚拟线程不是银弹。我遇到的问题是一个定时任务大量调外部接口开启虚拟线程后并发任务数量迅速上升数据库连接池HikariCP的maximum-pool-size只有 10结果大批任务在等待连接时全部阻塞接口响应时间直接打满。这其实不是虚拟线程本身的问题而是虚拟线程太“轻量”了让应用能同时跑更多任务反而把下游资源瓶颈暴露出来。处理方式也很直接监控连接池使用率把连接池调大或者给不同业务场景拆分独立的连接池。4.2 Scheduled 默认还是单线程调度器另一个非常容易忽略的坑是Scheduled定时任务的调度器。很多人以为开了虚拟线程定时任务也会自动变成“多线程并发”。大错特错。Spring Boot 的Scheduled默认使用的TaskScheduler依然是单线程的所有定时任务排在一个调度线程里按顺序跑。如果某一个任务执行时间特别长比如在一个定时任务里写了Thread.sleep(60000)或者调用了一个超时时间为 30 秒的远程接口它会把后面所有定时任务全部堵住。升级到 3.4 后这个问题不会自动缓解需要在配置里显式指定调度线程池大小spring: task: scheduling: pool: size: 8我这边之前生产环境就出现过类似情况每天凌晨的数据同步任务延长到了两三分钟结果后面的用户离线积分结算任务被推迟了十几分钟用户端看到的就是积分迟迟不到账。排查到最后发现调度线程池大小还是默认的 1。4.3 自定义 TaskExecutor 时别覆盖默认 Bean如果你在建项目时为了处理异步任务自定义了一个TaskExecutorBean升级到 3.4 后要留意自定义 Bean 可能会覆盖 Spring Boot 内部的默认异步执行器从而影响Async注解的行为。我第一次开虚拟线程时为了最大化利用虚拟线程专门加了一个配置Bean public AsyncTaskExecutor applicationTaskExecutor() { return new VirtualThreadTaskExecutor(virtual-); }然后发现所有Async方法都切到了虚拟线程上跑。本身这件事没错但虚拟线程和数据库连接池、以及一些依赖ThreadLocal的框架配合时会产生一些隐蔽问题比如 ThreadLocal 在线程复用的假设在虚拟线程场景下不成立部分上下文信息丢失。我的建议是虚拟线程开启后分模块验证。先把不依赖线程上下文、纯 IO 密集型的业务切过去等跑稳了再逐步放宽。不要一把梭全项目开启尤其是你依赖了类似InheritableThreadLocal、Runnable包装上下文的功能时必须单独做兼容处理。5. 第三方组件适配实录从 MyBatis 到 Flyway 的版本匹配5.1 MyBatis Starter循环依赖与元数据校验错误Spring Boot 3.4 升级后MyBatis 相关的报错非常典型。我这边遇到的是启动时报循环依赖错误堆栈指向SqlSessionFactory和DataSource之间形成了 A - B - A 的循环。第一反应是代码里有构造器注入循环但仔细检查后业务代码完全没动问题出在 MyBatis Starter 的版本。MyBatis Spring Boot Starter 需要和 Spring Boot 3.4 的自动配置机制兼容我这边当时用的版本太老它内部依赖的mybatis-spring还是基于旧版 Spring 反射机制编译的升级后自动装配顺序变化循环依赖就被触发。解决办法是升级对应组件的版本并同步检查mybatis-spring的版本。如果升级后依然有问题还有一个兜底方案暂时禁用 MyBatis 的自动配置自己手写一个Configuration类来创建SqlSessionFactory。这种方式不优雅但能让你在等待组件适配期间不被卡住。spring: autoconfigure: exclude: org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration注意这只是临时短接的手段不要长期留在代码里。组件官方发布适配版本后第一件事就是把这个 exclude 移除回到正常自动配置。5.2 Flyway脚本校验和数据库兼容性双双出问题另一个让我头疼的是 Flyway。Spring Boot 的依赖管理会固定 Flyway 的主版本从 3.3 升到 3.4Flyway 也跟着升了一个大版本区间。Flyway 的版本策略比较特殊主版本之间对数据库类型的支持会有增删社区版和商业版的边界也经常变化。我遇到的问题是项目使用的国产数据库驱动在 Flyway 新版本里不再被社区版支持导致启动时数据库迁移直接失败。而且这个问题不是简单切换配置就能解决需要锁定 Flyway 版本、数据库驱动类型、以及 Spring Boot 的依赖管理三者之间的兼容关系。这种场景下最稳妥的方式是在项目中显式声明 Flyway 的版本覆盖 Spring Boot 的默认固定值直到数据库驱动和 Flyway 新版本之间的兼容问题解决。但这个过程要谨慎Cover 版本后要以整套迁移脚本能正常执行为准千万不要认为“版本号不动就行”。5.3 Spring Cloud 必须看官方版本矩阵如果你的项目用了 Spring Cloud 组件比如 OpenFeign、Gateway那升级到 Spring Boot 3.4 之前必须先确认 Spring Cloud 的版本。Spring Cloud 和 Spring Boot 是有严格版本对齐关系的并不存在“任意 Spring Cloud 版本都兼容任意 Boot 版本”的美好幻想。我之前按老经验继续用旧的 Spring Cloud Release Train启动时出现NoSuchMethodError堆栈指向spring-cloud-commons。这个错误非常典型升级 Boot 会导致 Spring Cloud 内部调用的 Spring 类可能变了签名老版本 Spring Cloud 编译时的类路径和新版本不一致。解决办法只有一个去 Spring Cloud 官方文档查版本矩阵找到对应支持 Spring Boot 3.4 的 Spring Cloud Release Train然后整体升级。不要试图只换一个组件版本因为 Spring Cloud 内部组件之间的版本在任何 Release Train 里都已经绑定好了。手动拆开升级很容易按下葫芦浮起瓢。6. 测试与本地部署升级后最先暴露问题的环节6.1 测试上下文加载失败先查循环依赖配置升级后我第一次跑测试挂掉的几十个用例里有相当一部分是上下文加载失败报错信息里出现了循环依赖。翻代码时发现项目里其实是存在循环依赖的但在老版本里可能被“代码恰好先加载了某个 Bean”掩盖掉了或者项目曾经在配置里临时允许过循环引用。Spring Boot 对循环依赖是默认禁止的。如果项目里存在两个 Bean 互相注入而且你没有在配置里显式开启允许循环引用新版本启动测试上下文时会直接报错。处理方式有两个二选一要么彻底把循环依赖拆掉要么在配置里显式写spring: main: allow-circular-references: true我的观点是循环依赖能拆就拆。Spring Boot 3.4 的构造器注入比之前更敏感很多以前靠字段注入绕过去的循环引用升级后会被直接揪出来。趁升级的机会把这类代码重构掉比留着以后每次排查都痛苦要值。6.2 老测试注解还能用新代码建议用 MockitoBeanSpring 团队在新版测试框架里把 Mockito 相关的注解做了迁移原来基于MockBean和SpyBean的写法在新代码里建议逐步换成以MockitoBean和MockitoSpyBean为代表的新注解。这个变化在 Spring Framework 6.2 里已经出现Spring Boot 3.4 也继承了这一方向。不是说MockBean完全不能用旧代码不急着全量迁移但如果你的项目刚好升级顺手在新写的测试用例里用新注解未来可以少一些重构成本。这个改动很小主要是换一个注解名控制 Bean 的行为的方式基本一致迁移成本很低。6.3 测试配置加载顺序optional 前缀不能忘升级过程中另一个常见问题是测试环境的配置文件加载失败。如果你在测试目录下写了application-test.yml并且通过spring.profiles.activetest激活同时项目里又用到了spring.config.import要注意导入的外部配置文件在测试环境是否真的存在。Spring Boot 的spring.config.import加载失败时整个上下文会直接报错。升级之前可能因为没有检查到这条路径隐藏过去了升级之后配置处理更严格问题就暴露出来。处理方式很简单如果某个外部配置是“可选的”一定要加optional:前缀例如spring: config: import: optional:classpath:external-common.yml这个细节很容易被忽略但一旦踩到它的排错成本很高因为报错信息往往不会直接告诉你“某个外部配置文件找不到”而是报一些后面环节才出现的派生错误。先说结论外部配置一律加optional:除非你确定它必须存在。6.4 本地部署嵌入式容器参数语义有调整本地开发环境里升级后最直观的变化是嵌入式容器的一些参数语义不一样了。比如连接数、请求头大小、优雅停机等配置项在新版里可能改了默认值或调整了限制范围。最典型的是请求头默认大小以前能跑过的请求头升级后可能直接被拒绝。我的建议是升级后把server.*开头的所有配置项全部检查一遍重点是看有没有被标记为废弃的配置有没有默认值变化的配置。这个可以直接参考上一节说的 Actuatorenv接口对比升级前后环境中实际生效的值不要只看配置文件因为默认值变化不会显示在你的配置里。另外如果你打的是 WAR 包要部署到外部 Tomcat记得检查spring-boot-starter-tomcat在 POM 里是否标注了provided。Spring Boot 3.4 的内嵌容器版本变了如果不标provided打出的 WAR 里会带着旧的容器类和外部容器冲突后会出现各种奇怪的ClassCastException。这个问题在本地用java -jar测不出来只有部署到外部容器才暴露。7. 我处理 3.4 踩坑的固定排查顺序7.1 先编译再启动最后再补依赖有了这一轮踩坑经验我现在面对 Spring Boot 升级问题基本有一套固定的处理顺序。第一步是先跑mvn clean verify -DskipTests把编译层面的依赖冲突全部清完。编译不过的不用猜几乎都是依赖版本问题。第二步是把所有测试先跑一遍这能快速暴露上下文加载问题。第三步才启动应用并且第一次启动必须带--debug把完整的自动配置报告留下来。这步做完大部分问题才能定位到具体模块而不是像无头苍蝇一样到处查。7.2 用 Actuator 接口做升级前后对比我强烈建议把 Actuator 的几个接口用起来特别是conditions和configprops。升级时做一次前后对比能精准锁定三类问题自动配置没生效、配置值被覆盖、Bean 创建顺序异常。我日常排查时的命令大概是这样的# 导出升级前的状态 curl -s localhost:8080/actuator/configprops | jq . configprops-before.json curl -s localhost:8080/actuator/conditions | jq . conditions-before.json # 升级后再次导出 curl -s localhost:8080/actuator/configprops | jq . configprops-after.json curl -s localhost:8080/actuator/conditions | jq . conditions-after.json # 对比 diff configprops-before.json configprops-after.json diff conditions-before.json conditions-after.json如果项目里的 Actuator 端口没有对外暴露只在本地跑也行。重点是“对比”这个过程它能帮你从大量“看起来正常”的信息中快速找出真正变化的点。7.3 明确哪些坑属于版本问题哪些属于代码问题升级踩坑最耗时间的部分是区分“版本问题”和“代码问题”。我的判断标准是如果代码完全没改、配置完全没改升级后行为却变了优先怀疑版本问题。比如第三方库方法签名变了、自动配置顺序变了、默认值变了。只有当代码和配置都动了才需要优先检查业务逻辑。这个判断标准帮我省了非常多时间。很多同事升级时一看到报错就去翻业务代码结果业务代码根本没动过浪费半天精力最后还是回到依赖版本上。经历了这一轮升级我现在的习惯变成升级前先跑一次依赖树把间接依赖版本全搞清楚启动时永远开着--debug每次处理完一个坑在团队 wiki 里留一条记录。踩坑不可怕最烦的是同一个坑被全组人轮流踩一遍。这篇清单我会继续更新后面再遇到新的怪问题再回来补上。
返回列表