用AI迁移老Java系统,先守住业务行为
文章讨论将Java 8、Spring 4等遗留系统迁往新版本时,AI适用的环节、确定性工具的价值与修改前提。重点提醒:编译或启动成功不能保证业务一致,缺失测试和隐含规则可能让金额舍入等变化延后暴露。
AI 深度解读
把 Java 8、Spring 4 的老系统交给 AI,要求直接升级到 Java 21、Spring Boot 3,代码可能编译通过,应用也能启动,但财务报表里的分币舍入已经变了。原文用这个假设场景提醒:升级成功,不能只看构建是否变绿。
作者给出的路线是先读懂系统、固定现有行为,再用确定性工具批量迁移,让 AI 处理剩下的复杂问题。对维护老项目的程序员,这套分工比一条「帮我全部升级」的提示词更有参考价值。
先让 AI 解释系统,别急着改代码
Java 8、Spring 4、大量 XML、Hibernate 的 hbm.xml,以及从 2017 年起就没更新的依赖,只是老系统的外在表现。真正拖住升级的是业务知识流失、测试缺位,以及没人知道一次修改会影响哪里。
因此,作者建议先让 Claude Code、Cursor 或 Copilot 的代理模式只读仓库,每次回答一个明确的问题:
模块之间怎样依赖?请求从哪个 controller 进入?取消订单会修改哪些表、发送哪些消息?Spring XML 中的 bean 怎样注入?数据库里的 stored procedure 又藏了哪些规则?
产物应该是模块文档、Mermaid 图和业务规则清单。核实后的内容写进 CLAUDE.md、AGENTS.md 或等效项目上下文文件,供后续会话复用。
这一步仍需人工核对。反射、外部配置和源码里看不到的约定,都可能让模型判断错误。作者声称代理一小时能完成相当于一周人工阅读的工作,但没有提供项目规模、测量方法或对照记录,应把它看作经验描述。
没有行为基线,编译通过也不能放心
读懂代码之后,作者最强调的是特征测试(characterization tests):记录系统现在的输出,让后续修改一旦改变输出就报警。这种方法也叫 golden master。
它暂时不回答业务规则是否正确,只回答升级前后是否一致。
下面的测试用脱敏后的生产订单计算运费,并与旧代码生成的基准比较:
class CalculoFreteCaracterizacaoTest {
// 从生产环境取得并完成匿名化的真实输入。
static Stream<Pedido> pedidos() {
return CarregadorFixture.ler("fixtures/pedidos.json");
}
@ParameterizedTest
@MethodSource("pedidos")
void freteContinuaIgual(Pedido pedido) {
BigDecimal frete = new CalculoFreteService().calcular(pedido);
// 与首次运行旧代码时记录的值比较。
assertEquals(Golden.valor("frete", pedido.getId()), frete.toPlainString());
}
}
重点是 toPlainString():这里比较的是字符串,能够捕获 1.0 和 1.00 这样的尺度差异;直接使用 BigDecimal.equals 也会区分尺度。不过,Golden 和 CarregadorFixture 的实现没有给出,这段代码展示的是测试思路,不能直接当完整工程运行。
AI 可以辅助找分支、构造输入、批量生成测试,但必须明确禁止它顺手修复业务逻辑。所有测试都应先在旧代码上通过,再开始迁移。否则,基准本身就不可信。
特征测试保存的是现有行为,其中也可能包括现有缺陷。
尤其要覆盖金额、时间、编码和集合顺序。作者列出的典型风险包括:改写 equals、hashCode,从 Date 换到 java.time 时改变时区,以及无意依赖 HashMap、HashSet 的迭代顺序。Java 18 起默认字符集统一为 UTF-8,原先在 Windows 上按 Cp1252 工作的 new String(bytes),也可能读出不同结果。
批量改名交给 OpenRewrite,复杂判断再交给 AI
把 400 个文件中的 javax.persistence 换成 jakarta.persistence,主要需要一致性。作者建议用基于抽象语法树(AST)和类型信息的 OpenRewrite,而不是让模型逐文件自由改写。
原文配置涉及 Maven 插件 org.openrewrite.maven:rewrite-maven-plugin,配方依赖为 org.openrewrite.recipe:rewrite-migrate-java,启用:
org.openrewrite.java.migrate.UpgradeToJava21org.openrewrite.java.migrate.jakarta.JavaxToJakarta
插件和依赖的具体版本没有填写,只留了「当前版本」的占位说明,不能照抄成可运行配置。Spring Boot 升级则使用 rewrite-spring 中的 UpgradeSpringBoot_3_x 配方族,通过 mvn rewrite:discover 查看已安装版本实际提供哪些配方。
下面两条命令分别用于预览和执行迁移:
mvn rewrite:dryRun # 在 target/rewrite/ 生成补丁,不修改源码
mvn rewrite:run # 应用修改
先看第一行生成的补丁,再决定如何拆分迁移。工具行为可重复,不代表补丁天然符合业务要求。
版本升级也应分步进行:Java 8 到 11 要处理 JAXB、JAX-WS、CORBA 模块移出 JDK 的影响;11 到 17 要关注 JDK 内部 API 的强封装对反射调用的影响;17 到 21 在作者经验里通常较平稳。每一步都运行特征测试,问题才容易定位。
原文还提到 Amazon Q Developer,称其可把 Java 8、11、17 项目升级到 17 或 21,并要求项目能通过 Maven 构建,Ant、Gradle 项目不在该路径内。这里没有提供产品版本或验证日期,选型时需要重新核实。
升级稳定之后,再考虑把代码写得更现代
作者估计配方能覆盖 70% 到 80% 的工作,其余部分再交给 AI:替换停留在 2016 年的废弃 PDF 库、分析编译和运行时异常、查找藏在字符串里的反射引用,以及处理没有直接替代方案的旧 API。
例如 Nashorn 在 Java 15 被移除;SecurityManager 在 Java 17 被弃用,到 Java 24 被永久禁用。这些问题需要重新设计使用方式,不能靠机械替换解决。
代理的任务应缩小到「让计费模块在 Java 17 编译通过」,然后运行构建和测试、人工审查 diff、提交,再处理下一项。作者认为,三千行的 AI diff 很容易被直接批准,真正的审查反而消失了。
迁移稳定后,才进入结构改进。原文展示了把类似 2012 年写法的 JDBC DAO,改成 record 配合 Spring 6.1 的 JdbcClient,通过 sql(...).query(Cliente.class).list() 返回结果。但 record 不可变,若调用方还在执行 setNome,就不能直接替换,必须先检查全部使用位置。
同样,pattern matching、sealed 适合特定类型结构,virtual threads 适合大量阻塞 I/O;存在这些能力,不等于每个模块都该采用。拆分单体则沿用 strangler fig,让新模块逐步接管流量,最后移除旧实现。AI 可以分析类和表的关联,模块边界仍取决于团队职责、部署方式和业务归属。
我的判断
我认同这套顺序:先建立可观察的行为基线,再推进升级。它把 AI 擅长的阅读、解释、局部重写,与工具擅长的重复变换分开,比直接要求代理完成整仓迁移更容易审查。
但原文是一套经验方法,没有展示完整迁移案例、失败率或工时对照。「模型正确率 98%、剩下 2% 出错」「AST 工具正确率 100%」也没有实验支撑。确定性工具的优势是结果更可重复、修改范围更明确,配方覆盖不足、类型解析失败或运行时配置遗漏,仍然需要处理。
特征测试也不是完整保证。它能保护已覆盖输入对应的输出,却未必捕获消息发送、事务边界和数据库写入等副作用。原文开头提醒要追踪这些影响,后面却没有给出相应测试实现,这是落地时需要补齐的部分。
此外,涉及公司源码时,作者要求核对外部 API 的数据保留、训练使用条款,以及个人数据相关的 LGPD 要求。国内团队应按自己的合同、制度和适用要求处理,不能直接套用文中的法域背景。
能拿来做什么
- 先让代理只读分析一个模块,整理入口、依赖、数据库操作和副作用;人工确认后,再写入 AGENTS.md 或团队现有上下文文件。
- 挑出最不能出错的五条业务流程,用脱敏真实输入建立特征测试,并确认它们在旧版本全部通过;涉及写库、消息或外部调用时,要一并核对副作用。
- 对 Java 项目先运行
mvn rewrite:dryRun评估补丁,再按版本跨度逐步迁移,把 AI 任务限定在能够完整审查的范围。 - 面对前端或 Node 老项目,也可以借用「先记录行为、再分步升级」的流程;具体迁移工具需要另选,原文的 OpenRewrite 配方不能直接照搬。