Spring Boot 3.4升级踩坑:依赖兼容、路径匹配与结构化日志排查指南 说实话我对 Spring Boot 的版本升级一直是“谨慎乐观”。乐观是因为官方每次迭代确实在解决老问题谨慎是因为哪怕只是 minor 版本底层框架一换存量代码里那些“看似能用”的写法就会集中暴雷。这次把我们项目从 3.3.5 升到 3.4.2前前后后折腾了三个晚上遇到的问题从 springdoc 起不来、分页失效到路径 404、日志字段丢失能复现的我全都记录在这里后面如果继续踩到新坑我会持续往这篇里更。先说个总印象3.4.x 不是那种“无脑升”的版本它对第三方生态的适配要求比 3.2、3.3 高。主要原因倒不是 Boot 本身改了多少而是它脚下的 Spring Framework 升到了 6.2很多第三库是按 6.1 编译的一混用就出 NoSuchMethodError。下面我把踩过的坑按类别拆开写每个坑都尽量说清楚原因和最终解决方式方便你对照排查。1. 升级之前的整体盘点和心理建设1.1 3.4.x 的底层变化不能只看 Release Notes很多人升级前只瞄一眼 Release Notes看到“新增结构化日志”“依赖版本升级”就草草动手结果改完配置重启就懵了。其实 3.4 真正的变化集中在三个底层点第一是 Spring Framework 6.2 对路径匹配的调整。从 Spring Framework 6.0 开始Spring MVC 默认就切到了PathPattern但 6.2 又把不少边界情况收紧了尤其是“可选路径变量”和“斜杠匹配”的行为。原来用AntPathMatcher写的老接口可能在升级后突然 404这个我在 2.3 节会展开说。第二是配置属性绑定的严格化。ConfigurationProperties在 3.4 里对类型转换和集合绑定做了更强的校验。以前配置写错比如 list 里混进一个空字符串、数字写成了字符串可能最多打条 warning项目照样启动现在很多场景下直接启动失败报ConfigurationPropertyName或ConversionFailedException错误。这其实是好事但升级时会把存量配置里欠的“债”全翻出来得有点心里准备。第三是 Jackson 模块的加载顺序。3.4 对ObjectMapper的自动配置调整了不少自定义Jackson2ObjectMapperBuilderCustomizer时如果和 Boot 内置的JavaTimeModule等模块产生重复注册很容易出现“时间格式没变”“序列化结果和以前不一样”这类诡异问题。这个问题排查起来最费时间因为代码没报错只是输出变了。1.2 升级前先给自己做一张兼容清单我们在动手升级前做了一次盘点强烈建议你也照着走一遍先把项目里直接依赖和间接依赖拉出来看一遍。重点看 springdoc-openapi、mybatis-spring-boot-starter、mybatis-plus、redisson-spring-boot-starter、hutool 这类使用频率高、又与 Spring 底层耦合深的库。用 IDE 的 Maven Dependency Hierarchy 逐个查它们是否已经发布针对 Spring Boot 3.4 的适配版本。这一步没做后面 90% 的坑都跟它有关。再把配置文件里所有带server.、spring.、management.前缀的配置项过一遍。3.4 对配置项的前缀命名做了一些收敛不少老配置虽然还兼容但已经标了 deprecated控制台启动时会刷一堆 warn。最好开一次--debug启动把自动配置报告和 warn 日志留底方便升级后对比。最后就是评估项目里用了哪些“深层 API”。如果你们用了WebMvcConfigurer做拦截器、用了RequestContextHolder、自定义了HandlerMethodArgumentResolver升级前一定要在测试环境把相关接口全部回归一遍。这些扩展点最容易受到底层 API 签名变化的影响。总之别急着改版本号先花一个小时做上面三件事比踩完坑再回头查要划算得多。2. 高频踩坑现场还原2.1 坑一springdoc-openapi 一启动就报错我们第一个遇到的就是 springdoc。项目里用的是springdoc-openapi-starter-webmvc-ui2.6.0升级完 Spring Boot 3.4.2 后一启动直接报NoSuchMethodError: org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerMethod之类的问题当时差点以为是两个配置类冲突。原因其实很简单Spring Framework 6.2 改动了部分 Web MVC 内部 API而 springdoc 2.6.x 是基于 6.1 编译的运行期找不到旧方法自然就炸了。这不是配置问题是编译期字节码和运行期类库不一致。解决方式是把 springdoc 升到 2.8.x。我们最终用的 2.8.9这个版本明确支持 Spring Boot 3.4.x 和 Spring Framework 6.2dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.8.9/version /dependency如果你用的是 webflux 版本对应的springdoc-openapi-starter-webflux-ui也要同步升到 2.8.x。升级后注意/v3/api-docs和/swagger-ui.html两个路径是否正常如果 404多半是 Spring MVC 的静态资源映射或路径匹配策略变了去检查有没有自定义WebMvcConfigurer对这两个路径做过 rewrite。补充一个经验springdoc 升级后原先自定义的OpenAPIBean 里如果写了过时的springdoc.swagger-ui.*配置也会被 strict 模式拦住建议把springdoc开头的配置全部整理一遍删掉无用的旧项。2.2 坑二MyBatis-Plus 分页插件失效第二个坑比 springdoc 更难发现因为它不报错只是分页不生效。项目用了 mybatis-plus 的PaginationInnerInterceptor升级后所有分页查询接口突然全部返回全量数据一开始还怀疑是 SQL 写错了后来才定位到是 mybatis-plus 与 Spring Boot 3.4 的自动配置顺序不兼容。具体表现是MybatisPlusInterceptorBean 明明在配置类里定义了但 SQL 解析拦截器没生效。底层原因是 mybatis 相关 starter 在版本较低时其自动配置类在 3.4 的自动配置加载链中被后置导致拦截器在 SqlSessionFactory 创建完成后才注册。解决方式比较直接把 mybatis-plus starter 升到 3.5.7 以上我们用的是mybatis-plus-spring-boot3-starter3.5.7分页恢复。如果你是原生 mybatis用mybatis-spring-boot-starter的话建议升到 3.0.4。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency同时提醒一句分页插件如果原来是用Bean方式注册的升级后最好保持原样别改成手动 new 到 SqlSessionFactory 里那样反而容易和 Boot 的自动配置打架。检查分页是否生效最简单的方法是把PaginationInnerInterceptor的maxLimit设置为 100然后调用一个本来要查全表的接口如果结果被限制到 100 条说明插件真正起作用了。2.3 坑三路径匹配规则变了接口突然 404这个坑对我们老项目杀伤力最大。项目里有个接口Controller 方法签名是GetMapping({/user, /user/{id}}) public Result getUser(PathVariable(required false) Long id) { ... }在 3.3.x 上跑得好好的升级到 3.4.2 后访问/user/末尾带斜杠时直接 404访问/user/123没问题。后来查了 Spring Framework 6.2 的 path matching 变更发现PathPattern对“多模式映射 可选路径变量”的组合处理更严格了。以前 AntPathMatcher 对/user和/user/的处理比较宽容现在 PathPattern 在语义上把带斜杠和不带斜杠当作不同路径required false的变量也就覆盖不到这种边界。有两种处理方式。最省事的是恢复 AntPathMatcherspring: mvc: pathmatch: matching-strategy: ant_path_matcher但我不推荐这个方案。一是因为它只是暂时绕过下一版本可能直接把配置项废弃二是因为 AntPathMatcher 在性能上确实比 PathPattern 差尤其在大量请求匹配时。推荐的做法是改接口设计不要让一个方法同时处理“有无变量”两种形态干脆分成两个方法GetMapping(/user) public Result getUserList() { ... } GetMapping(/user/{id}) public Result getUser(PathVariable Long id) { ... }如果项目里有很多类似接口逐个改工作量太大也可以折中一下单独给这些旧接口用RequestMapping(path {/user, /user/{id}}, produces ...)但务必在测试环境把带斜杠和不带斜杠两种请求都回归一遍。这次踩坑后的体会是接口定义越“含糊”升级风险越高路径匹配这种底层语义变化靠经验是防不住的。2.4 坑四Jackson 时间格式静默改变第四个坑发生在接口返回值层。项目里全局配置过spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8升级到 3.4 后LocalDateTime字段在部分接口返回值里变成了数组形式比如[2025,1,10,15,30,0]前端直接就渲染不了了。查了半天代码发现没有任何一处改过 JavaTimeModule最后在自动配置报告里看到JacksonAutoConfiguration的优先级变化才知道是 Boot 对spring.jackson.*的绑定时机做了调整自定义定制器和内置模块的执行顺序和以前不一致。最稳妥的修法是不再依赖全局配置而是显式注册一个Jackson2ObjectMapperBuilderCustomizerBean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - { builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); builder.timeZone(TimeZone.getTimeZone(GMT8)); builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; }这样做的好处是明确控制序列化行为不受 Boot 内部 Bean 顺序影响。如果你的项目里已经有自定义ObjectMapper的 Bean要特别小心别和这个 Customizer 同时存在否则会出现 Customizer 不执行的问题。踩坑之后我建议团队里定一个规范全局时间格式一律用LocalDateTimeSerializer处理不要依赖spring.jackson.date-format这种“看起来改了但实际管不到 JSR310”的配置。3. 结构化日志与可观测性新功能背后的坑3.4 把结构化日志定成了标配功能这个方向是好的但引入新功能的同时也带来了一些和旧体系冲突的坑。3.1 启用 ECS 格式后的第一个坑项目接入日志平台时我看到 3.4 支持原生结构化日志想着能少引一个 logstash-logback-encoder就把配置改成了logging: structured: format: console: ecs启动后控制台确实变成了 JSON 格式输出也符合 Elastic Common Schema但日志采集 agent 在服务端却收不到任何新日志。排查后才发现生产环境采集器读的是日志文件不是控制台而我只配了console文件输出还是纯文本。正确的做法是同时配置 filelogging: structured: format: console: ecs file: ecs这里有个细节这个配置的值可以是ecs、logstash、gelf分别对应 Elastic Common Schema、Logstash JSON 格式和 Graylog Extended Log Format。如果你的日志平台用的不是 ES可以按需换成logstash或gelf。另外如果项目里原本就有自定义的logback-spring.xml里面定义了自己的 ConsoleAppender 和 pattern结构化日志的 console 配置会失效。因为 Boot 的结构化日志接管的是它自己创建的 appender不是你自定义的那个。我们后来把自定义的 appender 停掉统一交给 Boot 管理才恢复正常。这个取舍要想清楚用了原生结构化日志就别再叠一层 logstash-encoder两套体系很容易冲突。3.2 traceId 等了半天日志里却没有微服务项目都习惯在日志里加 traceId。升级到 3.4 后我遇到一个问题通过X-Trace-Id传入的 traceId在同步接口里能正常打到日志但一走到异步线程、Async、或者消息队列消费端traceId 就丢了。原因在于 3.4 中日志关联 traceId 依赖 Micrometer Tracing 的Observation上下文而 MDC 里的 traceId 需要手动从 TraceContext 传递。异步场景下子线程的 MDC 默认是空的自然就没了。解决方式是在跨线程时手动把 traceId 塞进子线程的 MDC。我们用了一个简单的包装器在执行异步任务前做了设置public class TraceIdWrapper { public static T CallableT wrap(CallableT task) { MapString, String context MDC.getCopyOfContextMap(); return () - { if (context ! null) { MDC.setContextMap(context); } try { return task.call(); } finally { MDC.clear(); } }; } }然后在ThreadPoolTaskExecutor提交任务时或者Async自定义执行器里调用这个 wrapper。如果你用的是 Spring 的TaskDecorator那是更标准的做法直接在Executors配置里塞这个装饰器就行。这个坑不深但排查起来很迷惑因为同步日志正常、异步日志丢失看起来就像代码没生效。3.3 日志脱敏和自定义 filter 的兼容问题我们项目里有很多手机号、身份证号要脱敏以前用 Logback 的replace规则pattern%d{yyyy-MM-dd HH:mm:ss} %replace(%msg){(\d{3})\d{4}(\d{4}), $1****$2}%n/pattern升级后这个 replace 规则在某些情况下直接不执行了。后来才明白结构化日志格式下日志输出不是走 pattern 拼字符串而是由 JSON encoder 处理自然也不会去解析你写在 pattern 里的 replace 标签。解决方式是改成自定义转换器或者在 JSON 字段输出前用程序里的脱敏工具统一处理日志内容。我们最终采用的是后者在写入日志前先调用一个DesensitizedUtils.maskJson()方法把敏感的 JSON 字段值做脱敏再打日志。虽然麻烦一点但比依赖日志框架的 replace 规则更可靠而且结构化日志下也能稳定生效。这里给个建议如果你已经上了结构化日志尽量把“脱敏”从日志框架层面挪到业务代码或切面层面越早处理越稳妥。日志框架的这些文本替换功能更适合本地调试不适合生产结构化采集。4. 测试与构建链路上的细碎问题升级过程中还有一类问题躲不开测试跑不起来构建老失败。这些问题虽然不直接影响线上运行但特别耗时间。4.1 SpringBootTest 上下文加载慢、配置类不生效升级后第一次跑集成测试整体耗时从原来的 40 秒涨到了将近两分钟而且有些TestConfiguration里定义的 Bean 在某些测试类里不生效。一开始以为是机器问题后来发现和 3.4 的上下文缓存机制有关。Spring Boot 3.4 对上下文缓存的 key 计算变得更细了比如MockBean、ActiveProfiles、DynamicPropertySource的 static 方法任何一个属性变化都会导致缓存失效从而重新加载上下文。所以测试类之间如果配置差异大整体耗时就会明显上升。排查下来最影响的是DynamicPropertySource。我们有个测试类漏写了static关键字在 3.3 上还能跑3.4 直接给你抛异常提示这个方法必须是 static。这个错误不是升级后才有的但 3.4 明确把它变成强制要求。改法就是老老实实加 staticDynamicPropertySource static void props(DynamicPropertyRegistry registry) { registry.add(server.port, () - 0); }要降低测试耗时可以尽量复用同一个 Spring 上下文把相同配置的测试类放在同包同 properties 下少用MockBean多在src/test/resources里放统一的application-test.yml。我们把每个测试类上那些冗余注解清理了一下耗时就降回正常水平了。4.2 MockMvc 测试拿不到预期 JSON单元测试里用MockMvc调用接口明明接口返回正常但andExpect(jsonPath($.code).value(200))一直失败响应体打印出来却是空。这个坑也是升级后出现的最后发现是ObjectMapper的序列化行为变了某些场景下MappingJackson2HttpMessageConverter没有正确注册进 MockMvc。我是这样排查的先把响应体打印出来mockMvc.perform(get(/api/user)) .andDo(print()) .andExpect(status().isOk());打印结果里能看到 HTTP 状态正常但 body 为空。这说明问题出在 message converter 上而不是业务代码。解决方案是在测试类里显式注入 ObjectMapper 并配置 MockMvcAutowired private ObjectMapper objectMapper; mockMvc MockMvcBuilders .webAppContextSetup(context) .addConverter(new MappingJackson2HttpMessageConverter(objectMapper)) .build();如果你是直接用SpringBootTest加AutoConfigureMockMvc还有另一个小坑3.4 中 Jackson 的FAIL_ON_EMPTY_BEANS默认值有变化某些只有一个 getter 的 DTO 在序列化时直接抛异常导致响应体为空。解决办法是检查 DTO把所有没有字段的 getter 去掉或者在配置里重新允许空 Bean 序列化。我个人倾向于修 DTO别为了测试开空序列化线上容易漏数据。4.3 Maven 插件、JDK 版本、mainClass 的三角关系构建链路是升级时最容易忽视又最容易报错的部分。我们的 CI 机器上同时装了 JDK 8、11、17升级到 3.4 后maven 打包时一直报“无效的目标发行版”因为默认的maven.compiler.source/target还是 1.8。Spring Boot 3.4 最低要求 Java 17这个问题必须清楚根因不是 Spring Boot 不能跑在 JDK 17 上是你的 Maven 编译参数还在用老版本 JDK 级别。建议直接在 pom 里统一配置properties java.version17/java.version maven.compiler.release17/maven.compiler.release /properties注意用maven.compiler.release而不是 source/target它更能保证编译和运行期 JDK API 一致避免出现“编译过了、跑起来 NoClassDefFoundError”的情况。另一个坑是 spring-boot-maven-plugin 在 3.4.x 下重新打包时偶尔找不到启动类报Unable to find main class。一般情况下它会从Start-Class属性推断但如果你用了多模块且 parent 配置比较复杂最好显式指定 mainClassplugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.Application/mainClass /configuration /plugin这个配置在开发阶段看着多余但到了 CI 上能省很多时间。如果你们用 gradle 构建思路也一样bootJar时显式指定主类tasks.named(bootJar) { mainClass com.example.Application }5. 常见问题排查速查表与验收清单5.1 升级适配版本速查表下面这个表是这次升级后我们沉淀下来的适配版本对照直接照着填依赖基本不会踩大坑组件推荐版本备注Spring Boot3.4.23.4.0 刚发布时有些小问题建议直接用 3.4.2 以上springdoc-openapi2.8.x2.6.x 在 Spring Framework 6.2 下会报 NoSuchMethodErrormybatis-spring-boot-starter3.0.4兼容 3.4 自动配置链mybatis-plus-spring-boot3-starter3.5.7分页插件失效会发生在低版本redisson-spring-boot-starter3.283.27 之前版本在 6.2 下可能遇到兼容问题hutool5.8.27注意旧版反射工具方法签名问题spring-kafka3.2.x和 3.4 的 Observability 集成更顺这个表只是参考最终版本建议以官方 release 页面为准。团队升级时建议先开一个分支把依赖统一升到这些版本后跑一遍集成测试不要一个一个试否则容易像我们一样把时间耗在“依赖之间互相不匹配”的连环坑里。5.2 启动排查手段速查表遇到启动环节的各种问题按这张表做基本能快速定位现象排查手段常见根因启动报 NoSuchMethodErrormvn dependency:tree先看间接依赖版本第三方库版本基于旧 Spring Framework 编译启动失败绑定异常看--debug自动配置报告配置项类型或 list 值非法部分接口 404检查是否开启spring.mvc.pathmatch路径匹配策略差异JSON 序列化异常打印响应体或开启 Jackson debug模块加载顺序/空 Bean 序列化日志格式不对检查结构化日志同时配置 file 和 console只配置了 console 导致采集不到线上日志这些手段里最推荐的就是启动加--debug。不是让它跑更慢而是让自动配置报告把所有“条件不成立、配置被忽略”的原因全打出来比翻源码高效太多。5.3 升级后的验收清单最后建议按下面这个清单做一遍回归别等到上线前才手忙脚乱所有分页查询接口确认没有出现“全量返回”情况尤其是用了分页插件的老接口。时间格式相关的接口特别是含 LocalDateTime/LocalDate 字段的逐一比对升级前后的返回值。路径不规范的接口比如末尾斜杠、双斜杠、可选变量的场景全部请求一次。带有自定义拦截器的接口确认拦截器注册成功不出现 404 或 500。日志采集链路确认结构化日志的字段名在日志平台能正确检索traceId 在异步链路中正常。测试环境跑一次完整集成测试观察上下文加载耗时是否有异常增长。CI 构建产物的启动日志确认 mainClass 正确、JDK 版本无误。我个人的习惯是每次升级完把项目里所有带 3.x 版本的第三方依赖全部搜一遍用 dependency hierarchy 做一次体检然后再看启动过程中的 warn 日志。很多时候坑在警告里已经写了一半答案了。这篇我会持续更新后面如果再碰到其他值得记录的 3.4.x 问题我会直接补充进来希望能帮你少走点弯路。