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

张开发
2026/4/14 16:11:48 15 分钟阅读

分享文章

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 TreeLST在树上做精确变换再把结果写回代码——全程保留原有的格式、注释和空白。这是一个关键细节。普通的文本替换工具不理解代码语义正则表达式不知道import javax.persistence.Entity和字符串里写的javax.persistence.Entity有什么区别。OpenRewrite 的 LST 是类型感知的它知道每个标识符解析到哪个类、哪个方法所以它的变更是精确的。变更逻辑封装在Recipe里。Recipe 可以是声明式的YAML 配置也可以是命令式的Java 实现它通过Visitor 模式遍历 LST找到匹配的节点施加变换。官方提供了数百个开箱即用的 Recipe覆盖 Spring、JUnit、Lombok、Mockito 等主流框架的迁移场景。你也可以写自己的。如果你想深入了解这套机制我此前写过一个系列OpenRewrite 基础与原理无损语义树LST详解Recipe 与 Visitor 实现用 JavaTemplate 创建复杂 LST这篇文章不会重复这些内容而是聚焦在一个实际问题上拿它来做 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它就变成了可执行的文档不仅记录了「应该怎么改」还能直接帮你改。这次迁移里我们把httpclient5与httpcore5的版本对齐问题写成了一个自定义 Recipetype: 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。整个过程分两步。第一步跑官方 RecipeOpenRewrite 的rewrite-spring模块提供了UpgradeSpringBoot_3_5Recipe它是一个组合 Recipe内部编排了数十个子 Recipe覆盖了绝大多数迁移场景。一条命令mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \ -Drewrite.recipeArtifactCoordinatesorg.openrewrite.recipe:rewrite-spring:LATEST \ -Drewrite.activeRecipesorg.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 \ -pl web-app运行完成后13 个文件被修改。主要变更包括javax.*全面换成jakarta.*——javax.persistence、javax.validation、javax.servlet里的每一处 import一个不漏。WebSecurityConfigurerAdapter被移除Security 配置改为直接注入SecurityFilterChainBean。WebMvcConfigurerAdapter、HandlerInterceptorAdapter、AsyncConfigurerSupport这些废弃的适配器类全部替换为对应的接口实现。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 installBUILD 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-toolkit 的v2分支里。任何项目需要做同样的迁移把这个模块引入、跑两条命令就够了。它不需要懂 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.54.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。

更多文章