OpenJDK 21 升级指南

OpenJDK 21 升级指南

OpenJDK 21 已经发布半年有余,在这个版本中, Generational ZGC 也一起发布了。在 ZGC | What’s new in JDK 16 中, Per Lidén 宣称,将 ZGC 的最大停顿时间从 10ms 降低到了 1ms。再加上 JVM GC 性能测试(二):递增流量JVM GC 性能测试(三):真实流量 文中,GenZGC 的惊艳表现,这些种种先进技术,着实充满诱惑,忍不住想吃口螃蟹 🦀。这篇文章,D瓜哥就来分享一下,自己在升级 OpenJDK 21 中的一些经验。

本文仅介绍升级 OpenJDK 的相关内容,ZGC 原理等会专门撰文介绍。

升级依赖

依赖升级不是 KPI,也不涉及需求交付。所以,大多数项目的依赖自从项目创建后,就很少升级。如果想比较顺利地将项目升级到 OpenJDK 21,那么,先将项目所用依赖做一个整体升级是一个事半功倍的操作。可以直接使用 Maven 命令来检查依赖可以升级的情况:

mvn versions:display-dependency-updates

执行该命令后,会有如下类似输出:

# 检查依赖升级情况
$ mvn versions:display-dependency-updates

# 此处省略一万个字
# @author: D瓜哥 · https://www.diguage.com

[INFO]   org.springframework:spring-aop ......... 5.3.33 -> 6.1.6
[INFO]   org.springframework:spring-aspects ..... 5.3.33 -> 6.1.6
[INFO]   org.springframework:spring-beans ....... 5.3.33 -> 6.1.6
[INFO]   org.springframework:spring-context ..... 5.3.33 -> 6.1.6
[INFO]   org.springframework:spring-core ........ 5.3.33 -> 6.1.6
[INFO]   org.springframework:spring-jdbc ........ 5.3.33 -> 6.1.6
[INFO]   org.springframework:spring-web ......... 5.3.33 -> 6.1.6

[INFO]   org.mybatis:mybatis-2-spring ............ 1.1.0 -> 1.2.0
[INFO]   org.mybatis:mybatis-spring .............. 2.1.1 -> 2.1.2

[INFO]   org.junit.jupiter:junit-jupiter ........ 5.9.3 -> 5.10.2
[INFO]   org.junit.jupiter:junit-jupiter-api .... 5.9.3 -> 5.10.2

可以根据这个输出情况,将相关依赖做一个整体升级。这里再注重提醒三个方面。

Lombok

如果项目使用了 Lombok 依赖,务必将其升级到 v1.18.30+,低于此版本的 Lombok 会报错,具体原因见: [BUG] lombok 1.8.26 incompatible with JDK21 · #3393

Netty

如果项目使用了 Netty,建议进来将其升级到 4.1.93.Final+。OpenJDK 21 对 DirectByteBuffer 的构造函数做了改动,该版本做了兼容。修改日志见: Netty.news: Netty 4.1.93.Final released,代码改动详情见: Adapt to DirectByteBuffer constructor in Java 21 #13366

反向兼容 JDK 8

另外,提醒一下,如果开发环境还是以 JDK 8 为主,那么 Spring 最好就不要升级到 6.x,能不能忽略类似的相关问题呢?解决办法请参考: Versions Maven 插件简介,文章里对忽略版本,一键升级等操作,都做了比较详细的介绍。

@javax.annotation.Resource

直接将本地使用的 JDK 版本切换成 JDK 21 后,编译大概率会报错,提示找不到 javax.annotation.Resource,这是由于 JEP 320: Remove the Java EE and CORBA Modules 提案,在 OpenJDK 11 中移除了 JavaEE 的相关内容。所以,需要将其使用 Jar 包的形式专门引用一下。

<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>1.3.5</version>
</dependency>

<!-- 或者 -->
<!-- @author: D瓜哥 · https://www.diguage.com -->

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId> (1)
    <version>1.3.2</version>
