升级 Spring Boot 3.4.x 的过程,我印象最深的一句话是:版本看着不大,坑倒是一个不少。当时我从 3.3.7 跳到 3.4.0,先是拿一个边缘服务试水,原以为替换坐标、改两个配置就能过,结果连启动都没撑过去。后来把踩坑过程整理进了内部文档,也就是今天这篇 SpringBoot 3.4.x 踩坑记录及解决方案。准备升级的朋友可以直接对照排查,已经升级完的也可以看看有没有漏掉的地方。这个系列我会持续更新,后续遇到问题会继续往里面补。
1. 升级前必须做的版本基线检查
1.1 先搞清楚 3.4.x 对运行时的要求
Spring Boot 3.4.x 的最低 Java 版本还是 17,但要注意它已经完整支持到 Java 23/24。我们自己团队内部大部分服务用的 Java 21,理论上没有迁移压力,但有个老服务还在 Java 17 上,升级后发现某些新特性用不了,才意识到问题不止是语法层面,还包括依赖里的一些字节码库对 JDK 版本的适配。
这里我的建议很直接:如果有条件,尽量统一 Java 21 再升 Spring Boot 3.4.x。Java 17 能用,但虚拟线程、结构化并发这些特性在 3.4.x 里配置起来更顺手,踩坑成本也更低。升级前最好先确认整个依赖树里有没有老旧的字节码操作库,比如 CGLIB、ASM、Byte Buddy,这些往往和 JDK 版本强相关,Spring Boot 版本升上去了,它们没跟上,启动阶段就各种诡异报错。
另外,Spring Framework 6.2 是 Spring Boot 3.4 的底层底座。如果项目代码里直接依赖了 Spring Framework 的 API,建议先粗查一遍有没有用到已经废弃或者签名变化的方法。我自己遇到过AntPathMatcher相关的用法,在 6.2 里默认行为更严格,结果接口路径匹配直接不对,排查了大半天,最后发现是通配符解析规则变了。这类问题在编译期不一定暴露,运行期才炸,特别难查。
1.2 用官方迁移器提前扫一遍配置属性
Spring Boot 每次大版本升级都会带来配置属性变更,3.4.x 也不例外。最实用的工具就是spring-boot-properties-migrator。在升级阶段把它临时加到依赖里,启动时它会自动对比旧配置和新配置,打印出哪些属性被移除、哪些被重命名,比如server.tomcat.basedir这类老写法它会明确提示新路径。
我实际使用中它的提示非常直接,例如:
The following properties are invalid: spring.datasource.continue-on-error要注意的是,这个依赖只是辅助工具,发现不合法配置后不会帮你自动改,而且它本身有性能开销,不要在线上长期保留。正确的做法是本地启动一次,收集所有迁移告警,统一改完配置后再把这个依赖从 pom/gradle 中摘掉。否则就算应用能启动,那些无效配置也会一直刷 warn 日志,影响排查真实问题。
1.3 我建议的升级顺序
千万别一次性从 3.2.x 直接跨到 3.4.x。我们内部有个服务就是这么干的,结果各种中间件兼容性问题叠加,完全分不清是哪个版本引入的。稳妥的路径是:
- 从当前版本先升到 3.3.x 最新补丁版,跑通全部测试用例。
- 跑完后把依赖版本也一起梳理一遍,确认没有明显的不兼容项。
- 再上 3.4.x,保持补丁版本最新,比如 3.4.1、3.4.2。
每次升级保留一个独立分支,记录具体改动点。这样一旦线上出问题,可以快速对比是版本升级带来的还是业务改动导致的。别嫌麻烦,多分支多验证,比事后返工省事太多。
2. 配置文件加载和配置绑定的几个坑
2.1 config.import 导致启动直接报错
Spring Boot 3.4.x 对spring.config.import的处理比 3.3.x 更严格。之前某些情况下 import 一个不存在的配置文件,可能只是警告或者被忽略,但在 3.4.x 里直接启动失败,报类似:
No such property: classpath:config/application-common.yml我们有个基础配置包,原先约定各服务通过spring.config.import引入公共配置。升级后发现只要路径写错或文件缺失,整个服务起不来。这不是坏事,反而能提前暴露配置依赖问题。但要注意一点:如果你之前依赖了“配置缺失也照常启动”的行为,升级后必须显式处理。
正确做法是把公共配置放到optional:前缀下,比如:
spring: config: import: optional:classpath:config/application-common.ymloptional:表示文件存在就加载,不存在也不阻断启动。我建议能明确配置来源的不要加 optional,避免环境差异导致配置黑洞;只有那些真正可选的辅助配置才用 optional。
2.2 @ConfigurationProperties 绑定失败,环境变量被“吞”
升级后另一个高频问题出现在@ConfigurationProperties上。3.4.x 对集合类型和 Map 类型的绑定规则有变化,尤其是 Map 嵌套 POJO 的情况。我们有个配置类是:
@ConfigurationProperties(prefix = "biz.rule") public class RuleProperties { private Map<String, RuleConfig> rules = new HashMap<>(); }配置里写的是:
biz.rule.rules[order].enabled=true biz.rule.rules[order].timeout=1000之前 3.3.x 能正常绑定,升级到 3.4.x 后 rules 一直为空。排查后发现问题出在环境变量转换环节:当通过环境变量覆盖配置时,BIZ_RULE_RULES_ORDER_ENABLED这种写法在新版本里的转换逻辑对下划线和方括号的处理不一致,结果没能正确映射到 Map key 上。
这里我的建议是:
- 配置绑定尽量用配置文件本身,少用环境变量覆盖复杂 Map 结构。
- 如果必须用环境变量,key 不要带特殊字符,用简单单词,避免歧义。
- 启动时加上
--debug或开启spring.main.log-startup-info=true,打印实际绑定结果,确认配置有没有生效。
最坑的是这类问题不报错,只是行为不符合预期,线上出了问题很难第一时间想到是配置没绑定上。
2.3 日志配置和结构化日志
Spring Boot 3.4 引入了结构化日志的新能力,默认不会开启,但很多日志相关的旧配置属性位置变了。比如logging.pattern.console在某些场景下的优先级和 3.3.x 不一样。我们有个服务把日志输出到 JSON,升级后发现部分字段没了,查了半天发现是 structured logging 的默认字段集和自定义 pattern 有覆盖关系。
如果只是普通文本日志,升级影响不大。但如果你用了 logstash-logback-encoder 这类第三方库,建议先检查它的版本是否兼容 logback 1.5.x。Spring Boot 3.4 默认管理的 logback 版本较高,老版本 encoder 会出现NoSuchMethodError。升级时尽量把 logstash-logback-encoder 更新到最新版,避免动态查表式的版本匹配。
3. 自动配置注册机制迁移
3.1 spring.factories 为什么失效
Spring Boot 2.7 开始就逐步废弃META-INF/spring.factories中的自动配置注册,到了 3.4.x 这个废弃基本变成了实际移除。如果你还在老的自定义 starter 里写:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.common.config.MyAutoConfiguration升级后会发现自动配置根本不生效,而且不报明显错误。这不是依赖没引进来,而是 Spring Boot 启动时根本不再扫描该路径。
解决办法是把自动配置类注册迁移到新文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports每一行写一个自动配置类的全限定名即可:
com.example.common.config.MyAutoConfiguration com.example.common.config.SecurityAutoConfiguration这个文件是 UTF-8 编码,换行符用 LF,别用 CRLF,否则某些平台下读取会出错。
我当时改完自己写的两个 starter 后,又发现团队里另一个基础组件还是老的注册方式,结果那个组件提供的ObjectMapper自定义配置一直没加载,导致接口返回的 JSON 格式和升级前不一致。这类影响非常隐蔽,建议升级时全局搜一下spring.factories和EnableAutoConfiguration关键字,全部检查一遍。
3.2 AutoConfiguration.imports 的写法细节
新注册文件里的顺序也有讲究,尤其是多个自动配置之间有先后依赖时。Spring Boot 3.4.x 支持通过注解指定顺序:
@AutoConfiguration(after = DataSourceAutoConfiguration.class) @AutoConfiguration(before = MyBatisAutoConfiguration.class) public class MyDatabaseCustomizerConfiguration { }如果不用注解,可以配合@AutoConfigureOrder或@Order,但更推荐直接在@AutoConfiguration注解上写清楚依赖关系,语义更清晰,别人看代码也容易理解。
我自己踩过的坑是:自己写的自动配置里依赖了 MyBatis 的SqlSessionFactory,但没声明 after,结果启动时报:
Consider defining a bean of type 'org.apache.ibatis.session.SqlSessionFactory' in your configuration.加上after声明后问题消失。这个坑在多个微服务里都会出现,升级时一定要检查自定义自动配置的装配顺序。
3.3 条件装配顺序混乱
除了自动配置本身,@ConditionalOnMissingBean这类条件注解的行为也可能变。因为 3.4.x 调整了部分自动配置内部类的加载顺序,某些在 3.3.x 里能正常生效的“兜底 Bean”不再被触发。
举个例子,项目中定义了一个全局的RestTemplateBean:
@Bean @ConditionalOnMissingBean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); }在 3.4.x 中,Spring Boot 自己可能已经先创建了一个 RestTemplate,导致我们的@ConditionalOnMissingBean不生效。最终表现为超时时间、拦截器配置全部没走我们的自定义版本。
排查方法是在启动日志里找到 Bean 的定义来源,或者直接用BeanFactory打印所有同类型 Bean 的名称。我建议升级后重点检查所有@ConditionalOnMissingBean标注的 Bean,确认真正加载的是你预期的那一个。
4. 数据访问层面的报错
4.1 Hibernate 版本升级带来的 dialect 问题
Spring Boot 3.4.x 默认管理的 Hibernate 版本是 6.6.x。相比 3.3.x 里的 Hibernate 6.5,差异不算小。最直接的问题是数据库方言的推断。我们用的 PostgreSQL 在升级后偶发启动报错:
org.hibernate.service.spi.ServiceException: Unable to create requested service [org.hibernate.engine.jdbc.env.spi.JdbcEnvironment]其实底层原因很多时候是包含了不兼容的 PostgreSQL JDBC 驱动版本。Spring Boot BOM 升级后,JDBC 驱动也悄悄升级了,但老驱动版本中某些行为和新 Hibernate 冲突。
相比之下更推荐显式配置方言,不让 Hibernate 自动推断:
spring: jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect虽然多写一行配置,但能减少很多潜在自动推断错误。另外,如果你的业务代码里直接用到了 Hibernate 内部 API,比如SessionFactory的某些实现类、HqlParser之类,升级前最好确认 API 是否还存在,6.6 里很多内部类挪了包名,编译能过不代表运行期就稳。
4.2 SQL 初始化脚本不执行
升级后另一个我遇到的场景是:应用启动时不再自动执行schema.sql和data.sql。Spring Boot 3.4.x 对spring.sql.init.mode的默认行为维持了传统逻辑:内嵌数据库时默认embedded,非内嵌数据库时默认不执行。但很多人会把初始化脚本放在classpath:下,换库或者调整启动方式后,脚本突然不跑了。
解决方案很直接,显式指定:
spring: sql: init: mode: always但要注意,always模式下每次启动都会执行脚本,如果脚本不是幂等的,重复启动就会报唯一约束错误。所以脚本里尽量用CREATE TABLE IF NOT EXISTS、INSERT ... ON CONFLICT DO NOTHING这类幂等写法,或者用 Flyway/Liquibase 做版本化迁移。
我建议生产环境不要依赖spring.sql.init做结构性变更,它更适合本地开发、测试环境初始化数据。生产库的表结构变更还是交给 Flyway/Liquibase 比较靠谱。
4.3 Flyway/Liquibase 在 3.4.x 下的版本兼容
Spring Boot 3.4.x 依赖的 Flyway 版本相对较高,公司里如果有老项目还在用 Flyway 6.x 或 7.x,直接升上来会出现日志里没有任何执行记录,甚至启动直接卡住。此时先检查 Flyway 版本是否在 Spring Boot BOM 的管理范围内。如果手动指定了低版本,必须对照官方支持矩阵调整。
Liquibase 也是同理,Spring Boot 3.4.x 默认管理 Liquibase 4.30 左右,老版本配置项liquibase.changeLog在新版里依然兼容,但liquibase.parameters、上下文配置这些细节变了。团队里统一用配置中心下发迁移脚本时,容易出现“脚本位置找不到”的报错,优先确认 classpath 路径配的是不是classpath:db/changelog/db.changelog-master.yaml且 YAML 格式本身能被新版解析。
5. Web 层和测试切片
5.1 REST 客户端默认行为变化
Spring Boot 3.4.x 在 Web 层面的改动较多,特别是对 RestClient、RestTemplate、WebClient 的自动配置方式有了更明确的限定。以前我们用RestTemplateBuilder注入时可以获得一些默认配置,升级后如果RestTemplate是单独手动 new 的,那些默认拦截器、消息转换器就不会生效。
实际遇到的问题是接口调用第三方服务时,请求头的Content-Type变成了text/plain,因为构建时没有正确设置HttpMessageConverter。在新版本里更推荐使用RestClient取代RestTemplate,它是响应式的老风格替代,但又不是 WebFlux,适合传统 Spring MVC 项目。
示例:
@Service public class OrderClient { private final RestClient restClient; public OrderClient(RestClient.Builder builder) { this.restClient = builder .baseUrl("https://example.com") .defaultHeader("Accept", "application/json") .build(); } }这种方式在测试时也更容易 mock。升级后如果原有代码大量使用RestTemplate,至少要把依赖注入方式改成RestTemplateBuilder,避免直接new RestTemplate()。
5.2 @MockBean 弃用警告与替换
Spring Boot 3.4.x 对@MockBean和@SpyBean标记了弃用,推出了新的@MockitoBean和@MockitoSpyBean。刚开始只是 warning,但团队里测试代码多,每个测试类都刷 warning 也很影响排查。所以建议逐步替换。
老写法:
@MockBean private OrderMapper orderMapper;新写法:
@MockitoBean private OrderMapper orderMapper;差异主要在底层实现上,@MockitoBean会更贴合 Spring TestContext 和 Mockito 的配合方式。我遇到的问题是,在某些@WebMvcTest切面下,@MockBean仍然可用,但配合@MockitoBean时,orderMapper注入不进测试上下文,最后发现是因为测试类里同时使用了@WebMvcTest和@Import,导致切片加载的上下文不完整。
建议迁移时一个测试类一个测试类地改,别全项目搜索替换,否则行为差异容易一起冒出来。改完记得跑一遍全量测试。
5.3 虚拟线程开启后的坑
Spring Boot 3.4.x 对虚拟线程的支持更加成熟,配置项依然是:
spring: threads: virtual: enabled: true但开启后我发现很多问题不是 Spring Boot 本身的,而是依赖的线程池和锁机制。
典型场景:某个中间件客户端内部使用ThreadLocal传递链路追踪 ID,而虚拟线程的数量可以非常多,ThreadLocal 不会按平台线程池那样复用,容易造成上下文泄漏。还有使用了synchronized的旧代码,在虚拟线程下可能出现更频繁的锁竞争,因为虚拟线程挂起时并不会释放底层载体线程。
我的建议是:
- 虚拟线程先在小流量服务上试,不要一上来就全量开。
- 开启后立刻检查链路追踪、数据库连接池、HTTP 客户端连接池的指标。
- 如果只是想让 Tomcat 处理请求时用虚拟线程,可以只配 server 相关的虚拟线程执行器,保留业务线程池不变,逐步扩大范围。
5.4 测试切片加载失败
升级到 3.4.x 后,我遇到一个很经典的报错:
Failed to load ApplicationContext Caused by: java.lang.IllegalStateException: No primary or single unique constructor found for interface ...原因其实是@SpringBootTest里引到了某个自动配置类,该类在 3.4.x 中构造方法签名变了,但测试上下文没有刷新。排查的时候先看报错前有没有BeanDefinitionStoreException,如果有,说明是配置元信息没更新。这时优先检查META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中的配置类是否还存在,以及是不是有重复注册。
测试切片环境下问题尤其多,比如@MyBatisTest或@JdbcTest,它们只加载相关自动配置,不加载完整上下文,一旦自动配置类顺序变化,就可能缺少某个必要 Bean。这类问题最直接的办法是给测试切片加上@Import,显式导入所需的配置类,而不是依赖 Spring 自动推断。
6. 构建工具与依赖兼容
6.1 Lombok 版本必须同步升
Spring Boot 3.4.x 官方 BOM 里 Lombok 版本并不是强制项,因为 Lombok 是编译期工具。但如果你还用旧版 Lombok,大概率会在编译阶段报错,比如:
java: java.lang.ExceptionInInitializerError或者更直接的:
java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor原因很简单:新版 JDK 编译器内部结构变化,旧 Lombok 解析不了。Lombok 1.18.36 以后对 JDK 23/24 的支持才完整。所以升级 Spring Boot 3.4.x 时一定要显式把 Lombok 提升到 1.18.36+,不能只依赖 BOM。
如果项目里有大量使用了@SneakyThrows、@Tolerate这类特性的代码,建议升级 Lombok 后全量编译一次,那些不起眼的编译告警也值得看。告警里常会出现“不推荐使用该 API”的提示,提前处理能避免后续坑。
6.2 Maven/Gradle 插件问题
Maven 项目升级 Spring Boot 3.4.x 时,spring-boot-starter-parent和spring-boot-maven-plugin的版本必须一起改。如果只改依赖版本,不改插件版本,会出现打包后无法启动的问题。
典型报错:
Error: Could not find or load main class com.example.Application这是因为老插件生成的 manifest 里Main-Class路径不对。解决方案就是把插件版本同步到 3.4.x。还有maven-compiler-plugin的版本,推荐使用 3.13.0 或更高,避免和 JDK 23 配合时出现参数解析错误。
Gradle 项目被坑得更多。Spring Boot 3.4.x 要求 Gradle 8.4 以上,我建议直接用 8.12+,否则插件内部使用的 API 和当前 Gradle 版本不匹配,会出现Could not resolve plugin或任务异常。升级后记得把dependency-management插件也一起更新,保证 BOM 生效。
6.3 BOM 依赖冲突
Spring Boot 3.4.x 管理了大量第三方依赖版本,包括 Jackson 2.18、Micrometer 1.4、Netty 4.1.x 等。如果你的项目里手动覆盖了某些版本,升级后可能触发冲突。
我遇到的真实案例是:项目为了处理特殊 JSON 序列化,手动指定了 Jackson 2.15,结果 Spring Boot 3.4.x 默认的 Jackson 2.18 没生效,导致JavaTimeModule行为不一致,接口返回的时间格式和升级前不同。好在这种问题通过看启动日志就能发现,日志里会有 Jackson 版本警告。
还有 Netty,微服务网关里手动引入了老版本 Netty,升级后启动时出现NoSuchMethodError,就是因为异步底层库版本不一致。建议升级后跑一遍mvn dependency:tree或 Gradle 的dependencies任务,把有冲突的版本列出来,优先以 Spring Boot BOM 为准,除非你有非常明确而且经过验证的理由覆盖版本。
7. 常见异常速查表
我把这段时间遇到以及处理过的问题整理成了一张速查表,看到类似报错可以直接定位。
| 报错或现象 | 常见原因 | 处理方案 |
|---|---|---|
| No property: classpath:config/application-common.yml | spring.config.import路径错误或文件不存在 | 检查路径,或加optional:前缀 |
| Consider defining a bean of type 'SqlSessionFactory' | 自动配置加载顺序变化 | 自定义自动配置上加@AutoConfiguration(after = MyBatisAutoConfiguration.class) |
| Failed to load ApplicationContext | 测试切片加载了不兼容的自动配置 | 显式@Import所需配置,重跑测试 |
| NoSuchMethodError: logback... | logback 或第三方日志库版本不兼容 | 升级到 Spring Boot 3.4.x 兼容版本 |
| Could not find or load main class | spring-boot-maven-plugin版本未同步 | 插件版本同步到 3.4.x |
| SQL 初始化脚本不执行 | spring.sql.init.mode默认不会初始化非内嵌数据库 | 显式配置mode: always,注意脚本幂等 |
| RestTemplate 请求头变成 text/plain | 消息转换器没配好或未用 Builder 构建 | 使用 RestClient 或RestTemplateBuilder |
| Map 类型配置绑定为空 | 环境变量覆盖转换问题 | 配置文件内绑定,避免复杂 Map key 的环境变量写法 |
| Lombok 编译期异常 | Lombok 版本过旧 | 升级到 1.18.36+ |
| 虚拟线程开启后线程上下文泄漏 | ThreadLocal/锁使用不当 | 先小流量验证,逐个排查阻塞逻辑 |
这张表我之后还会继续扩充。如果你现在遇到表里没列出的问题,大概率是版本组合比较特殊,建议先把 Spring Boot BOM、JDK、Lombok、Maven/Gradle 插件这四类基础环境锁定,再缩小范围排查。
在持续维护这个踩坑列表的过程中,我最大的体会是:Spring Boot 3.4.x 整体架构没有颠覆性变化,但很多细节行为变得更严谨了。以前那些“差不多能跑”的写法,升级后会直接暴露出来。如果你正准备升级,千万别只盯着自己业务代码里的问题,花点时间检查公共 starter、自定义自动配置、测试切片和构建插件,这些地方才是大部分坑的所在。最后再分享一个小技巧:升级完成后的第一周,把线上日志里的 warn 和 error 单独拉出来看看。很多问题在测试环境根本触发不了,只有真实流量进来才会露头。慢一点、稳一点,3.4.x 是可以安全接管的。