前言
众所周知,SpringBoot3.0迎来了全面支持JDK17的局面,且最低支持版本就是JDK17,这就意味着,Spring社区将完全抛弃JDK8,全面转战JDK17。作为JAVA开源生态里的扛把子,Spring可以说是整个JAVA生态的风向标,可以说,当Spring转战JDK17,会很快带领JAVA生态全面的跟进JDK17。而我本篇文章重点讲述Spring版本和JDK17升级中的实践整理。
为什么是Spring Boot 2.7
Spring Boot 3.0是全面放弃JDK8,而Spring社区当然不会把事情做的那么决绝,在推出3.0之前,Spring就开始着手布局JDK17升级。而2.7版本,就是Spring社区为了升级JDK17而推出的过渡版本,具体包括一以下几个方面的升级和改进:
1.支持了JDK 17的新特性,例如switch表达式、文本块、局部变量类型推断等。这使得在Spring应用程序中使用JDK 17的特性变得更加容易和方便。
2.利用了JDK 17的性能优化:JDK 17引入了许多性能优化,例如新的垃圾收集器、线程调度等。Spring 2.7利用了这些性能优化,可以提高Spring应用程序的性能和响应速度。
3.默认配置与JDK 17兼容:Spring Boot 2.7的默认配置与JDK 17兼容,这意味着您不需要进行额外的配置就可以在JDK 17上运行Spring Boot应用程序。这点很重要,Spring Boot 2.7依赖于Servlet 4.0,而Servlet 4.0本身并不直接支持JDK 17, Spring Boot 2.7为了支持JDK 17进行了一些兼容性调整和优化,以使其能够在JDK 17上运行。
4.改进的安全性:JDK 17通过增强加密算法、禁用旧版TLS和SSL协议等增强了安全性。Spring Boot 2.7利用了这些安全改进,提高了应用程序的安全性。
5.持续的性能优化和提升。相比于老系统的2.1到2.3版本,2.7版本对内存管理和bean管理都有很大程度的优化和提升,内存使用更加合理。虽然官网没有给出所谓的性能提升对比,但性能的优化和系统的稳定性是一定加强的。
总之,使用Spring Boot 2.7可以更好地利用JDK 17的特性,提高应用程序的性能和响应速度,同时还可以获得更好的兼容性和安全性。所以通过Spring Boot 2.7过渡升级JDK17,是一种更为温和方式,且遇到的兼容性问题最小。当然这不全是我自顾自说,Spring官方给出了这样的说明:
vbnet
If you're currently running with an earlier version of Spring Boot, we strongly recommend that you upgrade to Spring Boot 2.7 before migrating to Spring Boot 3.0.
为什么是JDK17
关于JDK17的新特性和优势,对于它支持的新语法和编程特性,我在此不再赘述,因为网上有很多文章介绍。
对于我落地JDK17的动力主要源于两个方面:一是更为安全的语言特性,二是更加优异的垃圾回收器和性能提升。
a. 安全的语言特性
1.安全性首先体现在JDK17对于包扫描和反射的权限控制,可能大家对当年的FastJson漏洞记忆犹新,它的病根在于对反射的滥用。而对于这种反射的滥用,在JDK17里有了更严格的控制。
JDK 17对反射进行了优化,主要表现在对反射调用进行了权限控制。具体来说,它通过setAccessible()方法启动或禁止访问安全检查开关。当参数值为true时,反射的对象在使用时取消安全检查,提高反射的效率;当参数值为false时,反射的对象执行安全检查。这样的优化使得在处理反射调用时,可以更加灵活地控制访问权限。
1.除此之外,JDK 17增强了包扫描的权限控制。在之前的版本中,Java的包扫描是基于类的,而在JDK 17中,它扩展到了对整个包的权限控制。这使得开发者可以更加精细地控制对特定包的访问权限。
2.针对于语法本身,引入了密封的类和接口,具体使用细节大家可以网上查看。通过密封类和接口,进一步增加了面向对象开发的封闭性,提升代码质量的安全可靠。
b. 垃圾收集器
JDK17引入了ZGC作为垃圾收集器,此处引用一下京东科技同事做的关于不同垃圾收集器在不同JDK版本下的压测结果:
压测服务背景:
DOS平台上选择了不同配置的机器(2C4G、4C8G、8C16G),并分别使用JDK8、JDK11和JDK17进行部署和压测。整个压测过程限时60分钟,用180个虚拟用户并发请求一个接口,每次接口请求都创建512Kb的数据。最终产出不同GC回收器的各项指标数据,来分析GC的性能提升效果。

