18、JDK 23 新特性:JDK 23 迁移指南:从旧版本升级的注意事项与最佳实践

升级JDK版本的时候,最怕的就是兼容性问题,特别是那些用了内部API、弃用API的代码,升级后可能就编译不过或者运行出错了。JDK 23虽然引入了很多新特性,但是也有一些API被弃用或者移除了,升级的时候得注意这些变化。

鹏磊我之前升级项目的时候,遇到过不少问题,比如用了sun.misc.Unsafe的内存访问方法,JDK 23里被弃用了,得迁移到VarHandle;还有ThreadLocal在虚拟线程场景下性能不好,得迁移到作用域值。这些问题不提前处理,升级后就得花时间改代码。

今天咱就聊聊从旧版本升级到JDK 23的注意事项和最佳实践,看看怎么安全、顺利地完成升级。

升级前的准备工作

评估当前项目状况

升级前得先评估一下项目的状况,看看有哪些潜在问题:

  1. 检查JDK版本:确认当前使用的JDK版本
  2. 检查依赖项:确认第三方库和框架是否兼容JDK 23
  3. 检查代码:识别使用了已弃用或移除API的代码
  4. 检查构建工具:确保Maven、Gradle等构建工具支持JDK 23
# 检查当前JDK版本
java -version

# 检查项目依赖
mvn dependency:tree  # Maven项目
gradle dependencies  # Gradle项目

# 检查使用了已弃用API的代码
javac -Xlint:deprecation MyClass.java

提前评估可以发现问题,避免升级后出问题。

使用jdeprscan工具

JDK提供了jdeprscan工具来扫描代码中使用的已弃用API:

# 扫描JAR文件中的已弃用API
jdeprscan --release 23 myapp.jar

# 扫描类文件
jdeprscan --release 23 com/example/MyClass.class

