AI助力JDK8到21迁移实战

📅 2026/7/23 3:57:50 👁️ 阅读次数 📝 编程学习
AI助力JDK8到21迁移实战

2026 年 9 月,Oracle JDK 8 的 NFTC(免费使用条款)将到期。而 Oracle JDK 8 的公共更新早在 2019 年 1 月就已停止。

不管从哪个时间点看,还在 JDK 8 上的企业都不得不面对一个现实:迁移到 21 不是"要不要"的问题,而是"什么时候做"的问题。

但 JDK 8 到 21,中间隔了 13 个版本、10 年的语言演进。一个中型项目几百万行代码,纯手工迁移,测试排期、兼容性问题、行为变更——想想就头大。

所以很自然的一个问题:大模型能不能帮上忙?

我用一个真实的 Java 8 项目做了实验,让大模型辅助做 JDK 21 迁移。下面是完整的过程和结果。


迁移的核心工作量

JDK 8 → 21 的迁移,工作量大致分三层:

第一层:语法升级(简单,可自动化)

  • • 匿名内部类 → Lambda 表达式

  • • 传统 for 循环 → Stream API

  • • 字符串拼接 → Text Blocks

  • • switch 语句 → switch 表达式

  • • 普通类 → Records

  • Optional.isPresent() + get()ifPresent()/orElse()

第二层:API 替换(中等,大部分能自动)

  • java.util.Date/Calendarjava.time.LocalDateTime/Instant

  • • JAXB 依赖外置(javax.xml.bind在 JDK 11 被移除,需添加外部依赖)

  • javaxjakarta命名空间迁移(Jakarta EE 9+ 的包名变更,与 JDK 升级配套处理)

第三层:行为变更(高风险,需要人工判断)

  • • 虚拟线程兼容性(synchronized在虚拟线程中的 pinning 问题,JDK 25 已修复)

  • ThreadLocal在虚拟线程下的表现变化

  • • GC 参数变化(CMS 已被移除,需要切换到 G1/ZGC)

  • • 某些sun.misc包下的内部 API 已被移除

  • • 模块系统(JPMS)强封装:反射访问受限,需要--add-opens/--add-exports

  • • Security Manager 废弃与禁用(JDK 17 标记废弃,JDK 24 永久禁用)

大模型在第一层做得很好,第二层做得还行,第三层经常翻车。下面具体看。


第一层:语法升级——AI 的舒适区

我喂给 Claude Code 一段典型的 JDK 8 代码:

// 原始 JDK 8 代码
public class OrderProcessor {

public List<String> getActiveUserEmails(List<User> users) {
List<String> emails = new ArrayList<>();
for (User user : users) {
if (user.getStatus() == UserStatus.ACTIVE) {
String email = user.getEmail();
if (email != null && !email.isEmpty()) {
emails.add(email.toLowerCase());
}
}
}
Collections.sort(emails);
return emails;
}

public Optional<User> findUser(List<User> users, Long id) {
for (User user : users) {
if (user.getId().equals(id)) {
return Optional.of(user);
}
}
return Optional.empty();
}

public String formatReport(String title, List<String> items) {
StringBuilder sb = new StringBuilder();
sb.append("======== " + title + " ========\n");
for (int i = 0; i < items.size(); i++) {
sb.append((i + 1) + ". " + items.get(i) + "\n");
}
return sb.toString();
}
}

AI 输出:

// AI 迁移后的 JDK 21 代码
public class OrderProcessor {

public List<String> getActiveUserEmails(List<User> users) {
return users.stream()
.filter(u -> u.getStatus() == UserStatus.ACTIVE)
.map(User::getEmail)
.filter(email -> email != null && !email.isEmpty())
.map(String::toLowerCase)
.sorted()
.toList(); // JDK 16+ 直接返回不可变 List
}

public Optional<User> findUser(List<User> users, Long id) {
return users.stream()
.filter(u -> u.getId().equals(id))
.findFirst();
}

public String formatReport(String title, List<String> items) {
return STR."""
======== \{title} ========
\{IntStream.range(0, items.size())
.mapToObj(i -> STR."\{i + 1}. \{items.get(i)}")
.collect(Collectors.joining("\n"))}
""";
}
}

评价:令人满意。

Stream API、方法引用、toList()、Text Blocks——AI 全部正确应用。

唯一要注意的是字符串模板(STR."...")。这个特性在 JDK 21 中只是首次预览(JEP 459),JDK 22 进入第二次预览,JDK 23 被撤回,至今仍未成为正式特性。所以这段代码在 JDK 21 上必须加--enable-preview才能运行,而且即使能跑,API 在后续版本中也发生了变化。稳妥的做法是先用 Text Blocks 替代字符串拼接,字符串模板等它正式定型再迁移。

第一层迁移,AI 几乎是完美的。手动做的话,这些代码可能要改 20 分钟,AI 20 秒搞定。


第二层:API 替换——有好有坏

第二层就有点意思了。

我给了 AI 一段包含java.util.DateSimpleDateFormat的代码:

// 原始 JDK 8 代码
public Date parseDate(String dateStr) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
return sdf.parse(dateStr);
} catch (ParseException e) {
throw new RuntimeException("日期解析失败: " + dateStr, e);
}
}

public long daysBetween(Date start, Date end) {
long diff = end.getTime() - start.getTime();
return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
}

AI 迁移为:

// AI 迁移后的代码
public LocalDateTime parseDate(String dateStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
try {
return LocalDateTime.parse(dateStr, formatter);
} catch (DateTimeParseException e) {
throw new RuntimeException("日期解析失败: " + dateStr, e);
}
}

public long daysBetween(LocalDateTime start, LocalDateTime end) {
return Duration.between(start, end).toDays();
}

评价:语法正确,但要注意返回值变更。

DateLocalDateTime本身没问题。但调用方如果之前拿Date去和java.sql.Date交互,或者用了Dateafter()/before()方法,迁移后需要联动修改。这是 AI 看不到的——它只改了当前文件,不知道调用方怎么用这个返回值。

这就是第二层迁移的真正痛点:API 替换不是简单的搜索替换,它涉及接口契约的变化。如果你返回类型从Date变成了LocalDateTime,所有调用方都得改。

AI 迁移第二层的安全做法是:告诉它"保留公开方法的签名不变,内部实现改为 java.time"。

// 更好的迁移:保持签名,改内部实现
public Date parseDate(String dateStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.parse(dateStr, formatter);
return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
}

这样减少连锁改动,可以分批迁移。

另外还有一个容易忽略的边界问题:Duration.between(LocalDateTime, LocalDateTime).toDays()和原来的TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)在大多数情况下结果一致,但LocalDateTime不感知时区和 DST(夏令时)切换。如果原始代码涉及跨时区或夏令时的日期计算,迁移后需要额外验证。


第三层:行为变更——AI 翻车的地方

第三层涉及 JDK 内部行为的变化,AI 的表现开始不稳定。

我给它一段使用了ThreadLocal的代码:

// 原始代码
public class RequestContextFilter implements Filter {

private static final ThreadLocal<RequestContext> contextHolder =
new ThreadLocal<>();

@Override
public void doFilter(ServletRequest request, ...) {
RequestContext ctx = buildContext(request);
contextHolder.set(ctx);
try {
chain.doFilter(request, response);
} finally {
contextHolder.remove();
}
}

public static RequestContext getContext() {
return contextHolder.get();
}
}

我问 AI:"这段代码迁移到 JDK 21 后,在虚拟线程环境下有什么问题?"

AI 的回答:

"这段代码在虚拟线程下可能有问题,因为虚拟线程是 JVM 管理的协作式调度,ThreadLocal 在虚拟线程中的行为与传统线程一致,但在使用虚拟线程池时需要注意 ThreadLocal 的清理。"

这个回答只对了一半。

真实的问题是:虚拟线程支持数以万计的并发,但 ThreadLocal 的内存开销是按线程算的。如果用传统线程池(如 Tomcat 的 200 线程池),200 个 ThreadLocal 实例可以接受。但如果用虚拟线程(可以创建 10 万个),ThreadLocal 的内存和 GC 压力会急剧上升。ThreadLocal 在虚拟线程中仍然可用,但建议改用ScopedValue(JDK 20+预览)。

AI 没有主动提到ScopedValue,也没有量化 ThreadLocal 在大量虚拟线程下的内存问题。如果你完全相信 AI 的迁移建议,上线后可能会遇到意外的性能问题。

第三层的 AI 输出只能作为提醒,不能作为决策依据。


实操建议:AI 辅助迁移的工作流

这次实验下来,我觉得最优的迁移方式不是"让 AI 全量迁移",也不是"完全不用 AI",而是这样:

第一步:AI 做语法扫描

用 AI 扫描整个项目,定位需要迁移的位置,输出一个清单:

需要升级的语法模式:
- 匿名内部类 → Lambda:237 处
- for 循环 → Stream:108 处
- switch 语句 → switch 表达式:45 处
- 字符串拼接 → Text Blocks:62 处

需要替换的 API:
- java.util.Date:89 处
- SimpleDateFormat:34 处
- javax.xml.bind:12 处
- ThreadLocal:16 处(需检查虚拟线程兼容性)

这个清单帮你评估工作量,而不是猜。

第二步:AI 做批量语法迁移

第一层语法升级可以直接让 AI 批量处理。用脚本或 AI 工具对整个模块做一次性替换,然后走代码审查。

注意:每次只提交一个包或一个模块的变更,不要一个 PR 改几百个文件。

第三步:API 替换按模块推进

第二层的 API 替换建议按模块推进。每个模块在独立的分支上做迁移,做完后充分测试,再合并到主分支。

第四步:行为变更做专项审查

第三层的问题需要人工列一个清单,逐项检查:

  • • 是否有synchronized在可能被虚拟线程调用的路径上(JDK 25 已修复 pinning,但 JDK 21 仍需关注)

  • • 是否有 CMS GC 参数需要替换为 G1/ZGC

  • • 是否有sun.misc.*的调用

  • • 是否有反射相关的setAccessible需要调整,是否需要添加--add-opens/--add-exportsJVM 参数

  • • ThreadLocal 的使用是否需要替换为 ScopedValue

  • • 是否有SecurityManager的使用(JDK 24 已永久禁用)

把这些交给 AI 去"查",让它在代码库里辅助定位这些风险点,再做人工决策。


附:迁移工具链

除了大模型,JDK 迁移还有一些专门工具值得用上:

工具

用途

说明

jdeps

分析模块依赖关系

JDK 自带,扫描代码对内部 API 的依赖

jdeprscan

扫描已废弃 API 的使用

JDK 自带,定位需要替换的废弃方法

IntelliJ IDEA 迁移助手

IDE 内置检测

升级 JDK 版本后自动提示不兼容的代码

OpenJDK Migration Guide

官方迁移指南

每个版本的 JDK Release Notes 都有 "Migration" 章节

Claude Code / Cursor

AI 辅助批量迁移

适合第一层语法升级和第二层 API 替换

建议的组合方式:先用jdeprscan+jdeps做静态扫描,拿到风险清单;再用 AI 工具做批量语法迁移;最后人工审查第三层行为变更。


总结

迁移层次

AI 表现

是否需要人工

语法升级

优秀

走代码审查即可

API 替换

良好

需要检查接口兼容性

行为变更

不稳定

必须人工判断

JDK 8→21 迁移这件事,AI 最好的角色是"高级助手"而不是"主力"。它能显著加速第一层和第二层,让你从"这个项目迁移要三个月"变成"语法升级两周搞定,剩下时间做兼容测试"。

最难的部分从来不是改代码,而是确保改了之后系统还能正常跑。

所以别让 AI 全自动跑一个几百万行的项目。找个模块,让它处理第一层和第二层,你跑一遍测试,看看效果——这样逐步建立信任,再铺开到整个项目