</dependency>
1上述两个依赖代码基本一样。推荐使用该版本,不耽误以后同时使用更高版本的 jakarta.annotation:jakarta.annotation-api

Spring 6+ 同时支持新旧版的 @Resource

另外,有一点需要特别说明:Spring 6+ 支持新版的 jakarta.annotation.Resource 注解,同时还兼容旧版的 javax.annotation.Resource。相关代码如下:

有些文章提到,Spring 6 不支持 javax.annotation.Resource 注解,从下面的 Spring 代码来看,这是完全错误的。
点击查看 Spring 源码
CommonAnnotationBeanPostProcessor.java
public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor
    implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable {

  // Defensive reference to JNDI API for JDK 9+ (optional java.naming module)
  private static final boolean jndiPresent = ClassUtils.isPresent(
      "javax.naming.InitialContext", CommonAnnotationBeanPostProcessor.class.getClassLoader());

  private static final Set<Class<? extends Annotation>> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(3);

  @Nullable
  private static final Class<? extends Annotation> jakartaResourceType;

  @Nullable
  private static final Class<? extends Annotation> javaxResourceType;

  @Nullable
  private static final Class<? extends Annotation> ejbAnnotationType;

  static {
    jakartaResourceType = loadAnnotationType("jakarta.annotation.Resource");
    if (jakartaResourceType != null) {
      resourceAnnotationTypes.add(jakartaResourceType);
    }

    javaxResourceType = loadAnnotationType("javax.annotation.Resource");
    if (javaxResourceType != null) {
      resourceAnnotationTypes.add(javaxResourceType);
    }

    ejbAnnotationType = loadAnnotationType("jakarta.ejb.EJB");
    if (ejbAnnotationType != null) {
      resourceAnnotationTypes.add(ejbAnnotationType);
    }
  }

  private final Set<String> ignoredResourceTypes = new HashSet<>(1);

  private InjectionMetadata buildResourceMetadata(Class<?> clazz) {
    if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
      return InjectionMetadata.EMPTY;
    }

    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    do {
      final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

      ReflectionUtils.doWithLocalFields(targetClass, field -> {
        if (ejbAnnotationType != null && field.isAnnotationPresent(ejbAnnotationType)) {
          if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalStateException("@EJB annotation is not supported on static fields");
          }
          currElements.add(new EjbRefElement(field, field, null));
        }
        else if (jakartaResourceType != null && field.isAnnotationPresent(jakartaResourceType)) {
          if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalStateException("@Resource annotation is not supported on static fields");
          }
          if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
            currElements.add(new ResourceElement(field, field, null));
          }
        }
        else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceType)) {
          if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalStateException("@Resource annotation is not supported on static fields");
          }
          if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
            currElements.add(new LegacyResourceElement(field, field, null));
          }
        }
      });

      ReflectionUtils.doWithLocalMethods(targetClass, method -> {
        Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
        if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
          return;
        }
        if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) {
          if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
            if (Modifier.isStatic(method.getModifiers())) {
              throw new IllegalStateException("@EJB annotation is not supported on static methods");
            }
            if (method.getParameterCount() != 1) {
              throw new IllegalStateException("@EJB annotation requires a single-arg method: " + method);
            }
            PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
            currElements.add(new EjbRefElement(method, bridgedMethod, pd));
          }
        }
        else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) {
          if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
            if (Modifier.isStatic(method.getModifiers())) {
              throw new IllegalStateException("@Resource annotation is not supported on static methods");
            }
            Class<?>[] paramTypes = method.getParameterTypes();
            if (paramTypes.length != 1) {
              throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method);
            }
            if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
              PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
              currElements.add(new ResourceElement(method, bridgedMethod, pd));
            }
          }
        }
        else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) {
          if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
            if (Modifier.isStatic(method.getModifiers())) {
              throw new IllegalStateException("@Resource annotation is not supported on static methods");
            }
            Class<?>[] paramTypes = method.getParameterTypes();
            if (paramTypes.length != 1) {
              throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method);
            }
            if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
              PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
              currElements.add(new LegacyResourceElement(method, bridgedMethod, pd));
            }
          }
        }
      });

      elements.addAll(0, currElements);
      targetClass = targetClass.getSuperclass();
    }
    while (targetClass != null && targetClass != Object.class);

    return InjectionMetadata.forElements(elements, clazz);
  }
}