# 扫描整个目录
jdeprscan --release 23 -cp lib/*.jar target/classes

# 输出示例:
# com.example.MyClass: warning: [removal] sun.misc.Unsafe.putInt(Object,long,int) has been deprecated for removal

用jdeprscan可以提前发现使用了已弃用API的代码。

主要兼容性问题

sun.misc.Unsafe内存访问方法弃用

JDK 23中,sun.misc.Unsafe的内存访问方法被标记为弃用,需要迁移到标准API:

// 问题代码:使用Unsafe内存访问方法
import sun.misc.Unsafe;

private static final Unsafe UNSAFE = getUnsafe();
private static final long VALUE_OFFSET;

static {
    try {
        VALUE_OFFSET = UNSAFE.objectFieldOffset(
            MyClass.class.getDeclaredField("value")
        );
    } catch (Exception e) {
        throw new Error(e);
    }
}

private volatile int value;

public void setValue(int newValue) {
    UNSAFE.putInt(this, VALUE_OFFSET, newValue);  // 已弃用
}

// 迁移方案:使用VarHandle
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

private static final VarHandle VALUE_HANDLE;

static {
    try {
        VALUE_HANDLE = MethodHandles.lookup()
            .findVarHandle(MyClass.class, "value", int.class);
    } catch (Exception e) {
        throw new Error(e);
    }
}

private volatile int value;

public void setValue(int newValue) {
    VALUE_HANDLE.set(this, newValue);  // 标准API
}

迁移到VarHandle,代码更安全,未来版本也会继续支持。

ThreadLocal在虚拟线程场景下的问题

如果使用虚拟线程,ThreadLocal的性能开销大,建议迁移到作用域值:

// 问题代码:在虚拟线程场景下使用ThreadLocal
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();

void handleRequest(Request request) {
    USER_ID.set(request.getUserId());
    try {
        processRequest(request);
    } finally {
        USER_ID.remove();  // 得手动清理
    }
}

// 迁移方案:使用作用域值
import java.util.concurrent.ScopedValue;

private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

void handleRequest(Request request) {
    ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
        processRequest(request);  // 在作用域内,USER_ID可用
    });  // 作用域结束,自动清理
}

作用域值在虚拟线程场景下性能更好,生命周期也自动管理。

模块系统变化

如果项目是模块化的,需要注意模块系统的变化:

// 检查模块配置
// module-info.java

module com.example.myapp {
    requires java.base;
    requires java.desktop;  // 检查是否需要
    // ...
}

// 如果使用了内部API,可能需要添加
// --add-exports 或 --add-opens 选项

模块系统可能有新的要求,需要检查模块配置。

分阶段升级策略

策略1:逐步升级

如果当前使用的JDK版本较低,建议逐步升级:

# 第一步:升级到JDK 17(LTS)
# 测试通过后,再升级到JDK 21(LTS)
# 最后升级到JDK 23

# JDK 8 -> JDK 17 -> JDK 21 -> JDK 23

逐步升级可以降低风险,每个阶段都能充分测试。

策略2:并行运行

在升级过程中,可以并行运行新旧版本:

# 开发环境使用JDK 23
export JAVA_HOME=/path/to/jdk-23

# 生产环境暂时使用旧版本
# 等测试通过后再切换

并行运行可以降低风险,有问题可以快速回退。

策略3:功能开关

使用功能开关控制新特性的使用:

// 使用功能开关
public class FeatureFlags {
    private static final boolean USE_SCOPED_VALUES = 
        Boolean.getBoolean("use.scoped.values");
    
    public void handleRequest(Request request) {
        if (USE_SCOPED_VALUES) {
            // 使用作用域值
            ScopedValue.runWhere(USER_ID, request.getUserId(), () -> {
                processRequest(request);
            });
        } else {
            // 使用ThreadLocal(旧方式)
            USER_ID.set(request.getUserId());
            try {
                processRequest(request);
            } finally {
                USER_ID.remove();
            }
        }
    }
}

功能开关可以逐步迁移,降低风险。

迁移步骤

步骤1:更新构建配置

更新构建工具的配置,支持JDK 23:

<!-- Maven配置 -->
<properties>
    <maven.compiler.source>23</maven.compiler.source>
    <maven.compiler.target>23</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <release>23</release>
                <compilerArgs>
                    <arg>--enable-preview</arg>  <!-- 如果需要预览特性 -->
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>
// Gradle配置
plugins {
    id 'java'
}

java {
    sourceCompatibility = JavaVersion.VERSION_23
    targetCompatibility = JavaVersion.VERSION_23
}

tasks.withType(JavaCompile) {
    options.compilerArgs += '--enable-preview'  // 如果需要预览特性
    options.release = 23
}

更新构建配置,支持JDK 23。

步骤2:修复编译错误

修复编译错误,处理不兼容的代码:

// 修复使用了已弃用API的代码
// 1. 使用jdeprscan找出问题
// 2. 迁移到标准API
// 3. 测试确保功能正常

修复编译错误,确保代码能正常编译。

步骤3:处理运行时问题

处理运行时可能出现的问题:

// 检查运行时行为变化
// 1. 运行测试套件
// 2. 检查日志中的警告
// 3. 性能测试

处理运行时问题,确保应用正常运行。

步骤4:更新依赖

更新第三方依赖,确保兼容JDK 23:

<!-- 更新依赖版本 -->
<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-library</artifactId>
        <version>2.0.0</version>  <!-- 更新到兼容JDK 23的版本 -->
    </dependency>
</dependencies>

更新依赖,确保兼容JDK 23。

常见问题处理

问题1:内部API访问

如果代码访问了内部API,需要迁移到标准API:

// 问题:访问内部API
import sun.misc.Unsafe;  // 内部API

// 解决方案:使用标准API
import java.lang.invoke.VarHandle;  // 标准API

迁移到标准API,避免使用内部API。

问题2:弃用API使用

如果代码使用了弃用API,需要迁移到新API:

// 问题:使用弃用API
ThreadLocal<String> tl = new ThreadLocal<>();  // 在虚拟线程场景下性能差

// 解决方案:使用新API
ScopedValue<String> sv = ScopedValue.newInstance();  // 新API

迁移到新API,提升性能和可维护性。

问题3:模块系统问题

如果项目是模块化的,可能需要调整模块配置:

// 检查module-info.java
module com.example.myapp {
    requires java.base;
    // 可能需要添加新的依赖
    requires java.logging;
    // ...
}

调整模块配置,确保模块系统正常工作。

测试策略

单元测试

运行单元测试,确保功能正常:

# 运行单元测试
mvn test  # Maven项目
gradle test  # Gradle项目

单元测试可以快速发现问题。

集成测试

运行集成测试,确保系统集成正常:

# 运行集成测试
mvn verify  # Maven项目
gradle integrationTest  # Gradle项目

集成测试可以确保系统集成正常。

性能测试

进行性能测试,确保性能不下降:

// 性能测试
public void performanceTest() {
    long start = System.nanoTime();
    // 执行操作
    long duration = System.nanoTime() - start;
    System.out.println("Duration: " + duration + " ns");
}

性能测试可以确保性能不下降。

最佳实践

1. 充分测试

升级后要充分测试,确保功能正常:

# 运行所有测试
mvn clean test

# 运行集成测试
mvn verify

# 性能测试
# ...

充分测试可以确保升级成功。

2. 逐步迁移

不要一次性迁移所有代码,逐步迁移:

// 第一步:迁移核心功能
// 第二步:迁移辅助功能
// 第三步:迁移边缘功能

逐步迁移可以降低风险。

3. 保持兼容

在迁移过程中,保持向后兼容:

// 保持向后兼容
public class CompatibleAPI {
    @Deprecated
    public void oldMethod() {
        newMethod();  // 调用新方法
    }
    
    public void newMethod() {
        // 新实现
    }
}

保持向后兼容,降低迁移风险。

4. 文档更新

更新文档,说明迁移情况:

/**
 * 迁移说明:
 * - 从ThreadLocal迁移到ScopedValue
 * - 从Unsafe迁移到VarHandle
 * - ...
 */
public class MigratedClass {
    // ...
}

更新文档,方便团队理解迁移情况。

回退策略

准备回退方案

如果升级出现问题,要有回退方案:

# 回退到旧版本
export JAVA_HOME=/path/to/jdk-21  # 回退到JDK 21

# 或者使用Docker容器
docker run -it openjdk:21  # 使用JDK 21容器

准备回退方案,降低风险。

版本控制

使用版本控制管理代码,方便回退:

# 创建迁移分支
git checkout -b jdk-23-migration

# 如果出现问题,可以回退
git checkout main

版本控制可以方便回退。

总结

从旧版本升级到JDK 23,需要充分准备、逐步迁移、充分测试。主要注意sun.misc.Unsafe的内存访问方法弃用、ThreadLocal在虚拟线程场景下的问题、模块系统的变化等。

鹏磊我觉得升级JDK版本是个系统工程,不能急,得一步步来。先用jdeprscan扫描代码,找出问题,然后逐步迁移,充分测试,最后再上线。这样虽然慢点,但是安全,不会出大问题。

总的来说,JDK 23的升级虽然有一些兼容性问题,但是通过合理的迁移策略和充分的测试,可以安全、顺利地完成升级。升级后可以享受新特性带来的好处,提升开发效率和代码质量。

本文章最后更新于 2025-11-28