一月的架构评审上,有人提议全量升 Spring Boot 4
开年第一次架构评审,议题是把公司 47 个 Java 服务统一升到 Spring Boot 4。提这个的人理由很充分:Boot 3.5 的 OSS 支持快到期,Spring AI 2.0 的里程碑版本已经明确要求 Boot 4 基线,晚动不如早动。
我当时没表态,说先让我做两周评估。这篇是评估的结论和过程。结论先说:我们最终没有全量升,只动了 9 个服务,剩下的排到下半年按业务节奏走。
先看基线要求到底变了什么
Boot 4.0 挂在 Spring Framework 7 上,硬门槛有三条:
| 项 | Boot 3.5 | Boot 4.0 |
|---|---|---|
| 最低 JDK | 17 | 17(推荐 21+) |
| Spring Framework | 6.2 | 7.0 |
| Servlet 容器 | Tomcat 10.1 / Jakarta EE 10 | Tomcat 11 / Jakarta EE 11 |
| Jakarta 命名空间 | jakarta.* | jakarta.*(EE 11 版本) |
| Hibernate | 6.6 | 7.x |
从表格看,JDK 17 这条对我们最友好——比想象中宽松,Boot 4 并没有像传言那样要求 JDK 21。我们内部的 JDK 分布是:
JDK 8 : 11 个服务 (老交易链路,2019 年前的遗留)
JDK 11 : 9 个服务
JDK 17 : 21 个服务
JDK 21 : 6 个服务 (2024 年后新建,主要是 AI 平台)
JDK 25 : 0 个服务
也就是说 20 个服务连 Boot 4 的最低门槛都没到,其中 11 个还停在 JDK 8。这部分如果升 Boot 4,等于把「JDK 8 → 17」和「Boot 2.x → 4.0」两件事绑在一起做,风险叠加。
与 Spring AI 的绑定关系才是真问题
单纯为了「不落后」去升 Boot 4,我是不赞成的。真正让我们必须动的是 Spring AI。
我们 AI 平台用的是 Spring AI 1.x,踩过一个很实际的坑:1.x 的 Advisor 链在流式响应下拿不到完整的工具调用记录,做审计和成本归因很别扭。Spring AI 2.0 重构了这块。但 2.0 的里程碑版本 POM 是这样的:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0-M2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
拉下来看依赖树,2.0-M2 的 spring-ai-core 对 spring-context 的要求是 7.0.x。Spring AI 2.0 和 Spring Framework 7 是绑死的,而 Framework 7 意味着 Boot 4。所以「想用 Spring AI 2.0 的新特性」和「升到 Boot 4」是同一件事,不能拆。
反过来,继续留在 Spring AI 1.x 是可行的:1.x 支持 Boot 3.x,官方还在维护。我们评估下来,1.x 能满足 90% 的需求,缺的 10%(审计链路、结构化输出的流式支持)可以用自己写的 Advisor 补。
这条判断直接决定了决策口径:AI 平台的新服务上 Boot 4,存量 AI 服务留在 1.x 不动。
拿一个中等服务试跑,看看实际要改什么
我挑了「文档解析服务」做试点:1.8 万行代码,Boot 3.4 + JDK 21,依赖里有 MyBatis-Plus、Redis、Elasticsearch、RabbitMQ。这个服务复杂度中等,依赖也不算冷门,有代表性。
第一天改完 POM 编译,报了 34 个错。分一下类:
| 问题类型 | 数量 | 修复方式 | 耗时 |
|---|---|---|---|
| 第三方 starter 未适配 Boot 4 | 12 | 2 个换版本,1 个换库,9 个等升级 | 2.5 天 |
| Hibernate 7 查询行为变更 | 8 | 改 HQL / Criteria | 1 天 |
| 配置属性重命名 | 6 | 改 yml | 0.5 天 |
spring.factories 自动装配失效 | 4 | 迁移到 AutoConfiguration.imports | 0.5 天 |
| Actuator 端点路径变化 | 2 | 改监控脚本 | 0.5 天 |
| 测试相关(Mockito / 容器) | 2 | 改测试基类 | 0.5 天 |
最耗时的是第三方 starter。具体来说,我们用的动态数据源 starter 和某个内部封装的 MQ starter 都还没发 Boot 4 兼容版。这就是生态迁移里最难受的部分——你的代码可以改,别人的包你改不了。动态数据源那个最后换成了官方的 AbstractRoutingDataSource 自己实现,多花了 1.5 天;MQ starter 是内部包,我们自己发了个新版本。
还有个隐蔽的报错,排查了很久:
Caused by: java.lang.IllegalArgumentException:
Could not find class [org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration]
at org.springframework.util.ClassUtils.resolveClassName(ClassUtils.java:338)
at ...spring.factories: line 7
原因是我们一个二方库里还留着老的 META-INF/spring.factories,里面写了 Boot 2 时代的自动配置类全限定名。Boot 4 已经完全不看 spring.factories 里的 EnableAutoConfiguration 键了,但这个二方库的 jar 是通过 SpringFactoriesLoader 自己加载的,走到 ClassUtils.resolveClassName 才炸。改造方式:
// 删掉 META-INF/spring.factories,改成
// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.internal.starter.mq.MqAutoConfiguration
com.internal.starter.trace.TraceAutoConfiguration
Hibernate 7 那 8 个错里有 5 个是同一个原因:query.setFirstResult() 配合 JOIN FETCH 时的分页行为变了,Hibernate 7 不再自动在内存里去重,会直接抛异常。以前是静默处理,现在报错,其实是好事,但存量代码得改。
成本算出来是多少
试点服务总共花了 5.5 人日(含回归测试和灰度观察)。按这个基准,我把 47 个服务分了三档:
| 档位 | 服务数 | 特征 | 单服务预估 | 合计 |
|---|---|---|---|---|
| A 档:现在就升 | 9 | JDK 21 + Boot 3.4/3.5 + 依赖干净 + 需要 Spring AI 2.0 | 4~6 人日 | 约 45 人日 |
| B 档:下半年随需求升 | 17 | JDK 17 + Boot 3.x,业务还在迭代 | 5~8 人日 | 约 110 人日 |
| C 档:暂不动 | 21 | JDK 8/11,或长期无变更的稳定系统 | 15+ 人日 | 315+ 人日 |
C 档里有 6 个服务是「一年改不到两次」的老系统,运行了五六年没出过事。给它们做 Boot 4 迁移,收益接近零,风险是实打实的。我的建议很直接:这些服务不动,等到它们因为业务原因必须重写的时候再说。
全量迁移的总代价是 470+ 人日,按我们团队 8 个人算,等于两个半月的产能全押进去,这期间业务需求全停。这个账在评审会上摆出来,全量升的方案就没再通过。
顺带测的性能:没有什么惊喜
迁移的价值里,性能通常是最容易被吹的一部分。我们的实测数据比较平淡,但值得记下来,免得有人拿「性能提升」当迁移理由。
同一个服务,Boot 3.5 + JDK 21 vs Boot 4.0 + JDK 21,压测 QPS 1200 持续 20 分钟:
| 指标 | Boot 3.5 | Boot 4.0 | 变化 |
|---|---|---|---|
| 启动耗时 | 8.4s | 7.9s | -6% |
| 堆占用(稳态) | 1.42 GB | 1.39 GB | -2% |
| P99 延迟 | 186ms | 181ms | -3% |
| 吞吐(QPS) | 1,240 | 1,261 | +1.7% |
| CPU | 52% | 52% | 持平 |
基本持平。启动快了 0.5 秒是因为自动装配的索引方式变了(AutoConfiguration.imports 比扫 spring.factories 快),但这 0.5 秒对我们的长驻服务没什么意义。
真正有性能差异的是 AOT 和原生镜像那条路,但我们的服务是长驻的,用不上。我们只有一个 Serverless 函数试过 GraalVM:冷启动从 2.4 秒降到 180 毫秒,内存从 380 MB 降到 62 MB。这个数字很诱人,但它只对 Serverless 场景有意义,而且构建时间从 40 秒涨到 4 分 20 秒,CI 成本明显上升。
试点的回滚预案
做任何迁移之前都要想好怎么退回来,这次因为改动全在编译期,回滚其实很简单——改回 POM 版本号重新发布就行。但有两个地方是单向的:
AutoConfiguration.imports的改造。改完之后 Boot 3.x 也能读这个文件,所以是兼容的,可以放心改;- Hibernate 7 的查询改写。这部分改了 HQL 和 Criteria 的写法,回到 6.6 可能行为不一致。我们的做法是在改之前把每个改动点都写了对应测试,回滚时逐个验证。
灰度方式是按流量比例:先 5% 观察 24 小时,关键指标(错误率、P99、GC、业务成功率)全部正常再全量。我们在 5% 阶段发现了一个问题——Actuator 的 /health 响应结构变了一点,我们的旧监控脚本解析失败,导致健康检查误报。这类问题在 5% 流量下暴露出来,代价很小。
最终怎么定的
三条原则,写进了我们的技术雷达:
- 新服务一律 JDK 21 + Boot 4.0,从 2026 年 2 月起生效。新建服务没有历史包袱,直接用新基线,成本几乎为零;
- AI 平台相关服务优先升,因为我们确实要吃 Spring AI 2.0 的红利。这部分 9 个服务排进 Q1,两个月内完成;
- 存量服务按「是否被业务需求触碰」决定。一个服务如果今年有大的功能迭代,就在迭代里顺带升级,摊薄成本;没有迭代计划的,不动。
另外定了一条硬规则:JDK 25 暂时不作为生产基线。它在 2025 年 9 月已经是 LTS 了,虚拟线程、结构化并发、紧凑对象头这些我们都在测,但生产基线统一锁 JDK 21,等 25.0.2 之后再评估。这不是保守,是我们吃过亏——2024 年我们第一时间上了 JDK 21.0.0,被一个 G1 的 bug 咬了半个月。
就写到这。如果哪天你也被《Spring Boot 4 迁移评估:值不值得现在动》里同一个坑绊住,回来翻这篇,能省半小时。