以上的压测结果对于我们来说非常诱人,ZGC配合JDK17的性能对于其他JDK和垃圾收集器组合来说是碾压获胜,且不论任何机器配置下,都推荐使用ZGC,ZGC的停顿时间达到亚毫秒级,吞吐量也比较高。这个对于一个高并发业务场景下,对于资源的优化是非常客观的。
c. OpenJDK17下载地址
提供了一个下载地址: adoptium.net/zh-cn/temur...
行云部署上的实践方案
a. Spring Boot 2.7
1. pom.xml版本依赖
实践的版本选择上我选择了2.7大版本下的最新小版本,即Spring Boot 2.7.17版本。pom.xml依赖如下:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jd.magnus</groupId>
<artifactId>magnus-multi-ddd</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
</parent>
<modules>
<module>magnus-multi-ddd-adapter</module>
<module>magnus-multi-ddd-application</module>
<module>magnus-multi-ddd-domain</module>
<module>magnus-multi-ddd-infrastructure</module>
<module>magnus-multi-ddd-client</module>
<module>magnus-multi-ddd-worker</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jsf-lite.version>1.0.0-HOTFIX-T2</jsf-lite.version>
<ump.version>20221231.1</ump.version>
</properties>
</project>
2. 动态配置
Spring Boot 2.7对动态配置进行了更新。具体来说,Spring Boot 2.7更改了自动配置注册文件的路径和格式,从META-INF/spring.factories变更为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。
同时,Spring Boot 2.7还引入了新的注解@SpringBootApplication,该注解包含了@EnableAutoConfiguration和@ComponentScan等注解,使得配置更加简洁和方便。此外,Spring Boot 2.7还更新了一些自动配置的类和方法,以支持新版本的Spring Framework和Java。
1.废弃的方法和类删除。列一下主要删除的方法和类:
scss
SpringBootServletInitializer:在Spring Boot 2.7中,该类已经被移除,建议使用SpringBootServletWebServerApplicationContext来代替。
ServletWebServerFactoryCustomizer:这个接口已经从Spring Boot 2.7中移除,可以使用WebServerFactoryCustomizer来代替。
BasicErrorController:这个类已经从Spring Boot 2.7中移除,可以使用ErrorController接口来代替。
ContentNegotiationStrategy:这个接口已经从Spring Boot 2.7中移除,可以使用RequestMappingHandlerMapping的setContentTypeResolver(ContentTypeResolver)方法来代替。
HttpMessageConverters:这个接口已经从Spring Boot 2.7中移除,可以使用HttpMessageConvertingComparator来代替。
HttpMessageConvertingComparator:这个类已经从Spring Boot 2.7中移除,可以使用ComparatorChain来代替。
ServletWebServerFactoryCustomizerBeanPostProcessor:这个类已经从Spring Boot 2.7中移除。
SpringBootApplicationContextLoader:这个类已经从Spring Boot 2.7中移除,可以使用SpringApplicationWebApplicationContext来代替。
SpringBootServletInitializerAutoConfiguration:这个类已经从Spring Boot 2.7中移除,可以使用SpringBootServletWebServerApplicationContextAutoConfiguration来代替。
此外,还有一些被移除的配置属性,例如spring.http.converters.preferred-json-mapper、spring.jackson.serialization-features.default-pretty-print-xml、spring.jackson.serialization-features.sort-property-names-by-default等。其他信息大家可以看Spring的版本更新说明:
github上的release版本说明:
3. 单元测试升级
在Spring Boot 2.7版本,已经不再依赖JUnit4, 而是将Test换成了 JUnit Jupiter, 这也导致之前单元测试使用的方法和注解会产生变化。
常用的一些方法和注解变化如下:
| 变更项 | JUnit4 | JUnit Jupiter | | @Test注解 | 包路径: org.junit.Test | 包路径: org.junit.jupiter.api.Test | | 断言 | 类:org.junit.Assert | 类:rg.junit.jupiter.api.Assertions ,提供了更简洁的断言方法 | | @RunWith | 需要使用@RunWith注解来指定测试运行器 | @RunWith移除,不再需要,单侧只需要@SpringBootTest即可 | | 参数化接口测试 | @RunWith配合@Parameters实现参数化 | @ParameterizedTest和@ValueSource注解配合使用 |
4. hibernate-validator包依赖问题
Springboot从2.3以后,spring-boot-starter-web中不再引入hibernate-validator,需要手动引入。此处可以直接引用spring-boot-starter-validation的包,里面会间接引用hibernate-validator的包,且版本号可以被spring boot parent统一管理。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
4. 诊断升级兼容性方法
如果是老项目版本升级,Spring Boot 提供了一种在启动时分析应用程序环境并打印诊断信息的方法,而且还可以在运行时为您临时迁移属性。要启用该功能,请将以下依赖项添加到您的项目中:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
b. 行云部署配置
1. 镜像配置
首先需要新的基础镜像包,包括OpenJDK17的。之前行云的镜像市场上是没有相关的镜像的,后来联系科技运维同事帮忙制作了新的镜像,新的镜像包是基于Tomcat应用类型的,大家可以在jdos的中国站的景象市场搜索到。镜像名如下,:
bash
base_tomcat/java-jd-centos7-jdk17-tomcat8.5.42-ngx197:latest
2. 编译配置
编译配置中,需要选择的JDK版本为17,同时Maven的版本也可以尽量选高一些。因为按照惯例,maven的版本会对JDK的版本兼容性有所不同,一般越是高版本的Maven对JDK17兼容性更好。虽然官方没有明确说明Maven版本支持情况,但我们选择高版本的Maven是比较稳妥的选择,所以在JDOS上我们选择maven-3.9.0版本比较好。