Nashorn JavaScript Engine

解决完编译问题后,启动报如下异常:

2024-01-02 14:27:27.062 [main] ERROR com.diguage.laf.config.spring.config.JavaScriptListener[67] - failed invoking script script/logback.js
java.lang.NullPointerException: Cannot invoke "javax.script.ScriptEngine.put(String, Object)" because "engine" is null

这是因为 JEP 372: Remove the Nashorn JavaScript Engine 提案,从 OpenJDK 11 开始,将 Nashorn JavaScript Engine 移除了。由于相关功能使用了 JavaScript 引擎,所以,就报了 “Cannot invoke "javax.script.ScriptEngine.put(String, Object)" because "engine" is null” 错误。处理办法如上,加回相关的依赖:

<dependency>
    <groupId>org.openjdk.nashorn</groupId>
    <artifactId>nashorn-core</artifactId>
    <version>15.4</version>
</dependency>

Java Validation API

最近,对一个项目升级中,遇到了如下一个报错:

Caused by: java.lang.ExceptionInInitializerError: Exception javax.validation.ValidationException: HV000183: Unable to initialize 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead [in thread "BZ-22001-108-T-17"]
    at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.buildExpressionFactory(ResourceBundleMessageInterpolator.java:199)
    at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.<init>(ResourceBundleMessageInterpolator.java:94)
    at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.getDefaultMessageInterpolator(AbstractConfigurationImpl.java:570)
    at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.getDefaultMessageInterpolatorConfiguredWithClassLoader(AbstractConfigurationImpl.java:790)
    at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.getMessageInterpolator(AbstractConfigurationImpl.java:480)
    at org.hibernate.validator.internal.engine.ValidatorFactoryImpl.<init>(ValidatorFactoryImpl.java:151)
    at org.hibernate.validator.HibernateValidator.buildValidatorFactory(HibernateValidator.java:38)
    at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.buildValidatorFactory(AbstractConfigurationImpl.java:430)

这是由于 Bean Validation 导致的问题。将依赖升级到如下版本即可:

<!-- @author: D瓜哥 · https://www.diguage.com -->
<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>3.0.2</version> (1)
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.5.Final</version>(1)
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>7.0.5.Final</version>(1)
</dependency>
1选择该版本是由于该版本支持 Java8,这样可以让项目无感升级到 OpenJDK21。

由于该版本的 Bean Validation 的基础包名已经从 javax. 改为 jakarta.,所以,需要修改程序,这部分工作已经有相关程序来自动完成,敬请关注: 使用 OpenRewrite 优化代码

JAXB

同样是由于 JEP 320: Remove the Java EE and CORBA Modules 提案, 在 OpenJDK 11 中移除了 JavaEE 的相关内容,其中也包括 JAXB。编译可能会报错,增加如下依赖即可:

<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.9</version>
</dependency>

Java 模块化

如果构建一切顺利,以为可以正常启动运行程序,结果却可能报如下错误:

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @66f57048
  at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:391)

这是由于在 JDK 9 中引入的 Java Platform Module System 导致的,该协议对 Java 的封装性做了进一步增强。更详细的内容可以看: ① 协议: Java Platform Module System JSR (376) ② 实现: JEP 261: Module System ③ 解释: Reflection vs Encapsulation

具体到该问题的解决办法也比较简单:将没开放的模块强制对外开放。有两个参数选项:

  1. --add-exports 导出包,意味着其中的所有公共类型和成员都可以在编译和运行时访问。

  2. --add-opens 打开包,意味着其中的所有类型和成员(不仅是公共类型)都可以在运行时访问。

