一、背景
项目升级JDK11,主要基于以下几个出发点:
- 目前公司使用的JDK8,在2019年1月已经停止更新维护
- 越来越多的框架和第三方库新版本不再兼容低版本JDK,如果想使用新版本的特性就必须升级。以下是一些常见的框架和第三方库,在其最新版本中需要使用JDK11及以上版本:
- Spring Framework 5.3及以上版本
- Hibernate ORM 5.4及以上版本
- Apache Tomcat 10.0及以上版本
- Jetty 10.0及以上版本
- Apache POI 5.0及以上版本
- Apache Lucene 8.7及以上版本
- JUnit 5.7及以上版本
- JDK21即将在2023年9月发布,距离JDK8已经相差了10多个版本,这些版本不断引入的新特性对于提高服务的启动、性能和内存使用情况将会有很多帮助
因此JDK升级是一个必要的过程,即使不是为了获取新特性,也应该考虑升级以获得最新的安全更新和性能优化。
二、版本选择
JDK发布周期通常是每6个月发布一个新版本(非LTS),而每个三年左右发布一个LTS(Long-term support)版本。LTS版本提供了长期的支持和维护,企业通常会优先选择LTS版本进行开发和部署。LTS版本受到官方支持和维护,因此可以得到长期的安全更新和错误修复。相比之下,非LTS版本只获得较短时间的支持,在短时间内很可能会被弃用。
目前JDK的四个LTS版本中主要以JDK8、JDK11为主,如下图所示:
对于本次升级来说,有JDK11、JDK17两个版本可供选择。
从长远来看,JDK17更代表未来,但是目前JDK11使用更加广泛,相关的升级指南和文档更加丰富,因此升级到JDK11比升级到JDK17的挑战要相对小,并且基于风险的控制,从JDK8升级到JDK11,随后再从JDK11升级到JDK17是一个更合理的选择。
因此本次升级选择JDK11,同时相应的Tomcat 7升级到Tomcat 8.5。
三、JDK11版本特性
升级前了解JDK11版本特性的必要性是非常重要的。不同版本之间会有新功能,架构变化、安全修复和性能优化等方面的不同特性。如果不了解这些新特性,就容易在应用程序开发和部署过程中遇到问题。以下是JDK11的新特性:
引入ZGC[(可伸缩,低延迟的gc)
相关JEP 333: ZGC: A Scalable Low-Latency Garbage Collector(Experimental)
JDK11引入了ZGC(实验性质,不建议用到生产环境)。ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),这应该是 Java 11 中最为瞩目的特性,没有之一。
ZGC 是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计:
- GC 停顿时间不超过 10ms
- 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆
- 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比)
- 方便在此基础上引入新的 GC 特性和利用 colord
- 针以及 Load barriers 优化奠定基础
- 当前只支持 Linux/x64 位平台
HTTP Client API(正式特性)
对Http Client API 进行了标准化,并且现在完全支持异步非阻塞,该 API 通过 CompleteableFutures 提供非阻塞请求和响应语义,可以联合使用以触发相应的动作。新 Http Client API,提供了对 HTTP/2 等业界前沿标准的支持,同时也向下兼容 HTTP/1.1,精简而又友好的 API 接口,与主流开源 API(如:Apache HttpClient、Jetty、OkHttp 等)类似甚至拥有更高的性能。
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://openjdk.java.net/"))
.build();
client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
Docker 容器改进
在JDK10之前,Docker容器中运行Java应用程序一直存在一个问题:容器中运行JVM程序在设置内存大小和CPU使用率后,会导致应用程序的性能下降,这是因为JVM无法识别在容器上设置的内存和CPU约束。
从JDK10开始,JVM会使用容器控制组 (cgroups) 设置的约束来设置内存和CPU限制。另外还添加了“JVM 选项”,使Docker容器用户可以精细地控制用于Java堆的系统内存量。
移除内容
这些Java EE 或 CORBA 模块中的包在JDK9弃用,在JDK11中删除。因此升级过程中,项目中使用到则需要手动引入
四、Tomcat版本区别
Tomcat 7和Tomcat 8.5之间存在以下几个区别:
- Servlet规范:Tomcat 7支持Servlet 3.0规范,而Tomcat 8.5支持Servlet 3.1规范。
- WebSocket支持:Tomcat 8.5对WebSocket的支持更好,Tomcat 7对WebSocket支持较弱。
- JSP规范:Tomcat 7支持JSP 2.2规范,而Tomcat 8.5支持JSP 2.3规范。
- 远程配置:Tomcat 8.5引入了远程配置API,允许在运行时动态修改Tomcat的配置文件,而Tomcat 7没有这个功能
- 对多线程的支持:Tomcat 8.5对高并发的支持更好,能够处理更多的请求,而Tomcat 7在高并发时可能会出现性能瓶颈。
- 加载jar包的顺序不同
其中,加载jar包的顺序会直接影响现有项目是否可以正常启动、运行。
Tomcat加载jar
Tomcat加载jar顺序、以及对应类加载器如下所示:
- Bootstrap ClassLoader类加载器:加载$java_home/lib 目录下的java核心api
- Extension ClassLoader类加载器: 加载$java_home/lib/ext 目录下的java扩展jar包
- Application ClassLoader类加载器:加载java -classpath/-Djava.class.path所指的目录下的类与jar包
- Common ClassLoader类加载器:$CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载
- Catalina ClassLoader类加载器:$CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载
- Shared ClassLoader类加载器:$CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载
- Web App1 ClassLoader类加载器:项目路径/WEB-INF/classes下的class文件
- Web App2 ClassLoader类加载器:项目路径/WEB-INF/lib下的jar文件
Tomcat 7通过WebappLoader.class加载WEB-INF/lib的jar包,然后在FileDirContext.class中获取文件列表后中按照文件名称首字母a-z进行排序后加载,源码如下:
Tomcat 8.5通过WebappClassLoaderBase.start()加载WEB-INF/lib的jar包,然后DirResourceSet.class中获取文件列表直接进行加载,并没有进行排序,从而导致在不同的操作系统中返回的结果不一致(Windows系统按照文件名称排序,Linux系统乱序)。源码如下:
导致的问题
由于加载jar包的顺序不同,可能会导致无法加载到原定的类。如图所示
- 项目中存在两个相同的类LogbackServletContainerInitializer.class(packge相同,jar包不同),项目当前加载的是a-suishen-patch-libs中的
- 在Tomcat 7中可以通过设置jar包的名称保证a-suishen-patch-libs中的先加载
- 但是在Tomcat 8.5就无法保证加载顺序,最终导致项目无法启动
解决方式
Tomcat 8.5在StandardRoot中的list方法指定了加载资源的顺序,源码如下:
因此,我们可以通过将需要先加载的jar设置在PreResources里,确保优先加载。
设置方式:在项目的META-INF/context.xml中配置
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resources>
<PreResources base="${catalina.base}/webapps/${project}/WEB-INF/lib/a-suishen-patch-libs-1.0-SNAPSHOT.jar"
className="org.apache.catalina.webresources.JarResourceSet"
webAppMount="/WEB-INF/classes"/>
</Resources>
</Context>
五、升级步骤
步骤一:代码修改
- JDK升级代码修改:可参考https://learn.microsoft.com/zh-cn/java/openjdk/transition-from-java-8-to-java-11
- jar包记载顺序设置
- 第三方依赖升级(按需):
- javassist升级至3.23.1-GA
- ASM升级至7.0
- Byte Buddy升级至1.9.0
- cglib升级至3.2.8
- Mokito升级至2.20.0
- 。。。。
步骤二:设置JVM参数
-Xms3000m -Xmx3000m -XX:+UseCompressedOops -XX:+PrintGCDetails -Xloggc:./logs/gc.log -XX:+TieredCompilation -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heap -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:MaxTenuringThreshold=15 -XX:+AlwaysPreTouch
步骤三:项目发布
发布过程中,观察cataliana、localhost、项目日志是否有异常,项目是否启动成功。
升级注意事项:
- cataliana、localhost、系统日志都需查看下是否有异常
- 重要业务发布线上时一定要谨慎,必须进行全面测试
六、升级问题总结
以下是项目升级过程中遇到的问题以及对应的解决办法。
模块移除一:NoClassDefFoundError
异常如下所所示:
Caused by: java.lang.NoClassDefFoundError: javafx/util/Pair
at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3166)
at java.base/java.lang.Class.getDeclaredMethods(Class.java:2309)
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:613)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:524)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:510)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:247)
… 31 more
Caused by: java.lang.ClassNotFoundException: javafx.util.Pair
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1420)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1228)
… 38 more
原因:JDK11移除了javafx
解决方式:手动引入依赖
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>11</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>11</version>
</dependency>
模块移除二:NoClassDefFoundError
异常如下所所示:
[org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]:
Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1080)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
原因:Java 11 删除了 java.xml.bind (JAXB)
解决方式:手动引入依赖
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
jar包冲突 NoSuchMethodError
异常如下所所示:
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'authorityMenuServiceImpl': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'roleMenuServiceImpl': Invocation of init method failed;
nested exception is java.lang.NoSuchMethodError: org.apache.commons.beanutils.BeanUtilsBean.<init>(Lorg/apache/commons/beanutils/ConvertUtilsBean;)V
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:321)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1272)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
原因:具有相同packge的BeanUtilsBean.class在多个jar中出现,Tomcat8.5加载了项目不需要的类,导致无法找到对应的方法
解决方式:通过查看依赖树,确定具体的冲突依赖,排除不需要的jar
mvn dependency:tree
或
./gradlew app:dependencies
javassist依赖版本不匹配
异常如下所所示:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'hessianAppService': Instantiation of bean failed; nested exception is java.lang.ExceptionInInitializerError
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1163)
Caused by: java.lang.NullPointerException: null
at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:103)
at javassist.util.proxy.DefineClassHelper.toClass3(DefineClassHelper.java:151)
at javassist.util.proxy.DefineClassHelper.toClass2(DefineClassHelper.java:134)
at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:95)
at javassist.ClassPool.toClass(ClassPool.java:1143)
at javassist.CtClass.toClass(CtClass.java:1316)
at com.alibaba.dubbo.common.compiler.support.JavassistCompiler.doCompile(JavassistCompiler.java:123)
at com.alibaba.dubbo.common.compiler.support.AbstractCompiler.compile(AbstractCompiler.java:59)
at com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler.compile(AdaptiveCompiler.java:46)
at
原因:引入的javassist的版本(3.21.0-GA)和JDK11不匹配
解决方式:javassist升级到3.23.1-GA版本
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.21.0-GA</version>
</dependency>
log4j循环依赖配
异常如下所所示:
org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/admin]]
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:440)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:198)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:753)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.StackOverflowError
at org.apache.log4j.Category.<init>(Category.java:55)
at org.apache.log4j.Logger.<init>(Logger.java:37)
at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)
原因:log4j-over-slf4j.jar 和 slf4j-log4j12.jar、或jcl-over-slf4j.jar和slf4j-jcl.jar循环依赖导致溢出
解决方式:需要排除slf4j-log4j12、或者slf4j-jcl.jar
参考文档:
https://blog.csdn.net/qq_19749625/article/details/126893801
https://www.slf4j.org/codes.html#log4jDelegationLoop
跨域问题
异常如下所所示:
01-Mar-2023 14:29:17.159 INFO [localhost-startStop-1] org.apache.catalina.core.ApplicationContext.log Initializing Spring root WebApplicationContext
01-Mar-2023 14:29:36.839 SEVERE [localhost-startStop-1] org.apache.catalina.core.StandardContext.filterStart Exception starting filter [CorsFilter]
javax.servlet.ServletException: It is not allowed to configure supportsCredentials=[true] when allowedOrigins=[*]
at org.apache.catalina.filters.CorsFilter.parseAndStore(CorsFilter.java:764)
at org.apache.catalina.filters.CorsFilter.init(CorsFilter.java:185)
at org.apache.catalina.core.ApplicationFilterConfig.initFilter(ApplicationFilterConfig.java:281)
at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:262)
at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:105)
at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4604)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5255)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:753)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:727)
原因:tomcat高版本在处理跨域问题时,当allowedOrigins=[*]时,不允许配置supportsCredentials=[true]
解决方式:配置supportsCredentials=[false]或者删除、或者考虑使用"allowedOriginPatterns"代替
七、展望
在项目升级到JDK11后,后续希望能更充分的利用JDK11、Tomcat8,5生态带来的便利性,探索启动优化、开发便捷性带来的性能提升。
作者介绍
- 孙景亮 资深服务端开发工程师