3. JVM参数配置
然后就是配置JVM启动参数,我们需要开启ZGC.具体启动参数以4C8G的资源为例,配置参数如下:
ini
-Xms5324m -Xmx5324m -XX:MaxMetaspaceSize=256m -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=983m
-Djava.library.path=/usr/local/lib -server -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Logs
-Djava.awt.headless=true -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000
-Djmagick.systemclassloader=no -Dnetworkaddress.cache.ttl=300 -Dsun.net.inetaddr.ttl=300
-XX:+UseZGC
此处需要注意,ZGC不要配置并行GC线程的数量,并发标记线程数等信息,配置了反而会出现启动报错情况。
c. 兼容性问题说明
关于兼容性问题之前一篇文章里详细介绍了,包括如何兼容京东的UMP, DUCC等。这些中间件的兼容性问题产生主要由于JDK17中对于反射和扫描的安全性检查导致的,一个简单的解决办法是将没开放的module强制对外开放。所以需要一些额外配置。
先整理结论,额外配置集合如下,该集合可以配置在VM 启动参数之中:
css
--add-opens java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/sun.util.calendar=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
--add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
--add-opens java.management/java.lang.management=ALL-UNNAMED
--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
--add-opens java.management/sun.management=ALL-UNNAMED
--add-opens java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/sun.net.util=ALL-UNNAMED
1. SGM依赖需要加入
css
--add-opens java.management/java.lang.management=ALL-UNNAMED
--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
--add-opens java.management/sun.management=ALL-UNNAMED
2. R2M需要加入
csharp
--add-opens java.base/java.time=ALL-UNNAMED
3. DUCC依赖需要加入
csharp
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
--add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/sun.nio.ch=ALL-UNNAMED
4. AKS依赖需要加入
csharp
--add-exports java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/sun.util.calendar=ALL-UNNAMED
5. Pfinder依赖需要加入
csharp
--add-opens java.base/sun.net.util=ALL-UNNAMED
6. Swagger兼容性配置
分两步:
1.properties配置文件中增加一下配置:
ini
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
1.代码在配置类中新增BeanPostProcessor重写:
typescript
/**
* 增加如下配置可解决Spring Boot 2.7.15 与Swagger 3.0.0 不兼容问题
**/
@Bean
public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
List<T> copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null).collect(Collectors.toList());
mappings.clear();
mappings.addAll(copy);
}
@SuppressWarnings("unchecked")
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
try {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
field.setAccessible(true);
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
};
}
7. JDK维度兼容性问题(只挑我遇到的问题重点说)
•JDK11就删除了javaFX库,所以该库下的所有方法在JDK17中不可用。如果你是从JDK8直接升级到JDK17, 需要注意,javaFX下的javafx.util包方法有可能会被大家不小心用到。
以下列举一下javafx.util下的一些常用工具类(项目中尽量不要再用):
| 类名 | 方法说明 | | javafx.util.Pair | getKey():获取 Pair 对象的键。 getValue():获取 Pair 对象的值。 setKey(K key):设置 Pair 对象的键。 setValue(V value):设置 Pair 对象的值。 | | avafx.util.Duration | toSeconds():将持续时间转换为秒。 toMillis():将持续时间转换为毫秒。 toNanos():将持续时间转换为纳秒。 add(Duration other):将另一个持续时间添加到当前持续时间。 subtract(Duration other):从当前持续时间中减去另一个持续时间。 | | javafx.util.converter | fromString(String value):将字符串值转换为目标类型。 toString(T value):将目标类型的值转换为字符串。 | | javafx.util.StringConverter | fromString(String value):将字符串值转换为目标类型。 toString(T value):将目标类型的值转换为字符串。 |
•其次, Java EE(Java Enterprise Edition)规范在 Java 9 之后被重新命名为 Jakarta EE。这是由于 Java EE 规范的开源版本迁移到了 Eclipse Foundation,并改名为 Jakarta EE。
因此有一些包名路径变更,为了兼容JSF,需要手动引入一些JAR包。但由于我们部署环境采用的是外置的Tomcat8,所以还是包含java EE的相关包。不需要额外加入,但本地debug时,需要加入。
尽管 Jakarta EE 是 Java EE 的继任者,但为了保持向后兼容性,许多 Java EE 规范和 API 在 Jakarta EE 中仍然存在,并且在 Jakarta EE 中的命名空间从 javax 变为 jakarta。且一些命名空间也改变了,比如javax包下的方法和属性都不能再试用,例如: javax.xml.bind.*
更改为jakarta.xml.bind.*
。以下有一个该问题引起的JSF报错修复:
xml
关于JSF启动有报错信息:运行时找不到 javax.xml.bind.JAXBException 类。在 JDK 9 及更高版本中,javax.xml.bind 包被移除了,并且不再包含在标准的 Java SE 中。
如果您的项目依赖于 JAXB API,您可以尝试以下解决方法之一:
如果您使用的是 JDK 8 或更早版本,请确保您的项目使用的是兼容的 JDK 版本。
如果您使用的是 JDK 9 或更高版本,并且需要使用 JAXB API,您可以添加以下依赖项来解决该问题:
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>3.0.1</version> <!-- 根据您的需求选择合适的版本 -->
</dependency>
•此处正好多说一下Spring boot 3.0的一个小问题,@Resource在Spring boot 3.0上,已经不再依赖javax.annoation包,所以包路径也由javax.annotation.Resource改为了jakarta.annotation.Resource。当然此处在2.7版本依然兼容,可以不用修改。
以下贴一个报名改动对比图:
| module | packages | replacement groupId | replacement artifactId | | java.activation | javax.activation | com.sun.activation | jakarta.activation | | java.xml.ws.annotation | java.annotation | jakarta.annotation | jakarta.annotation-api | | java.xml.bind | javax.xml.bind.* | jakarta.xml.bind.com.sun.xml.bind | jakarta.xml.bind-apijaxb-impl |
垃圾回收器的话,从JDK14开始,已经删除了CMS,所以在JDK17下,只建议使用ZGC。
还有一个最大的变化是之前的--illegal-access
参数不在可用,如果在java 17使用这个参数访问受限的api则会报出InaccessibleObjectException
,大多数情况下只要升级了依赖项是不会碰到这个情况的,但如果出现问题,则可以使用--add-opens
来对不可访问的api授权。以上的SGM,R2M,DUCC,AKS,Pfinder的兼容性问题都是因为这个特性变化引起的。
脚手架支持
目前最的DDD脚手架已经支持Spring Boot 2.7.17 和JDK17 ,下载脚本如下:
ini
mvn archetype:generate \
-DarchetypeGroupId=com.jd.magnus \
-DarchetypeArtifactId=magnus-multi-ddd-archetype \
-DarchetypeVersion=1.0.0-SNAPSHOT \
-DinteractiveMode=false \
-DarchetypeCatalog=remote \
-Dversion=1.0.0-SNAPSHOT \
-DgroupId=com.jdl.sps \
-DartifactId=bff-demo1
该脚手架以在京东内部申请为开源项目,开源项目地址如下:
IDE配置注意事项
1.在Idea中,需要安装JDK17,然后项目需要配置对应的JDK17版本,截图如下:

1.大家有两种选择:1. 直接安装Idea JDK17插件。 2. 或者自己下载OpenJDK17安装,然后绑定路径。
modules也需要更改Language level, 截图如下:

1.注意:对外的JSF client JDK还要用JDK8进行打包,也不要在client JDK中使用JDK17的新语法特性。因为外部依赖系统不一定是JDK17版本的。
2.本地Debug注意事项。
当本地debug时,需要配置debug的启动参数,配置VM options,并将上面【行云部署上的实践方案-c.兼容性问题说明】章节中所说的兼容性问题配置添加到该处。如下图所示:

此方法也适用于本地debug JUnit Test单元测试本地调试时用。当然,如果为了避免每个单元测试都要手动配置,可以点击图中的Edit configuration templates,配置模板,做到一劳永逸。具体配置如下图所示:

1.Maven test注意事项。
同理,在使用maven命令对工程进行test命令时,也需要额外配置启动参数,来兼容JDK17订单安全性检查,需要在pom文件中的maven-surefire-plugin增加如下配置:

总结
目前,将部门内的京旗API服务, 发货平台BFF服务,物流发货商家基础信息服务作为试点,已经在行云测试环境上用JDK17+Spring Boot2.7版本进行试运行,京东三方依赖包括JSF lite版本,ducc, easyJob,jmq, 云redis, ump, pfinder。经测试,兼容性没有太大问题,服务可用。后续还会进一步观察和测试。
据我了解,现在开源社区里,以apache为代表的大型开源项目都对JDK17有了不错的兼容, 未来可以逐步再从Spring Boot 2.7升级到Spring Boot 3.0。
作者:京东物流 赵勇萍
来源:京东物流 自猿其说 Tech 转载请注明来源