两者的区别在于 --add-opens 开放的更加彻底,不仅 public 类型、变量及方法可以访问,就连非 public 元素,也可以通过调用 setAccessible(true) 后也可以访问。简单起见,直接使用 --add-opens 即可。相关的参数在异常中也提醒出来了: module java.base"opens java.lang",结合起来,直接这样配置:在 java 明了启动参数中,增加 --add-opens java.base/java.lang=ALL-UNNAMED 选项即可。

下面再列出几个相关示例:

java.base/java.util

错误日志:

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field protected int[] java.util.Calendar.fields accessible: module java.base does not "opens java.util" to unnamed module @21282ed8

启动参数: --add-opens java.base/java.util=ALL-UNNAMED

java.base/java.math

错误日志:

java.lang.reflect.InaccessibleObjectException: Unable to make field final int[] java.math.BigInteger.mag accessible: module java.base does not "opens java.math" to unnamed module @21282ed8

启动参数: --add-opens java.base/java.math=ALL-UNNAMED

构建与测试

上面介绍了程序相关的错误及解决办法,下面介绍一下构建流程中出现的问题。

maven-compiler-plugin 配置

如果项目中,在编译阶段做了一些扩展性的东西,那么就可能触发上面 Java 模块化 中描述的问题。类似如下日志:

java.lang.IllegalAccessError: class com.diguage.plugin.lombok.ToStringProcessor (in unnamed module @0x551976c2)
cannot access class com.sun.tools.javac.api.JavacTrees (in module jdk.compiler)
because module jdk.compiler does not export com.sun.tools.javac.api to unnamed module @0x551976c2
        at com.diguage.plugin.lombok.ToStringProcessor.init(ToStringProcessor.java:44)

这个问题也可以通过增加参数来完成。不过,这个参数需要在 pom.xml 中通过给 maven-compiler-plugin 插件增加配置的方式来搞,如下:

<!-- @author: D瓜哥 · https://www.diguage.com -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.13.0</version>
  <configuration>
      <showWarnings>true</showWarnings>
      <fork>true</fork>
      <compilerArgs>
        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
      </compilerArgs>
  </configuration>
</plugin>

低版本的 Lombok 也会遇到类似问题,可以通过升级到高版本来解决。实在解决不了,兜底方案也可以直接在这里配置。

maven-surefire-plugin 配置

使用 Maven 进行构建或者专门执行测试时,可能也会遇到 Java 模块化 中描述的问题。同样,可以通过在 pom.xml 中配置 maven-surefire-plugin 插件的方式来解决,具体如下:

<!-- @author: D瓜哥 · https://www.diguage.com -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.2.5</version>
  <configuration>
      <skipTests>true</skipTests>
      <includes>
        <include>**/*Test.java</include>
      </includes>
      <argLine>
        --add-opens java.base/java.lang=ALL-UNNAMED
        --add-opens java.base/java.util=ALL-UNNAMED
        --add-opens java.base/java.math=ALL-UNNAMED
        --add-opens java.base/java.time=ALL-UNNAMED
      </argLine>
  </configuration>
</plugin>

IntelliJ IDEA 配置

在 IntelliJ IDEA 运行程序,大概率也会报错,可以通过在 “VM Option” 配置项中,增加 Java 模块化 提到的相关启动参数即可正常启动。

技巧

还有一个不是问题的问题需要解决一下:目前大多数开发人员用的还是 JDK 8,如何可以让大家无痛或者无感升级呢?

D瓜哥分享一个小技巧:可以使用 Maven 的 profile 机制,让其根据 JDK 版本号,自动激活不同的配置。具体入戏下:

<!-- @author: D瓜哥 · https://www.diguage.com -->
<profile>
  <id>Java1.8</id>
  <activation>
    <!-- 在 JDK 1.8 时自动激活-->
    <jdk>1.8</jdk>
  </activation>
  <properties>
    <spring.version>5.3.33</spring.version> (1)
  </properties>
  <!-- 在父 POM 中使用 dependencyManagement 生命 -->
  <!-- 在需要的子模块中可以直接使用 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>javax.servlet</groupId> (1)
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.2.5</version>
        <configuration>
        <includes>
          <include>**/*Test.java</include>
        </includes>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
        <configuration>
          <showWarnings>true</showWarnings>
          <fork>true</fork>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>

