Spring Boot 2 升 3:两条命令搞定 95%,AI 收尾

Spring Boot 2 升 3:两条命令搞定 95%,AI 收尾

一年多前,我在另一个迁移项目里尝试过 OpenRewrite,做了可行性验证,最终评估下来方案不合适,那个项目转而采用了 AI 的方式。顺着那次探索写了个系列,然后就搁置了。最近真正的迁移计划提上日程,翻出来一看——当时踩过的坑、记下来的东西,全都用上了。

任何学习和痛苦都不会白费,只是兑现的时间不一定。

为什么迁移这么痛

Spring Boot 2 的 EOL 早已过去,但很多团队的升级计划还停在 Jira 的某个角落吃灰。不是不想升,是真的怕。

怕什么?怕的不是技术本身,而是规模。javax.* 全部变成 jakarta.*,Spring Security 配置 API 重写,WebMVC 和异步配置的适配器类被移除,JUnit 4 注解也换了一套。任何一个变化单独处理都不难,但加在一起,乘以整个代码库的文件数量,就成了一个让人望而却步的工程量——而且还是那种高度重复、极易出错、回归验证代价极高的工作。

这类变更有一个共同特征:规则清晰,但执行枯燥,且不能出错。每一处 javax.persistence.Entity 都应该改成 jakarta.persistence.Entity,没有例外。这种工作本质上不需要人来做判断,它需要的是一种可以精确、批量、可审计地修改代码的机制。

OpenRewrite 就是为此而生的。

OpenRewrite 是什么

OpenRewrite 是一个专为大规模代码变更设计的自动化重构引擎,最初由 Netflix 内部孵化,现在由 Moderne 维护。它的核心思想是:把代码解析成一棵无损语义树(Lossless Semantic Tree,LST),在树上做精确变换,再把结果写回代码——全程保留原有的格式、注释和空白。

这是一个关键细节。普通的文本替换工具不理解代码语义,正则表达式不知道 import javax.persistence.Entity 和字符串里写的 "javax.persistence.Entity" 有什么区别。OpenRewrite 的 LST 是类型感知的,它知道每个标识符解析到哪个类、哪个方法,所以它的变更是精确的。

变更逻辑封装在 Recipe 里。Recipe 可以是声明式的(YAML 配置),也可以是命令式的(Java 实现),它通过 Visitor 模式遍历 LST,找到匹配的节点,施加变换。官方提供了数百个开箱即用的 Recipe,覆盖 Spring、JUnit、Lombok、Mockito 等主流框架的迁移场景。你也可以写自己的。

如果你想深入了解这套机制,我此前写过一个系列:

这篇文章不会重复这些内容,而是聚焦在一个实际问题上:拿它来做 Spring Boot 迁移,体验如何

但在开始之前,有个问题值得先回答。

既然有 AI,为什么还需要 OpenRewrite

这是个合理的质疑。现在的 AI coding assistant 能读懂代码、理解迁移规则,你把文件丢给它,告诉它 " 把所有 javax.persistence 换成 jakarta.persistence",它能做到。那为什么还需要专门的工具?

问题在于,AI 的输出是概率性的,而代码迁移需要的是确定性

你把 20 个文件交给 AI 处理,它改对了 18 个,第 19 个它觉得这里的 import 顺序也可以顺手调整一下,第 20 个它误判了某个同名类的包路径。这些都是真实会发生的情况。更麻烦的是,你很难事先知道它在哪里 " 发挥了创意 “——你必须逐文件做 diff 审查,工作量不比手动改少多少。

还有一个问题是规模。一个中等规模的 Spring Boot 项目可能有几百个 Java 文件,涉及 javax.* 的 import 可能分布在其中的大半。让 AI 一次处理完整个代码库,超出上下文窗口;分批处理,需要人工协调,还面临批次间不一致的风险。

OpenRewrite 在这两点上恰好是 AI 的对立面:同一个 Recipe,在任意规模的代码库上运行,结果完全一致、完全可预测。它不会 " 顺手 " 做额外修改,不会跳过某个文件,不会因为代码风格不统一而行为不同。每次运行产生的 diff 可以被审查、被 code review、被版本控制追踪。

这不是说 AI 没有用武之地。恰恰相反——在这次迁移里,有两处问题是 OpenRewrite 处理不了的,最终还是靠 AI 辅助解决的。后面会讲到。

AI 擅长处理语义复杂、需要判断的变更;OpenRewrite 擅长处理规则明确、需要精确执行的变更。迁移工作里两类都有,用对工具才是关键。

Recipe-First 是什么

