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

京东云开发者 · 2023年12月19日 · 1526 次阅读

前言

众所周知,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=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 启动参数之中:

--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 = 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 报错修复:

关于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。

作者:京东物流 赵勇萍

来源:京东物流 自猿其说 Tech 转载请注明来源

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册