<!-- @author: D瓜哥 · https://www.diguage.com -->
<profile>
  <id>Java21</id>
  <activation>
    <jdk>[21,)</jdk>
  </activation>
  <properties>
    <spring.version>6.0.19</spring.version> (1)
  </properties>
  <!-- 在父 POM 中使用 dependencyManagement 生命 -->
  <!-- 在需要的子模块中可以直接使用 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>jakarta.servlet</groupId> (1)
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
      </dependency>
      <dependency>
        <groupId>org.openjdk.nashorn</groupId>
        <artifactId>nashorn-core</artifactId>
        <version>15.4</version>
      </dependency>
      <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>2.3.9</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <!--在几乎所有模块都会使用,所以,直接在父 POM 中声明依赖 -->
  <dependencies>
    <dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>javax.annotation-api</artifactId>
      <version>1.3.2</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.2.5</version>
        <configuration>
        <includes>
          <include>**/*Test.java</include>
        </includes>
        <argLine>
          --add-opens java.base/java.lang=ALL-UNNAMED
          --add-opens java.base/java.util=ALL-UNNAMED
          --add-opens java.base/java.math=ALL-UNNAMED
          --add-opens java.base/java.time=ALL-UNNAMED
        </argLine>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
        <configuration>
        <showWarnings>true</showWarnings>
        <fork>true</fork>
        <compilerArgs>
          <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
        </compilerArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>
1开发机使用 JDK 8,所以,使用 Spring 5 + Servlet;正式环境使用 OpenJDK 21,所以,使用 Spring 6 + Jakarta Servlet。

使用上面的配置,只要程序没有直接使用 Servlet API,就可以在 JDK 8 和 OpenJDK 21 之间自由切换。真正做到平稳升级。

科技与狠活

文章最后,在整一点科技与狠活。

EMT4J

关于 JDK 升级的事项,其实还有很多检查项。理想情况下,最好有工具能自动检查这些项目。关于这个问题,阿里巴巴开发了 Migration Toolkit for Java,现在已经捐给 Eclipse 基金会了,代码在 adoptium/emt4j: Eclipse Migration Toolkit for Java。这个工具还提供了 Maven 插件,所以,可以直接使用这个插件来做检查工作。具体配置如下:

<plugin>
  <groupId>org.eclipse.emt4j</groupId>
  <artifactId>emt4j-maven-plugin</artifactId>
  <version>0.8.0</version>
  <!-- 可以将检查过程绑定到 Maven 构建周期的某个阶段,但不建议。 -->
  <!-- <executions>-->
  <!--   <execution>-->
  <!--     <phase>process-test-classes</phase>-->
  <!--     <goals>-->
  <!--       <goal>check</goal>-->
  <!--     </goals>-->
  <!--   </execution>-->
  <!-- </executions>-->
  <configuration>
      <!-- 当前版本 -->
      <fromVersion>8</fromVersion>
      <!-- 期望升级版本 -->
      <toVersion>21</toVersion>
      <outputFile>report.html</outputFile>
  </configuration>
</plugin>

然后执行如下命令就可以对应用程序做个全面检查:

mvn emt4j:check

在构建目录里找 report.html 文件,会有一个个超长的文件,列出成千上百个问题。(D瓜哥检查的一个应用有 2600 行的检查结果。)其实,不用担心,大部分问题可以忽略。但是,你很清楚可能潜在的问题,就像吃西药的时候,看到一大堆不良反应后,吃起来更放心。

OpenRewrite

上述工具检查出来的一部分问题,可以用另外“科技与狠活”解决,限于篇幅,这里就不展开了。敬请关注: 使用 OpenRewrite 优化代码

线上参数

随着 Java 的升级,Java 的启动参数也发生了不小变化。升级到 OpenJDK 21 后,原有的启动参数大概率没办法直接重用。那么,上线的时候,启动参数怎么配置呢?接下来,D瓜哥会分享一下在生产环境中的启动参数。敬请关注: 生产环境中 Java 21 启动参数