跳到主内容

Spring Boot升级实战:2.x到3.x跨大版本迁移的依赖治理清单与回归策略 ​

Spring Boot 2.7 的开源维护已经终止,安全补丁不再跟进,留在旧版本上等于裸奔;而升级到 3.x 又横着三座大山:JDK 基线跳到 17、javax.* 全量换成 jakarta.*、大量第三方 starter 存在兼容性断档。很多团队因此把升级计划一拖再拖。我们最近完成了一个 40 万行代码、120 个三方依赖的服务从 2.7 到 3.3 的迁移,编译期问题占了六成工作量,运行期行为变化占了三成,剩下的是配置项与安全组件改造。复盘下来,迁移的难度不在技术深度,而在治理顺序——顺序对了,两周可以收尾;顺序错了,会陷入「改一个报三个」的泥潭。本文按实际执行顺序复盘全过程。

第一步:先定 JDK 与依赖兼容矩阵,不要急着改代码 ​

升级最大的坑不是 Spring 本身,而是第三方依赖。动手前先列一张三方依赖清单,逐个确认其 Spring Boot 3 / Jakarta EE 9+ 兼容版本:

text
典型兼容断档清单(示例,以各项目实际依赖为准)
- 老版本 MyBatis starter        → 升 mybatis-spring-boot-starter 3.x
- Swagger 2.x / springfox      → 停更,迁移到 springdoc-openapi 2.x
- 老版本 Druid starter         → 升 druid-spring-boot-3-starter
- 内部公司框架                  → 联系维护组确认 3.x 适配版本或排期
- JDK 8 序列化类库(如旧版 fastjson)→ 评估换 fastjson2 或 jackson

JDK 基线方面,Spring Boot 3.0 起要求 Java 17,3.x 后续版本对 17/21 均可运行。建议直接选团队支持的最新 LTS,避免「为了升级 Boot 再升一次 JDK」的二次折腾。内部框架没有适配版的,要在这个阶段就决定:绕开、替换,还是推动适配——这是整个迁移排期里最不可控的部分,必须最先暴露。

顺带解释一下 javax 到 jakarta 这场变更的由来:Oracle 把 Java EE 移交 Eclipse 基金会时,javax 命名空间的商标权仍留在 Oracle 手中,基金会只能整体改名为 Jakarta EE 并启用 jakarta 命名空间。这不是 Spring 单方面的折腾,而是整个 Java 生态的一次强制迁移——理解了这一点,就能明白为什么连 Hibernate、Tomcat 的包名都变了,也就能把升级工作拆成「改依赖版本」和「改代码引用」两条独立推进的线,避免混在一起互相阻塞。

第二步:编译期改造,让机器干机械活 ​

javax 到 jakarta 的替换、废弃 API 的迁移这类规则明确的改动,交给 OpenRewrite 批量处理:

bash
# 在 Maven 项目根目录执行,自动改写 pom 与源码
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3

机器跑完之后,人工主要处理三类残留:一是反射、配置文件、模板里的 javax 字符串引用(扫描工具查不到运行时拼接的类名);二是 SecurityFilterChain 组件式配置替代 WebSecurityConfigurerAdapter 的结构性改造;三是各类属性名变更,此时引入 spring-boot-properties-migrator 依赖,启动时会报告废弃属性并临时映射,全部处理完再移除:

java
// SecurityFilterChain 组件式写法:替代被移除的 WebSecurityConfigurerAdapter
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/public/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated())
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    return http.build();
}

第三步:清理运行期行为变化,这一步最容易漏 ​

编译通过不等于升级完成。Spring Framework 6 带来了一批默认行为变化,直接改业务结果而不报错,属于高危区:

行为变化影响处理
尾斜杠匹配默认关闭/api/list/ 不再命中 /api/list显式声明或网关层归一化
数据绑定默认不再自动转字符串到枚举的宽松模式部分入参解析报 400检查接口契约
Actuator 部分端点路径与暴露策略调整监控采集断线同步改采集配置
spring.redis 等属性挪到 spring.data.*配置失效但不报错用迁移工具确认
Hibernate 升级到 6.x个别原生 SQL 与隐式 join 行为变化全量跑数据层测试

其中尾斜杠匹配的变更是线上故障重灾区:客户端带着尾斜杠请求的老接口,升级后会直接 404。建议升级前 grep 所有对外接口文档与网关日志,确认调用方实际请求形态。

行为变化的排查不能只靠读更新日志,最高效的方法是做响应快照 diff:升级前用脚本录制核心接口的请求与响应 JSON,升级分支上重放请求并做字段级比对,两小时就能揪出三处序列化行为变化——这类问题若等到上线后由调用方报障再定位,代价会高一个数量级。双跑期间还要额外关注慢 SQL 日志:Hibernate 6 生成的 SQL 与旧版本存在差异,个别关联查询会因隐式连接策略变化出现新的全表扫描,双跑期是发现这类性能劣化的最后窗口。

回归策略:双跑对比加灰度放量 ​

对于核心服务,纯靠测试用例覆盖行为差异是不够的,我们采用三层回归:

  1. 契约测试:把升级前录制好的接口请求/响应快照逐个回放,diff 响应体差异,重点看字段增删与序列化行为(Jackson 从 2.13 到后续版本的默认值变化会影响 null 处理)。
  2. 双跑对比:新旧两个版本同时部署,生产流量镜像到新版本,只执行不返回,比对两个版本的核心指标(状态码分布、耗时分布、日志 ERROR 数)。
  3. 灰度放量:按 1% → 10% → 50% → 100% 切流,每档观察 24 小时,回滚预案保持随时可用。

排期与分工上,这个规模的一次迁移建议拆成四个阶段并各留缓冲:依赖治理约一周、编译期改造约一周、行为变化清理与测试修复约一周、灰度放量一周起步。最容易压缩的是第一阶段,也最不应该压缩——依赖矩阵清不干净,后面每一周都在为它买单。另一个实用建议是安排专人每天汇总「新增报错 Top5」,让阻塞项在站会上被看见,而不是散落在各自分支里各自排查。

yaml
# 灰度期间的快速回滚检查项(示例清单)
checks:
  - http 5xx 比例对比基线涨幅 < 0.1%
  - P99 耗时涨幅 < 10%
  - ERROR 日志关键字(ClassNotFound / NoSuchMethod)零出现
  - MQ 消费位移持续推进无堆积

升级检查清单 ​

  1. 先锁 JDK 版本与三方依赖兼容矩阵,未适配的内部框架提前推动,别让它在收尾期爆发。
  2. 机械替换交给 OpenRewrite,人工专注安全配置、反射引用与内部框架适配。
  3. 引入 spring-boot-properties-migrator 扫配置,处理完再移除。
  4. 重点验收「不报错但行为变了」的区域:尾斜杠匹配、序列化默认值、Hibernate 6 SQL 行为。
  5. 契约测试回放 + 双跑对比 + 灰度三道防线,任何一道异常先停放量再排查。

跨大版本升级的正确心态是「把风险前置暴露」:前两周看似进度慢,其实是在把最深的不确定性挖出来;一旦编译全绿、依赖矩阵清零,后面就是平滑收尾。升级完成后记得把过程沉淀为团队的升级手册,下一次就不用重走弯路。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。