Recipe-First 不是一个官方概念,而是一种做事方式的选择:当你发现代码需要变更时,第一反应不是打开编辑器,而是问「能不能写成 Recipe」

这个区别听起来微妙,但影响很深远。

手动改代码,改完就结束了。这次迁移积累的知识——哪些 API 变了、怎么替换、有哪些坑——存在于开发者的记忆里,不可转移,下次换一个项目还得重来。而把这些知识写成 Recipe,它就变成了可执行的文档:不仅记录了「应该怎么改」,还能直接帮你改。

这次迁移里,我们把 httpclient5httpcore5 的版本对齐问题写成了一个自定义 Recipe:

type: specs.openrewrite.org/v1beta/recipe
name: com.example.FixHttpComponents5VersionMismatch
displayName: Fix HttpComponents 5 version mismatch
recipeList:
  - org.openrewrite.maven.ChangePropertyValue:
      key: httpcore5.version
      newValue: 5.3.4
      addIfMissing: true

这个 Recipe 解决的问题很具体:httpclient5 5.4.x 依赖 httpcore5 5.3.x,但很多项目里 httpcore5.version 被锁定在了旧版本,导致运行时 NoSuchMethodError。如果靠手动排查,每次遇到这个问题都要重新走一遍诊断流程。而有了这个 Recipe,下一个遇到同样问题的项目,一条命令解决。

内部公共库的 API 迁移也是同样的思路。ApiResponse.success() 改名为 ApiResult.ok()@RequireLogin 换成 @Authenticated——这些变更写成 Recipe 之后,不再需要有人去记「内部库 v2 的变更清单」,Recipe 本身就是那份清单,而且是可运行的。

Recipe-First 的本质是把迁移知识资产化

一次完整迁移过程

迁移的起点是一个标准的 Spring Boot 2.7.18 项目,包含 REST API、Spring Security 配置、异步处理、HttpClient 封装,以及一批 JUnit 4 测试。目标是升级到 Spring Boot 3.5.0。

整个过程分两步。

第一步:跑官方 Recipe

OpenRewrite 的 rewrite-spring 模块提供了 UpgradeSpringBoot_3_5 Recipe,它是一个组合 Recipe,内部编排了数十个子 Recipe,覆盖了绝大多数迁移场景。一条命令:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:LATEST \
  -Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 \
  -pl web-app

运行完成后,13 个文件被修改。主要变更包括:

javax.* 全面换成 jakarta.*——javax.persistencejavax.validationjavax.servlet 里的每一处 import,一个不漏。WebSecurityConfigurerAdapter 被移除,Security 配置改为直接注入 SecurityFilterChain Bean。WebMvcConfigurerAdapterHandlerInterceptorAdapterAsyncConfigurerSupport 这些废弃的适配器类全部替换为对应的接口实现。JUnit 4 的 @RunWith(SpringRunner.class) 换成 @ExtendWith(SpringExtension.class)org.junit.Test 换成 org.junit.jupiter.api.Test,断言类也一并迁移到 JUnit 5 的 Assertions。HttpClient 4 的 API 迁移到 HttpClient 5。ResponseStatusException.getStatus() 换成 getStatusCode()

第二步:跑自定义 Recipe

官方 Recipe 处理不了项目特有的问题。先构建自定义 Recipe 模块,再运行:

mvn install -pl migration-recipes -q
mvn org.openrewrite.maven:rewrite-maven-plugin:6.12.0:run -pl web-app

这一步完成了两件事:httpcore5 版本对齐(避免运行时 NoSuchMethodError),以及内部公共库 v1 到 v2 的 API 迁移。

两步跑完,mvn clean install,BUILD SUCCESS,测试全部通过,/actuator/health 返回 {"status":"UP"}

当然,这中间还有两处需要手动处理的问题。

Recipe 搞不定的地方

两处手动修复,都有一个共同原因:变更涉及的不是语法替换,而是 API 行为的根本改变,没有机械化的对应规则可以套用。

第一处:HttpClient 5 移除了超时配置方法

RestTemplateConfig 里原本有这样的代码:

httpClient.setConnectTimeout(5000);
httpClient.setReadTimeout(30000);

HttpClient 5 把这些方法彻底移除了,不是改名,是消失了。超时配置的方式变成了通过 RequestConfig 对象统一管理:

RequestConfig requestConfig = RequestConfig.custom()
    .setConnectTimeout(Timeout.ofSeconds(5))
    .setResponseTimeout(Timeout.ofSeconds(30))
    .setConnectionRequestTimeout(Timeout.ofSeconds(10))
    .build();

