8 月最后一个周五,我把公司那个跑了三年的项目从 Spring Boot 3.5 升到 4.0。3.5 是 3.x 最后一个版本,开源支持今年 6 月底已经停了,漏洞补丁就断了,涉及资金的后端不敢赌。按 Migration Guide 的建议,先升 3.5 最新 patch 清完 deprecation 警告,再改版本号到 4.0.8,顺手引入 spring-boot-properties-migrator。 编译通过只花了半小时。我当时还发消息跟同事说"这也太顺了"。八天后我才明白这句话说得有多早。
starter 白嫖时代结束了
踩得最轻的一个坑,反而来自 Boot 4 架构变动最大的那块——模块化。原来巨大的 spring-boot jar 被拆成一堆小模块,每个叫 spring-boot-<技术名>,根包是 org.springframework.boot.<技术名>。听着是好事,但有个隐含后果:以前你能"白嫖"的技术,现在都得显式声明。 Migration Guide 里那句 features that only relied on a third-party dependency to work can require an additional starter,我第一遍读漏了,直到测试环境部署——Flyway 不跑了。 我的用法很典型:pom 只加 flyway-core,Boot 3 检测到类路径上有 Flyway 就直接接管。Boot 4 里 Flyway 的自动配置搬进了 spring-boot-flyway 模块,不引依赖它就不在 classpath 上。结果就是:应用正常启动,日志里没有任何 Flyway 输出,数据库还是老结构,接口一跑 SQL 报"表不存在"。启动日志对比—— 升级前(3.5):
o.f.c.internal.license.VersionPrinter : Flyway Community Edition by Redgate
o.f.core.internal.command.DbValidate : Successfully validated 14 migrations
o.f.core.internal.command.DbMigrate : Current version of schema `main`: 14.00.00
升级后(4.0.8):这三行直接消失,应用照样 Started in 6.5 seconds,岁月静好。 这种问题不会自己冒头,"表不存在"的告警十分钟内弹到运维群,我翻完整个启动日志才确认 Flyway 压根没跑——谁平时会去数 Flyway 的日志输出。 修复很直接,把裸依赖换成官方 starter:
<!-- Boot 3.5:只加第三方依赖,自动配置照样生效 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- Boot 4:必须用 starter,Flyway 才会被自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
Liquibase 同理。wiki 里那张 starter 对照表值得逐行过——我们过完表又补了一个:测试里用了 @WithMockUser,得加 spring-boot-starter-security-test,这类注解的基础设施也拆进了独立的 test 模块。 版本号顺手记一笔:spring-boot-starter-web 没删,改名成 spring-boot-starter-webmvc,旧的标 deprecated,我们一步到位改了。OAuth2 那几个 starter 同套路改名,diff 很大但机械。项目特别老的话还有 spring-boot-starter-classic 这个过渡桥,把所有模块拉回来先跑起来,再按需拆细。我们规模不大,直接一步到位了。
Jackson 3:改个包名,动半个项目
真正耗了我两天的是 Jackson 3 的包名迁移。 Boot 4 把默认 JSON 库换成 Jackson 3,包名从 com.fasterxml.jackson 全面迁到 tools.jackson,唯一例外是 jackson-annotations 还留在原坐标。项目里所有 import com.fasterxml.jackson.databind.ObjectMapper 这类引用都得改,IDE 全局替换能解决七成,剩下三成才是坑。 麻烦在于注解没搬家——@JsonProperty、@JsonIgnore 还在原包,一半 import 要改,一半不用改,批量替换容易改过头。Boot 自己的注解倒是编译期能现形:@JsonComponent 变 @JacksonComponent,@JsonMixin 变 @JacksonMixin。 最阴的是注入 ObjectMapper 的地方,运行时才炸。本地一调公共序列化工具:
NoSuchBeanDefinitionException: No qualifying bean of type
'com.fasterxml.jackson.databind.ObjectMapper' available
我一开始以为是三方 SDK 换版本闹的,对着依赖树扒到第三遍才反应过来是 bean 类型不对——迁移指南里写 Boot 4 自动配置的是 Jackson 3 的 JsonMapper,原话 defining an ObjectMapper bean is no longer sufficient,你得声明 JsonMapper 或 XmlMapper bean。而我们那个三方 SDK 还依赖 Jackson 2,databind 的类留在 classpath 上,IDE 不报错。
// Boot 3.x:直接注入 ObjectMapper
private final ObjectMapper objectMapper;
// Boot 4:要注入 Jackson 3 的 JsonMapper
private final JsonMapper jsonMapper;
另外两处,一处直接打到业务上:Jackson 3 默认不再把日期写成时间戳,我们有个接口的 createdAt,升级前返回 1726012800000 这种毫秒数,升级后同一行数据变成 "2024-09-11T00:00:00Z",前端解析直接懵。第二个是配置属性搬家:spring.jackson.read.* 和 write.* 移到 spring.jackson.json.read.* 和 write.* 下面,properties-migrator 会提醒。好在有过渡属性 spring.jackson.use-jackson2-defaults=true 能对齐 Boot 3.x 默认行为;实在来不及,还有个 deprecated 的 spring-boot-jackson2 模块兜底,明说了只是给你喘口气。 这两天改到眼睛发花,四百多行 diff 里一半是 import。评审的同事看了一眼说"这周你改的代码我不看业务逻辑,只看包名"。
测试挂掉的方式更隐蔽
Jackson 弄完,我以为大局已定,本地全量测试跑了一轮,红的。这次的坑藏在测试基建里,一部分还是运行时才炸。 最直接的是 @MockBean 和 @SpyBean 被移除,换成 @MockitoBean 和 @MockitoSpyBean,编译期全部现形,好办。有个细节:新注解只能用在测试类字段上,不能放 @Configuration 里声明共享 mock。 真正阴的是这个,编译能过:按迁移指南,@SpringBootTest 不再自动提供 MockMVC,我们这种组合升级后拿不到 MockMvc 了,得显式加 @AutoConfigureMockMvc。同理,TestRestTemplate 和 WebClient 的 bean 也不再自动出现。我们四十多个 Controller 测试,一半是 @SpringBootTest 加 MockMvc 的组合,全要补:
// Boot 3.x
@SpringBootTest
class OrderControllerTest {
// Boot 4:必须显式声明
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {
好在这属于机械修改,一个下午搞定。但要是有人习惯性认为 @SpringBootTest 万能,升级后看到的会是 NoSuchBeanDefinitionException 而不是一句"请加注解",排查能耗半天。
有些坑我们没踩到
比如 Undertow 被移除——Boot 4 要求 Servlet 6.1 基线,Undertow 还没跟上,我们一直用 Tomcat,没感觉。真正碰到的只有探针:Actuator 的 liveness/readiness 默认开启了,K8s 健康检查突然多出两个探针组,对着告警规则核了一遍才放心。剩下像 Spring Batch 默认内存模式、DevTools live reload 默认关闭、Kafka/AMQP 重试收归 Spring Framework,wiki 里都有,抄一遍没意思。
迁移收尾之后
收尾是在周五,全量回归过了,灰度也放了。最想留给还没动的人一句话:别信"编译通过"。这次三个大坑两个半是运行时问题——Flyway 静默不跑、ObjectMapper 注入失效、MockMvc 凭空消失,全量测试和测试环境部署验证一个都不能跳。先在 3.5 上清完 deprecation 警告再动手,properties-migrator、classic starter 这些过渡工具提前装上,能省不少事。 跟 3.0 那次 javax 换 jakarta 比这轮算不上痛苦,只是磨人。模块化、JSpecify 空安全、Jackson 3 都是迟早要还的债,趁支持期结束动一次,总比带着更大包袱再动强。