3502 字
10 分钟

jdk1.8升级,jdk21还是jdk17?

2024-10-15
2026-07-29

jdk1.8升级,jdk21还是jdk17?#

Spring Boot 3.x 至少需要 Java 17。它不支持 Java 8.

springboot3和2的组件对比:

Spring Boot 3.x 相较于 Spring Boot 2.x 带来了一系列重要的更新和改进,这些变化旨在提高性能、增强功能、并确保与最新 Java 版本的兼容性。以下是 Spring Boot 3.x 与 Spring Boot 2.x 之间的一些主要区别和新特性:

  1. Java 版本要求
    Spring Boot 3.x 要求至少使用 Java 17,这是最低版本要求。同时,Spring Boot 3.x 也已经通过了 Java 19 的测试,确保了更好的兼容性和性能。这意味着开发者需要使用 Java 17 或更高版本来运行和开发基于 Spring Boot 3.x 的应用程序。
  2. Spring Framework 版本
    Spring Boot 3.x 基于最新的 Spring Framework 6 构建,提供了更好的性能和功能。这是对之前 Spring Boot 2.x 使用的 Spring Framework 5.x 的一个重大升级。
  3. GraalVM 支持和原生镜像
    Spring Boot 3.x 引入了对 GraalVM 的支持,允许开发者使用 GraalVM 将 Spring 应用程序编译成本地可执行的镜像文件。这可以显著提升应用程序的启动速度、峰值性能以及减少内存使用。这一特性取代了之前的 Spring Native 项目。
  4. Jakarta EE API
    由于 Java EE 已经变更为 Jakarta EE,Spring Boot 3.x 支持 Jakarta EE 10,并且所有的 Java EE 依赖项都已经迁移到了 Jakarta EE API。这要求开发者在使用这些依赖项时,需要相应地更新包名从 javax 开头变更为 jakarta。
  5. 配置属性兼容性
    在 Spring Boot 3.x 中,一些配置属性被重新命名或删除,开发人员需要更新 application.properties 或 application.yml 配置文件。为了帮助开发者进行升级,Spring Boot 提供了 spring-boot-properties-migrator 模块,该模块可以在启动时分析应用程序的环境并打印诊断结果,同时在运行时为开发者临时迁移属性。
  6. 应用可观察性提高
    Spring Boot 3.x 通过 Micrometer 和 Micrometer 追踪提高应用可观察性。新版本支持 Micrometer 1.10 中引入的新的 Observation API,并自动配置 Micrometer 追踪,包括对 Brave、OpenTelemetry、Zipkin 和 Wavefront 组件的支持。
  7. 性能优化
    Spring Boot 3.x 对性能进行了优化,包括启动时间的改进、内存占用的减少以及并发性能的提升。这些优化使得 Spring Boot 3.x 在生产环境中能够更好地满足高性能和高可扩展性的需求。
  8. 其他新特性和改进
    响应式编程支持:Spring Boot 3.x 更加注重响应式编程范式,提供了更多与响应式相关的功能和支持。
    云原生支持:改进了对云原生应用程序开发的支持,提供更多的云服务集成和部署选项,如 Kubernetes、Docker 等。
    开发工具改进:提供了更好的开发工具集成和开发体验,包括更快的启动时间、改进的调试支持等。
    总结
    image
JDK版本类型发布日期强调推荐
8LTS03/2014拉姆达斯先前发布模型下的最后一个 LTS 版本。Oracle 的免费更新结束,但仍由其他人维护。在接下来的几个月内升级到 11 或 17!
9Feature09/2017模块引入了新的发布模型。停产。立即升级至 17 或 21!
10Feature03/2018变量停产。立即升级至 17 或 21!
11LTS09/2018新的 HTTP 客户端立即升级至21!
12Feature03/2019停产。立即升级至21!
13Feature09/2019停产。立即升级至21!
14Feature03/2020切换表达式停产。立即升级至21!
15Feature09/2020文本块停产。立即升级至21!
16Feature03/2021记录停产。立即升级至21!
17 号LTS09/2021密封课程支持 LTS 版本。 考虑在接下来的几个月内升级到 21。
18Feature03/2022默认 UTF-8停产。立即升级至21!
19Feature09/2022仅预览和孵化器功能停产。立即升级至21!
20Feature03/2023仅预览和孵化器功能停产。立即升级至21!
21LTS09/2023模式匹配,虚拟线程当前的 LTS 版本。

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

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

Terminal window
# 检查依赖升级情况
$ 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>
<version>1.3.2</version>
</dependency>
上述两个依赖代码基本一样。推荐使用该版本,不耽误以后同时使用更高版本的 jakarta.annotation:jakarta.annotation-api

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

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

有些文章提到,Spring 6 不支持 javax.annotation.Resource 注解,从下面的 Spring 代码来看,这是完全错误的。
  • 点击查看 Spring 源码

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>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.5.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>7.0.5.Final</version>
</dependency>
选择该版本是由于该版本支持 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>
</properties>
<!-- 在父 POM 中使用 dependencyManagement 生命 -->
<!-- 在需要的子模块中可以直接使用 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<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>
</properties>
<!-- 在父 POM 中使用 dependencyManagement 生命 -->
<!-- 在需要的子模块中可以直接使用 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<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>
开发机使用 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 启动参数

https://www.diguage.com/post/upgrade-to-openjdk21/

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

jdk1.8升级,jdk21还是jdk17?
https://www.motouguai.com/posts/upgrade-to-jdk18-jdk21-or-jdk17-1kykcu/
作者
Qiaoan
发布于
2024-10-15
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