CloseableHttpClient httpClient = HttpClients.custom()
    .setDefaultRequestConfig(requestConfig)
    .build();

这种变更 Recipe 处理不了——它需要理解旧代码的意图,然后用完全不同的结构重新表达。这是 AI 擅长的事,给它看旧代码和新 API 文档,它能正确完成这个转换。

第二处:@Override 不再成立

OpenRewrite 迁移完成后,SecurityConfig 里的 userDetailsService() 方法上出现了一个编译错误:@Override 无效,因为 Spring Boot 3 里这个方法不再来自任何父类,它已经是一个普通的 @Bean 方法。删掉那个 @Override 即可。

这个问题理论上可以写成 Recipe,但它边界模糊——不是所有 @Override 都该删,只有这个特定的方法在特定的迁移上下文里需要删。OpenRewrite 可以精确操作 LST,但告诉它「只删这种情况下的 @Override」,Recipe 本身就变得比直接删更复杂了。

这两处加在一起,不到 10 分钟。放在整个迁移工程量的比例里,是噪音级别的。

这也说明了一个问题:Recipe 的边界不是工具的缺陷,而是合理的分工。规则明确的变更,交给 Recipe;需要语义判断的,交给人或 AI。迁移工作里两类都有,但比例差异悬殊——Recipe 处理了 95%,手动处理了剩下的 5%。

Recipe 是资产,不是脚本

脚本是一次性的。它解决了眼前的问题,跑完就归档,下次遇到类似的问题,你得重新写或者重新找。Recipe 不一样——它是可复用的迁移知识,随着时间积累,会成为团队真正有价值的工程资产。

这次整理出来的 Recipe 放在 spring-boot-migration-toolkitv2 分支里。任何项目需要做同样的迁移,把这个模块引入、跑两条命令就够了。它不需要懂 OpenRewrite 内部机制,不需要查迁移文档,不需要团队里有人曾经踩过那些坑——因为踩坑的经验已经固化在 Recipe 里了。

更重要的是,Recipe 有一个脚本和 AI 都做不到的特性:可验证性。你可以先用 rewrite:dryRun 看它会改什么,再决定要不要真的执行。执行之后产生的是干净的 git diff,可以被 code review,可以被 CI 门禁拦截,可以被回滚。整个变更链路是透明的、可审计的。

顺便一提:之前写过一篇 用 OpenRewrite 把传统 Spring REST 服务一键转换成 MCP Server 的文章,Recipe-First 的思路在那里也同样适用——只要变更规则是确定的,就可以被 Recipe 化,就可以被重复使用。

这个逻辑不局限于 Spring Boot 升级。任何规模化的代码变更——内部框架升级、API 废弃替换、安全规范落地——都可以用这个方式来做。把变更知识写成 Recipe,把 Recipe 当作代码一样管理,这才是 OpenRewrite 想让你做的事。

现在升到哪个版本合适

既然要升,就升到一个值得的版本。Spring Boot 各版本的社区支持周期如下(数据来自 endoflife.date):

版本发布日期社区支持截止
3.32024-052025-06 ⚠️ 已过期
3.42024-112025-12 ⚠️ 已过期
3.52025-052026-06 ← 推荐
4.02025-112026-12

注:各版本均有 Broadcom 商业付费的扩展支持(Extended Support),到期后可继续获得安全补丁,但需购买 Spring 商业许可证,不在本文讨论范围内。

结论:先升到 3.5,4.0 留作下一步

  • 3.3 / 3.4 社区支持已过期,跳过
  • 3.5 是 3.x 系列最新稳定版,社区支持到 2026-06,迁移成本低
  • 4.0 虽然社区支持到 2026-12,但引入了更多 breaking changes,从 2.x 直接跳风险更高——先落地 3.5,再升 4.0 是更稳的两步走策略

本文所有示例均已基于 Spring Boot 3.5.0 验证,直接使用即可。

结语

Spring Boot 2 → 3 的迁移,本质上是一个规模化变更问题,而不是一个技术难题。大多数变更的规则是明确的,只是数量多、分布散、人工处理容易出错。

OpenRewrite 解决的正是这个问题。官方 Recipe 处理框架层面的变更,自定义 Recipe 处理项目特有的问题,少数需要语义判断的地方留给人或 AI。这次迁移里,两步命令完成了 95% 的工作,剩下的不到 10 分钟手动处理。

如果你还没开始迁移,现在是个好时机。如果你已经在手动改代码,可以停下来想想:这个改法,能不能写成 Recipe。

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus