一、引言
在上一篇文章《Java 21 生产迁移实战 ①》中,我们讨论了为什么要迁移到 Java 21。
今天,我们进入实战阶段——废弃 API 的替换与兼容性处理。
Java 21 中标记为废弃(Deprecated)的 API 数量不少,但真正需要立即处理的只有几类。本文将逐类列出这些必改的废弃 API,提供具体的代码替换方案,并介绍两个强大的自动化工具:Maven modernizer 插件和 OpenRewrite,帮助你快速完成迁移。
二、迁移准备工作
2.1 环境要求
| 组件 | 版本要求 |
|---|---|
| Java | 21 |
| Maven | 3.8+ |
| Spring Boot | 3.2+(如果使用) |
2.2 迁移策略
迁移策略:
┌─────────────────────────────────────────────────────┐
│ │
│ 1. 自动扫描(modernizer) │
│ ↓ │
│ 2. 批量修复(OpenRewrite) │
│ ↓ │
│ 3. 手动修复(剩余问题) │
│ ↓ │
│ 4. 测试验证 │
│ ↓ │
│ 5. 灰度发布 │
│ │
└─────────────────────────────────────────────────────┘
三、废弃 API 替换清单
3.1 第一类:java.security.AccessController
废弃原因:Java 17+ 引入了更简洁的安全模型,AccessController 已不再需要。
替换方案:直接调用目标方法,不再使用 AccessController.doPrivileged()。
代码对比:
// ❌ 旧代码:使用 AccessController
import java.security.AccessController;
import java.security.PrivilegedAction;
String result = AccessController.doPrivileged(
(PrivilegedAction<String>) () -> {
// 需要权限的操作
return System.getProperty("user.home");
}
);
// ✅ 新代码:直接调用
String result = System.getProperty("user.home");
批量替换模式:
// 模式匹配:AccessController.doPrivileged
AccessController.doPrivileged\((PrivilegedAction<[^>]+>) \(\) -> \{([^}]+)\}\)
// 替换为:直接执行 Lambda 体内容
// 提取 $2 中的代码
3.2 第二类:Thread.stop/resume/suspend
废弃原因:这些方法是不安全的,可能导致死锁、数据损坏等问题。
替换方案:使用正确的中断/取消模式。
⚠️ 重要提示:这不是用虚拟线程替代!虚拟线程是并发模型的升级,与线程停止机制无关。正确的做法是使用
Thread.interrupt()或CompletableFuture的取消机制。
代码对比:
// ❌ 旧代码:使用 Thread.stop()
Thread worker = new Thread(() -> {
while (true) {
// 执行任务
}
});
worker.start();
// 强行停止线程(不安全)
worker.stop();
// ✅ 新代码:使用中断机制
class Worker implements Runnable {
private volatile boolean running = true;
@Override
public void run() {
while (running && !Thread.currentThread().isInterrupted()) {
try {
// 执行任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
running = false;
}
}
}
public void stop() {
running = false;
}
}
Worker worker = new Worker();
Thread thread = new Thread(worker);
thread.start();
// 安全停止
worker.stop();
thread.interrupt();
使用 ExecutorService 的方案:
// ✅ 使用 ExecutorService 管理线程
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
});
// 取消任务(会中断线程)
future.cancel(true);
// 关闭执行器
executor.shutdownNow();
使用 CompletableFuture 的方案:
// ✅ 使用 CompletableFuture + 取消令牌
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
}
return "result";
});
// 取消任务
future.cancel(true);
3.3 第三类:URL 构造函数
废弃原因:URL 构造函数不进行严格的语法检查,可能导致运行时错误。
替换方案:使用 URI.create(str).toURL() 或 new URI(str).toURL()。
代码对比:
// ❌ 旧代码:使用 URL 构造函数
import java.net.URL;
// 可能抛出 MalformedURLException,但不检查所有语法错误
URL url = new URL("https://example.com/path?query=value");
// ✅ 新代码:使用 URI.toURL()
import java.net.URI;
import java.net.URL;
// URI 会进行严格的语法检查
URI uri = URI.create("https://example.com/path?query=value");
URL url = uri.toURL();
// 或者使用 try-catch
try {
URL url2 = new URI("https://example.com/path?query=value").toURL();
} catch (Exception e) {
// 处理异常
}
批量替换模式:
// 模式匹配:new URL("...")
new URL\("([^"]+)"\)
// 替换为:URI.create("$1").toURL()
URI.create("$1").toURL()
3.4 第四类:Finalizer
废弃原因:Finalizer 机制不可靠,可能导致内存泄漏、性能问题等。
替换方案:使用 java.lang.ref.Cleaner 或 try-with-resources。
代码对比:
// ❌ 旧代码:使用 finalize() 方法
public class ResourceHolder {
private NativeResource nativeResource;
public ResourceHolder() {
nativeResource = new NativeResource();
}
@Override
protected void finalize() throws Throwable {
try {
nativeResource.release();
} finally {
super.finalize();
}
}
}
// ✅ 新代码:使用 Cleaner
import java.lang.ref.Cleaner;
public class ResourceHolder implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private NativeResource nativeResource;
public ResourceHolder() {
nativeResource = new NativeResource();
cleanable = cleaner.register(this, new ResourceCleaner(nativeResource));
}
@Override
public void close() {
cleanable.clean();
}
private static class ResourceCleaner implements Runnable {
private final NativeResource resource;
ResourceCleaner(NativeResource resource) {
this.resource = resource;
}
@Override
public void run() {
resource.release();
}
}
}
// 使用方式
try (ResourceHolder holder = new ResourceHolder()) {
// 使用资源
} // 自动调用 close()
使用 PhantomReference 的方案:
// ✅ 使用 PhantomReference + ReferenceQueue
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class ResourceCleanupService {
private final ReferenceQueue<ResourceHolder> queue = new ReferenceQueue<>();
public ResourceCleanupService() {
Thread cleanerThread = new Thread(() -> {
while (true) {
try {
Reference<? extends ResourceHolder> ref = queue.remove();
// 清理资源
((ResourceHolder.ResourceRef) ref).cleanup();
ref.clear();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
cleanerThread.setDaemon(true);
cleanerThread.start();
}
public void track(ResourceHolder holder) {
new ResourceRef(holder, queue);
}
private static class ResourceRef extends PhantomReference<ResourceHolder> {
private final NativeResource resource;
ResourceRef(ResourceHolder holder, ReferenceQueue<ResourceHolder> queue) {
super(holder, queue);
this.resource = holder.nativeResource;
}
void cleanup() {
resource.release();
}
}
}
3.5 第五类:SecurityManager
废弃原因:SecurityManager 在 Java 17 中被标记为 deprecated for removal(JEP 411),Java 21 中仍然可用但会发出警告。
⚠️ 重要提示:SecurityManager 在 Java 21 中并没有被完全移除,但强烈建议移除相关代码,因为它将在未来的 Java 版本中彻底移除。
替换方案:移除所有 SecurityManager 相关代码。
代码对比:
// ❌ 旧代码:使用 SecurityManager
import java.security.SecurityManager;
public class SecurityConfig {
public static void configureSecurity() {
SecurityManager sm = new SecurityManager() {
@Override
public void checkPermission(java.security.Permission perm) {
// 自定义权限检查
}
};
System.setSecurityManager(sm);
}
}
// ✅ 新代码:移除 SecurityManager
public class SecurityConfig {
// SecurityManager 已不再需要
// 如果需要权限控制,使用 Java 模块系统或其他机制
}
处理 Spring Boot 应用:
// 如果你的 Spring Boot 应用中有 SecurityManager 相关配置
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// 移除 SecurityManager 相关代码
// System.setSecurityManager(null);
SpringApplication.run(Application.class, args);
}
}
禁用警告(临时方案):
// 方案一:使用 @SuppressWarnings 注解抑制单个警告
@SuppressWarnings("removal")
public void configureSecurity() {
SecurityManager sm = new SecurityManager();
System.setSecurityManager(sm);
}
// 方案二:在启动参数中禁用 SecurityManager 移除警告
// -Djava.security.manager=allow
// 注意:这只是临时方案,建议尽早移除 SecurityManager 相关代码
四、Maven modernizer 插件:自动扫描废弃 API
4.1 插件配置
在 pom.xml 中添加 modernizer 插件:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>modernizer-maven-plugin</artifactId>
<version>2.8.0</version>
<configuration>
<!-- 指定目标 Java 版本 -->
<javaVersion>21</javaVersion>
<!-- 是否在编译时检查 -->
<failOnViolation>true</failOnViolation>
<!-- 是否包含测试代码 -->
<includeTestClasses>true</includeTestClasses>
<!-- 自定义排除规则(可选) -->
<exclusions>
<exclusion>java.awt.**</exclusion>
</exclusions>
</configuration>
<executions>
<execution>
<goals>
<goal>modernizer</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
4.2 运行扫描
# 扫描主代码
mvn modernizer:modernizer
# 扫描测试代码
mvn modernizer:modernizer -DincludeTestClasses=true
# 只检查不报错
mvn modernizer:modernizer -DfailOnViolation=false
4.3 扫描报告示例
[INFO] --- modernizer-maven-plugin:2.8.0:modernizer (default-cli) @ my-project ---
[INFO] Found 15 violations in main classes
[INFO] Found 3 violations in test classes
[ERROR] org.codehaus.mojo.modernizer.ModernizerViolationException:
Found deprecated API usage:
1. java.security.AccessController.doPrivileged(PrivilegedAction)
in com.example.service.SecurityService.java:45
2. java.net.URL.<init>(String)
in com.example.util.HttpUtils.java:23
3. java.lang.Thread.stop()
in com.example.task.WorkerThread.java:78
...
五、OpenRewrite:一键迁移脚本
5.1 插件配置
在 pom.xml 中添加 OpenRewrite 插件:
<build>
<plugins>
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.42.0</version>
<configuration>
<activeRecipes>
<!-- Java 21 升级主配方(包含多个子配方) -->
<recipe>org.openrewrite.java.migrate.UpgradeToJava21</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-migrate-java</artifactId>
<version>2.18.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
5.2 UpgradeToJava21 包含的子配方
UpgradeToJava21 是一个伞形配方,包含以下子配方:
| 子配方 | 功能描述 |
|---|---|
UpgradeToJava17 | Java 17 升级(包含 AccessController、URL 构造函数等替换) |
RemediateFinalizers | 替换 finalize() 方法为 Cleaner |
RemoveUnusedImports | 移除未使用的导入 |
CleanupUnusedVariables | 清理未使用的变量 |
💡 提示:如果你只需要修复特定的废弃 API,可以使用
UpgradeToJava17配方,它已经包含了大部分废弃 API 的修复。
5.3 运行迁移
# 预览迁移效果(不修改文件)
mvn rewrite:dryRun
# 执行迁移(修改文件)
mvn rewrite:run
# 指定特定配方运行
mvn rewrite:run -Drewrite.activeRecipes=org.openrewrite.java.migrate.ReplaceDeprecatedUrlConstructor
5.4 迁移报告示例
[INFO] --- rewrite-maven-plugin:5.42.0:run (default-cli) @ my-project ---
[INFO] Using active recipes:
- org.openrewrite.java.migrate.UpgradeToJava21
- org.openrewrite.java.migrate.RemediateFinalizers
- org.openrewrite.java.migrate.RemoveSecurityManagerCalls
[INFO]
[INFO] Changes made:
[INFO]
[INFO] com.example.service.SecurityService.java
[INFO] - Removed AccessController.doPrivileged call
[INFO] - Inlined the privileged action body
[INFO]
[INFO] com.example.util.HttpUtils.java
[INFO] - Replaced new URL("...") with URI.create("...").toURL()
[INFO]
[INFO] com.example.task.WorkerThread.java
[INFO] - Replaced Thread.stop() with interrupt pattern
[INFO] - Added volatile running flag
[INFO]
[INFO] Total changes: 15 files modified, 3 classes removed
六、兼容性处理清单
6.1 必须处理的废弃 API
| 废弃 API | Java 版本 | 处理优先级 | 替换方案 |
|---|---|---|---|
AccessController.doPrivileged | 17+ | P0 | 直接调用 |
Thread.stop/resume/suspend | 1.2+ | P0 | 中断模式/ExecutorService |
new URL(String) | 17+ | P1 | URI.create(str).toURL() |
finalize() | 9+ | P1 | Cleaner / try-with-resources |
SecurityManager | 17+ | P1 | 移除 / 禁用警告 |
6.2 需要关注的其他变更
| 变更类型 | 影响范围 | 处理方式 |
|---|---|---|
| 模块系统增强 | 反射访问受限 | 使用 --add-opens 或重构代码 |
| 序列化变更 | 旧序列化格式 | 检查序列化兼容性 |
| JVM 参数变更 | 启动脚本 | 更新 JVM 参数 |
| 日期时间 API | Date/Calendar | 迁移到 java.time |
6.3 迁移检查清单
迁移检查清单:
┌─────────────────────────────────────────────────────┐
│ │
│ ✅ 使用 modernizer 插件扫描所有废弃 API │
│ ✅ 使用 OpenRewrite 批量修复可自动迁移的 API │
│ ✅ 手动修复 Thread.stop/resume/suspend │
│ ✅ 替换 finalize() 为 Cleaner │
│ ✅ 移除或禁用 SecurityManager │
│ ✅ 替换 URL 构造函数为 URI.toURL() │
│ ✅ 移除 AccessController.doPrivileged │
│ ✅ 更新 pom.xml 中的 Java 版本 │
│ ✅ 更新 IDE 项目配置 │
│ ✅ 更新 CI/CD 构建脚本 │
│ ✅ 更新生产环境 JVM 参数 │
│ ✅ 运行完整测试套件 │
│ ✅ 灰度发布验证 │
│ │
└─────────────────────────────────────────────────────┘
七、迁移实战技巧
7.1 渐进式迁移策略
渐进式迁移步骤:
1. 先升级到 Java 17(LTS),处理大部分废弃 API
2. 再升级到 Java 21,处理剩余问题
3. 使用 -Djava.version=21 进行编译测试
4. 使用 --release 21 确保兼容性
7.2 处理第三方依赖问题
// 如果第三方依赖使用了废弃 API,可以:
// 1. 升级依赖版本
<dependency>
<groupId>com.thirdparty</groupId>
<artifactId>library</artifactId>
<version>2.0.0</version> <!-- 使用兼容 Java 21 的版本 -->
</dependency>
// 2. 使用 --add-opens 临时解决
// 在启动参数中添加:
// --add-opens java.base/java.lang=ALL-UNNAMED
// 3. 使用迁移工具修复依赖源码(如果是开源项目)
7.3 测试策略
// 迁移后的测试策略:
// 1. 运行单元测试
// 2. 运行集成测试
// 3. 运行性能测试(检查 Finalizer 移除后的性能变化)
// 4. 运行并发测试(检查线程中断机制是否正确)
// 5. 运行安全测试(检查 SecurityManager 移除后的影响)
八、总结
8.1 废弃 API 替换总结
| 废弃 API | 替换方案 | 自动化工具 |
|---|---|---|
AccessController.doPrivileged | 直接调用 | OpenRewrite |
Thread.stop/resume/suspend | 中断模式/ExecutorService | OpenRewrite(部分) |
new URL(String) | URI.create(str).toURL() | OpenRewrite |
finalize() | Cleaner / try-with-resources | OpenRewrite |
SecurityManager | 移除 / 禁用警告 | OpenRewrite |
8.2 迁移工具对比
| 工具 | 功能 | 适用场景 |
|---|---|---|
| modernizer | 扫描废弃 API | 发现问题 |
| OpenRewrite | 批量修复废弃 API | 自动迁移 |
| IDE 内置工具 | 单个修复 | 精细调整 |
| Lombok | 简化代码 | 辅助工具 |
8.3 关键注意事项
关键注意事项:
1. Thread.stop() 的替换需要仔细设计,不能简单替换为 interrupt()
2. SecurityManager 在 Java 21 中仍可用,但建议尽早移除
3. Finalizer 的替换可能影响内存管理,需要充分测试
4. 第三方依赖可能使用废弃 API,需要同步升级
5. 迁移后需要进行全面的测试,特别是并发和性能测试
💡 互动话题:你在 Java 版本迁移中遇到过哪些坑?是如何解决的?欢迎在评论区分享你的经验!
