性能加速包: SpringBoot 2.7&JDK 17,你敢尝一尝吗 | 京东物流技术团队

01/23 10:11
阅读数 46

前言

众所周知,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官方给出了这样的说明:

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下载地址

提供了一个下载地址: https://adoptium.net/zh-cn/temurin/releases/?version=17&os=linux&arch=x64

行云部署上的实践方案

a. Spring Boot 2.7

1. pom.xml版本依赖

实践的版本选择上我选择了2.7大版本下的最新小版本,即Spring Boot 2.7.17版本。pom.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.废弃的方法和类删除。列一下主要删除的方法和类:
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版本说明:

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.7-Release-Notes

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统一管理。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

4. 诊断升级兼容性方法

如果是老项目版本升级,Spring Boot 提供了一种在启动时分析应用程序环境并打印诊断信息的方法,而且还可以在运行时为您临时迁移属性。要启用该功能,请将以下依赖项添加到您的项目中:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-properties-migrator</artifactId>
    <scope>runtime</scope>
</dependency>

b. 行云部署配置

1. 镜像配置

首先需要新的基础镜像包,包括OpenJDK17的。之前行云的镜像市场上是没有相关的镜像的,后来联系科技运维同事帮忙制作了新的镜像,新的镜像包是基于Tomcat应用类型的,大家可以在jdos的中国站的景象市场搜索到。镜像名如下,:

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的资源为例,配置参数如下:

-Xms5324m  -Xmx5324m  -XX:MaxMetaspaceSize此处需要注意,ZGC不要配置并行GC线程的数量,并发标记线程数等信息,配置了反而会出现启动报错情况。

 

c. 兼容性问题说明

关于兼容性问题之前一篇文章里详细介绍了,包括如何兼容京东的UMP, DUCC等。这些中间件的兼容性问题产生主要由于JDK17中对于反射和扫描的安全性检查导致的,一个简单的解决办法是将没开放的module强制对外开放。所以需要一些额外配置。

先整理结论,额外配置集合如下,该集合可以配置在VM 启动参数之中:

--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依赖需要加入

--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需要加入

--add-opens java.base/java.time=ALL-UNNAMED

3. DUCC依赖需要加入

--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依赖需要加入

--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依赖需要加入

--add-opens java.base/sun.net.util=ALL-UNNAMED

6. Swagger兼容性配置

分两步:

1.properties配置文件中增加一下配置:
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

1.代码在配置类中新增BeanPostProcessor重写:
/**
 * 增加如下配置可解决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 .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报错修复:

关于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 ,下载脚本如下:

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

该脚手架以在京东内部申请为开源项目,开源项目地址如下:

http://xingyun.jd.com/shendeng/openSource/detail/793

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。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部