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