性能加速包:SpringBoot 2.7&JDK 17,你敢尝一尝吗

原创
2023/12/20 18:00
阅读数 80



一、前言

众所周知,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的动力主要源于两个方面:一是更为安全的语言特性,二是更加优异的垃圾回收器和性能提升。

3.1 安全的语言特性

  • 安全性首先体现在JDK17对于包扫描和反射的权限控制,可能大家对当年的FastJson漏洞记忆犹新,它的病根在于对反射的滥用。而对于这种反射的滥用,在JDK17里有了更严格的控制。

JDK 17对反射进行了优化,主要表现在对反射调用进行了权限控制。具体来说,它通过setAccessible()方法启动或禁止访问安全检查开关。当参数值为true时,反射的对象在使用时取消安全检查,提高反射的效率;当参数值为false时,反射的对象执行安全检查。这样的优化使得在处理反射调用时,可以更加灵活地控制访问权限。

  • 除此之外,JDK 17增强了包扫描的权限控制。在之前的版本中,Java的包扫描是基于类的,而在JDK 17中,它扩展到了对整个包的权限控制。这使得开发者可以更加精细地控制对特定包的访问权限。

  • 针对于语法本身,引入了密封的类和接口,具体使用细节大家可以网上查看。通过密封类和接口,进一步增加了面向对象开发的封闭性,提升代码质量的安全可靠。

3.2 垃圾收集器

JDK17引入了ZGC作为垃圾收集器,此处引用一下曲振富老师做的关于不同垃圾收集器在不同JDK版本下的压测结果:

压测服务背景:

DOS平台上选择了不同配置的机器(2C4G、4C8G、8C16G),并分别使用JDK8、JDK11和JDK17进行部署和压测。整个压测过程限时60分钟,用180个虚拟用户并发请求一个接口,每次接口请求都创建512Kb的数据。最终产出不同GC回收器的各项指标数据,来分析GC的性能提升效果。

以上的压测结果对于我们来说非常诱人,ZGC配合JDK17的性能对于其他JDK和垃圾收集器组合来说是碾压获胜,且不论任何机器配置下,都推荐使用ZGC,ZGC的停顿时间达到亚毫秒级,吞吐量也比较高。这个对于一个高并发业务场景下,对于资源的优化是非常客观的。

3.3 OpenJDK17下载地址

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



四、行云部署上的实践方案

4.1 Spring Boot 2.7

4.1.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>

4.1.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。

  • 废弃的方法和类删除。列一下主要删除的方法和类:

  
  
  
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 

4.1.3 单元测试升级

在Spring Boot 2.7版本,已经不再依赖JUnit4, 而是将Test换成了 JUnit Jupiter, 这也导致之前单元测试使用的方法和注解会产生变化。

常用的一些方法和注解变化如下:

4.1.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.1.5 诊断升级兼容性方法

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

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

4.2 行云部署配置

4.2.1 镜像配置

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

  
  
  
base_tomcat/java-jd-centos7-jdk17-tomcat8.5.42-ngx197:latest

4.2.2 编译配置

编译配置中,需要选择的JDK版本为17,同时Maven的版本也可以尽量选高一些。因为按照惯例,maven的版本会对JDK的版本兼容性有所不同,一般越是高版本的Maven对JDK17兼容性更好。虽然官方没有明确说明Maven版本支持情况,但我们选择高版本的Maven是比较稳妥的选择,所以在JDOS上我们选择maven-3.9.0版本比较好。

4.2.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线程的数量,并发标记线程数等信息,配置了反而会出现启动报错情况。

4.3 兼容性问题说明

关于兼容性问题曲振富老师在他的文章里很详细得介绍了,包括如何兼容京东的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

4.3.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

4.3.2 R2M需要加入

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

4.3.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.3.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

4.3.5 Pfinder依赖需要加入

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

4.3.6 Swagger兼容性配置

分两步:

  • properties配置文件中增加以下配置:

  
  
  
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
  • 代码在配置类中新增BeanPostProcessor重写:

  
  
  
/** * 增加如下配置可解决Spring Boot 2.7.15 与Swagger 3.0.0 不兼容问题**/@Beanpublic 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); } } }; }

4.3.7 JDK维度兼容性问题(只挑我遇到的问题重点说)

  • JDK11就删除了javaFX库,所以该库下的所有方法在JDK17中不可用。如果你是从JDK8直接升级到JDK17, 需要注意,javaFX下的javafx.util包方法有可能会被大家不小心用到。

以下列举一下javafx.util下的一些常用工具类(项目中尽量不要再用):

  • 其次, 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版本依然兼容,可以不用修改。

以下贴一个报名改动对比图:

垃圾回收器的话,从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


六、IDE配置注意事项
  • 在Idea中,需要安装JDK17,然后项目需要配置对应的JDK17版本,截图如下:

  • 大家有两种选择:a. 直接安装Idea JDK17插件。b. 或者自己下载OpenJDK17安装,然后绑定路径。

modules也需要更改Language level, 截图如下:

  • 意:对外的JSF client JDK还要用JDK8进行打包,也不要在client JDK中使用JDK17的新语法特性。因为外部依赖系统不一定是JDK17版本的。

  • 本地Debug注意事项:

当本地debug时,需要配置debug的启动参数,配置VM options,并将上面【行云部署上的实践方案-c.兼容性问题说明】章节中所说的兼容性问题配置添加到该处。如下图所示:

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

  • 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。

-end-


本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

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