Spring Boot 从2.1.x 升级到3.0.x的艰辛历程

您所在的位置:网站首页 springboot2和3学哪个 Spring Boot 从2.1.x 升级到3.0.x的艰辛历程

Spring Boot 从2.1.x 升级到3.0.x的艰辛历程

2024-02-01 08:39| 来源: 网络整理| 查看: 265

为什么要升级

凡是因果,为什么吃饱了撑着要从2.1.x升级到3.0.x,这不是没事找事做吗?一次性到位该用什么版本的就用什么版本的不好吗。一个项目不应该确定他JDK的版本和Springboot的基本版本吗?既然确定了你改什么,升什么级。这是嫌程序员的时间太多了找点事给他们做?

初一开始我也这么想,为啥呀,我闲的蛋疼啊?好不容易实现业务了,各种测试也通过了。这个时候跟我说要做升级。为啥当初一开始不讨论清楚呢?

其实在这个升级的过程中我也渐渐认清了应用的商业价值。慢慢理解了,必须要有人去做这个。所以先说说这件事发生的来龙去脉吧。

最起初我们选择了JDK8 + Springboot 2.1.6.RELEASE版本来做基础版本。然后就陆续经过了3个月的开发+3个月的测试阶段。基本上感觉快告一段落了。该修复的bug都已经修复完了。什么SonarQube Scan, VeraCode Scan这些也都做了,然后针对报告里面所有提示的漏洞(vulnerabilities)也做了fix。然后就翘脚等着客户完成了UAT后准备正式上线。可是上线前,客户给了一个Prisma报告。(又来一个新的报告)。该报告显示,我们有144个漏洞等待修复

突然觉得很懵逼,不是SonarQube Scan, VeraCode Scan已经修复完了吗,怎么跑出来一个Prisma,又扫出这么多漏洞?结果一看。发现是他的扫描内容跟前两者完全不同,他是连着Springboot以及JDK和运行环境的所有相关风险漏洞都做了一次全面扫描。而这次的144,都是跟我们使用的各种开源包相关的(包括Springboot 2.1.6.RELEASE这个版本所带的所有包)。

升级第一阶段 分析

首先需要分析Prisma report, 看看这里面有那些地方被影响到了。

这里分为三个部分:

第一部分:

是compliances部分,这里只有1个问题,而且只是跟环境有关系的,简而言之就是说在创建image或者运行container的时候因该使用Non-root-user。这个因该不用过多解释。你给了应用运行环境这么大的权限,就不怕攻击死你。所以在创建image和运行container的时候需要使用非root的user来操作。

第二部分:

是vulnerabilities中的,也是跟运行container的内核有关系的部分。就是说当前使用的Linux内核有漏洞,应该升级Linux内核版本。这个直接在dockerfiles里面修改下内核版本就解决了。这部分问题有3个。所以剩下的141个vulnerabilities都是关于项目中使用的lib有漏洞的。

第三部分:

是lib相关的vulnerabilities。这部分就是需要我们来解决的。而这部分分析下来也包括3个部分

零散非Springboot依赖部分:

我们在开发项目的时候偶尔会依赖一些springboot之外的一些开源的包,像自己引入的测试包,xml或者json解析包,http连接包,数据库驱动包等等等等...这一类的lib通常是自己显示的放入到pom的依赖中的。mvn repository

Spring核心包和各个starter部分:

当我选择了一个版本的spring-boot-starter-parent。就意味着我所有相关的starter都会引入一个固定的版本。在Report中,其中有个重要的漏洞就是跟Springboot 2.1.6.RELEASE这个主版本相关的。他说spring-boot的这个版本有漏洞,所以需要升级到跟高版本。然后里面例如:

spring-boot-starter-web spring-boot-starter-thymeleaf

......

这些starter的版本就是基于springboot的基础版本的。然后报告中说这些也有漏洞。

然后又说在一些starter中还会依赖一些starter,例如在spring-boot-starter-web中我们还依赖:

spring-boot-autoconfigure spring-boot-starter-logging

......

然后报告中说这些也有漏洞。

这部分是比较难分析的。所以在这里推荐两个网站。

1. mvn repository

这个就是我们通常查找maven或者Gradle依赖的网站。

可以在这里面找到各个lib的各种版本,然后点进去,就能够看到当前版本是否有漏洞,然后他引用的一些lib是否有潜在的漏洞。例如,我查看了spring-boot的2.1.6.RELEASE版本,发现他自身就有一个漏洞,他的依赖中也存在96个潜在漏洞。(只是潜在漏洞,也就是你使用了他下面的某个功能,正好这个功能所依赖的这个lib有漏洞,那就会存在,如果不在项目中用到的功能,即使有漏洞,只要不依赖这个lib,就不会下载。因此也不存在漏洞)。下图就是当时查询的结果页面。

2. spring-boot reference (3.0.13)

这个就是显示每个大版本的springboot里面其他相关的引用lib的小版本都是什么。当前给的链接时3.0.13这个springboot大版本的相关文档,如果你需要查看你指定的version,请打开这个链接后把中间的version那部分替换成你自己的version。有了这个工具,就能清楚我应该替换里面哪个小版本的有漏洞的lib。

Springboot用到的starter里面的非Springboot的工具类:

这部分最复杂,就是在项目中用到了某些工具,但是这些工具我又没有直接依赖,而是通过springboot或者springboot的某个lib的依赖得来的。例如我使用的log4j和是slf4j的相关包,就是从我依赖的spring-boot-starter-web -> spring-boot-starter -> spring-boot-starter-logging中得到的。藏的很深是吧。但是都要给他挖出来修理修理。同理,用上面的两个工具,分析哪个相关的版本没有漏洞,然后再看看当前springboot大版本是否支持这个lib版本的大版本。再来说是否能够替换。

第一阶段修复 修复大版本

经过分析和讨论,我们确定了,当前2.1.6.RELEASE的确存在太多问题。首先大版本本身就有漏洞。其次,他依赖中的漏洞很多。所以经过上面两个工具的分析,得到结论是先升级大版本到2.7.13再说。原因是这个版本比较稳定,本身也没有漏洞,依赖漏洞再2.x的springboot中也算是最少的。而且当大版本升级到了2.7.13后,很多相关的starter的版本也会升级到更高版本。并且我们其中跟spring相关的几个有漏洞的包,大版本升级到2.7.13后正好他们的小版本就没有漏洞了。

Package Name

2.1.6.RLEASE

2.7.13spring-expression5.1.8.RELEASE5.3.28spring-beans5.1.8.RELEASE5.3.28spring-webmvc5.1.8.RELEASE5.3.28thymeleaf-spring53.0.11.RELEASE3.0.15.RELEASEspring-boot-autoconfigure2.1.6.RELEASE2.7.13spring-core5.1.8.RELEASE5.3.28spring-expression5.1.8.RELEASE5.3.28

这个阶段还算算是比较容易。首先替换好springboot大版本后,因为2.x都是使用的JDK1.8。所以还不会存在大问题。最多就是maven的编译打包clean等版本要提高了。所以本地maven的版本需要匹配。否者maven无法进行编译,或者修改build中的maven的各种plugins的版本到低版本maven都能运行的那个版本也就能通过了。

修复剩余spring问题

经过大版本的升级后,发现没有太多问题。运行也正常,但是还有一些spring相关的包依然没有办法达到没有漏洞的那个版本。这时候就需要手动的去修改pom相关的部分了。这里有几个方法

从某个spring的依赖中excluded掉本身的那个lib,然后自己写一个dependency来以来一个一样的lib,并且设定版本为指定版本。(这个可以成功,但是不推荐,原因自己查)分析并直接excluded掉有问题版本的lib。 因为可能一个项目中有多个lib都会针对某个lib有不同版本的引用。因此,排除掉那些不合格的lib,最后保留合格的。或者因为我们使用了一个lib,它里面某个lib是有漏洞的,但是我们项目并不会使用。因此直接排除,不需要再引入。pom中里面配置相关spring依赖的lib的版本。(强烈推荐)

         这里就要说到,当我们在pom中点开spring-boot-starter-parent的依赖是,在这个依赖的pom中会有一个spring-boot-dependencies的依赖。再点开这个依赖。你能够看到一个properties的列表

所以只要你能够找到的spring相关package的version我们都能够在这里面找到。

然后复制你想要修改版本的那一条,放到你自己的pom的properties配置当中。修改你想要的版本号。(事先查看下这个版本是否适合当前spring用,会否有冲突)。

当重新import包的时候,这个lib的版本就会变成你properties当中指定的版本。当然,这种修改适合小版本的变化,毕竟你不知道版本跨度过大,是否会有影响。例如我的commons-lang3.version本来是3.11.0.我改成3.12.0还是可以的。

在这个阶段,再推荐一个IDEA的maven插件

通过这个maven的插件,能够做pom的依赖分析。并可以找出里面是否有包冲突,每个包是通过那个或者哪些包引入的,版本又是多少。

 修复其他依赖版本问题

这个应该是最轻松的,因为是自己放到pom的依赖。改一下版本号到没有漏洞的。然后做一下简单的测试看是否适配就行了。要注意的是,这个时候可以看到,你自己导入的依赖。有可能在spring或者其他包里面其实有,但是版本不一样。所以这个时候你可以分析一下是否继续使用你的依赖版本,或者别的包导入进来的版本更适合。或者是解决他们的冲突。

升级第二阶段 分析

其实第一阶段升级到2.7.13版本的springboot后,还是挺顺利的。启动项目上也基本上没有遇到太多问题。但是最后我们完成了升级后发现还有两个问题,是关于spring-boot-starter-web和tomcat-embed的,它里面有个漏洞,说是必须要升级到6.x以上才能修复,其他方式修复不了。然后我们查看了spring-boot-starter-web如果要支持6.x的话,Springboot大版本至少要3.x才行。然后我们准备开始升级3.x这个时候问题才陆续出现了。首先,查了下,如果要支持springboot3.x的话,需要JKD17。而如今我们的JDK才是1.8。但是必须要处理漏洞风险。只有硬着头皮上了。

幸运的是,翻找资料告诉我们,如果想要把springboot2.x升级到3.x的话,最好先把当前springboot2.x升级到至少2.7以上再说。正好我们升级到了2.7.13了。省了很多事。

经过研究,我们发现springboot3.0.12是最稳定的,而且在这里面不管web的版本和tomcat-embed的版本能够到没有漏洞的那个版本上。

继续升级

升级过程中会遇到各种各样的问题,我这里只说说我升级的时候遇到的一些问题,不代表大多数人遇到的。因为我在网上搜索,发现人家遇到的问题我没有遇到,但是我遇到的问题别人也不一定遇到过。所以希望补全一点,让大家以后再升级的过程中如果遇到我相同的问题可以借鉴。

 升级帮助1, 使用spring的migrator依赖 org.springframework.boot spring-boot-properties-migrator runtime

   在准备升级到3.x的时候因为知道区别很大,可能会遇到很多问题,为了帮助解决各种migration的问题,springboot提供了一个新的migrator模块。一旦添加为项目依赖。这个不仅会在启动时候分析应用程序的环境并打印诊断信息,还会再运行时做属性的临时迁移。所以在做升级的过程中可以导入这个依赖。升级完成了再把它拿掉。

升级问题1 JDK和IDEA

首先,Springboot 3.x之后的版本需要JDK17的支持,所以先下载一个OpenJDK17放到某个目录下

然后,我之前的IDEA版本是2020.3 Community版,该版本最高支持JDK15。再上去的版本就没法编译没法运行了。所以我下了一个2021.2.3的 Community版。(有人说至少需要2021.2.4,但是我试过该版本就已经能够支持JDK17了)此时,打开IDEA,导入项目,然后把当前IDEA的所有相关JDK的依赖改成安置好的JDK17(项目maven编译依赖,运行依赖等)。

然后修改了pom依赖springboot的主版本到3.0.12. 看起来很漂亮,接下来就开始解决报错之旅了。

升级问题2 jakarta替代javax

自从Spring Boot 3.0的代码,Servlet相关的包的命名空间从javax改变为了jakarta。这个是Oracle关于商标权的纠纷问题,有兴趣的自行查询。所以首先升级上去后导致编译不通过,因为

 javax.servlet.http.HttpServletRequest 在新版本中已经不存在了。而改为了 jakarta.servlet.http.HttpServletRequest。所有用到这个类的地方(包括测试类)都要修改。

修改后至少保证了编译通过了。然后尝试配置好后继续运行。

升级问题3 mybatis-plus的支持(枚举handler失效)

这个时候编译是可以过了。但是发现启动的时候初始化数据的时候报错了。

java.lang.IllegalArgumentException: No enum constant

at java.base/java.lang.Enum.valueOf(Enum.java:273) at org.apache.ibatis.type.EnumTypeHandler.getNullableResult(EnumTypeHandler.java:49) at org.apache.ibatis.type.EnumTypeHandler.getNullableResult(EnumTypeHandler.java:26) at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:85)

 ... 53 common frames omitted

 看日志,mybatis在初始化数据的时候,有些bean里面有枚举类型的,之前能够通过

直接在yml中的mybatis-plus的配置里面加一个configuration:

default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler

 这样就不会有问题。但是升级版本后发现不管用了,日志说说他用的是EnumTypeHandler在解析枚举类。当然没有办法正确的解析到。

开始搜索了很多关于mybatis-plus怎么正确解析枚举类。都是说怎么使用EnumOrdinalTypeHandler或者说使用之定义枚举处理器来实现。但是发现这样改太麻烦了,我所有的mapper都需要改,我所有的枚举类也要改,并要给每个枚举类增加一个枚举handler。

于是在后来的尝试中发现,其实当前版本的springboot中,他根本没有读到我yml中配置的mybatis-plus的配置,为什么知道这点呢,是因为该配置里面有一个configuration:

log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这个是负责打印mybatis相关日志的。打开它的情况下,日志中会多出很多跟mybatis运行相关的如sql和mybatis使用的数据库连接之类的信息。但是我现在即使打开了这个配置,日志里面都没有。显然,这些配置失效了。于是专心研究为什么失效,网上也找不到相关文章。

最后网上找到说mybatis-plus 3.5.2以后有增加一个通用枚举类。但是我当前的mybatis相关版本是3.5.1.于是我就把他升级上去了。(但是还是应为版本漏洞的原因,我直接选了3.5.4)。等我升级好了然后再尝试运行程序,发现没有枚举报错了。但是相同的位置又报了另外一个错。

升级问题4 数据库驱动更新

这个时候找不到枚举的错倒是没有了,反而换成了另外一个错,错误如下:

java.lang.AbstractMethodError: Receiver class com.microsoft.sqlserver.jdbc.SQLServerResultSet does not define or inherit an implementation of the resolved method 'abstract java.lang.Object getObject(java.lang.String, java.lang.Class)' of interface java.sql.ResultSet.

我们用的是SQLServer的一个老的数据库驱动。然后我们使用的是Hikari动态数据源。然而在这个版本的ResultSet拿枚举数据的时候,会调用通用数据源里面的一个方法

getObject(String name,. Class clazz)

 当前的Hikari数据源的确继承了DataSource,并且用了这个方法。 

但是我们老的SQLServer数据库驱动的ResultSet中却没有这个方法。很显然数据库驱动太老了。于是升级。使用了新的SQLServer数据库驱动。还要顺便看看这个版本有没有漏洞,最终如下:

com.microsoft.sqlserver mssql-jdbc 10.2.0.jre17

 升级完驱动倒是可以了,但是发现它的好多版本或多或少都有点依赖漏洞。看了看,该版本有两个依赖漏洞,一个是关于H2版本的,我们本地项目H2版本已经手动升级成了没有漏洞的高版本。所以可以忽略了。另一个是关于它里面的package bouncycastle的,我们项目不用。所以排除:

com.microsoft.sqlserver mssql-jdbc 10.2.0.jre17 org.bouncycastle bcprov-jdk15on org.bouncycastle bcpkix-jdk15on

 到这里应该是好了吧。结果运行项目,报错。

com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Exception during pool initialization. com.microsoft.sqlserver.jdbc.SQLServerException: The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption. Error: "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target". ClientConnectionId:XXXXXX

 遇到了证书相关的问题。数据库连接未授权。找了下解决办法,得知需要在我们的jdbc url后面增加两个参数使其信任服务器的证书(推荐):

jdbc:sqlserver://IP;DatabaseName=XXXXX;encrypt=true;trustServerCertificate=true;

 或者,让他建立连接的时候根本不用encrypt (不推荐)。

jdbc:sqlserver://IP;DatabaseName=XXXXX;encrypt=false;

 改过之后继续运行,成功获得链接。成功识别枚举类,成功初始化数据。好兆头!

升级问题5 Call 老版本的 webService

 又是一个棘手问题,当运行顺利了,开始测一些API了,发现到某个位置又报错了。主要是因为新版本的Springboot已经默认不支持去调用老版的webService接口。所以要支持它能够正确建立webService的client,并且顺利调用,又是一大顿资料查询。说是要加一些支持的包进去。而且每个文章里面虽说都大同小异的加了那些包,但是使用过后还是报各种问题。至于他们提供的那些lib,我就不放出来了,反正对于我的项目,调过来调过去都会产生下面的这些错误。

错误1:

Caused by: java.lang.ClassNotFoundException: com.sun.xml.internal.ws.spi.ProviderImpl at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.loadClass(TomcatEmbeddedWebappClassLoader.java:72) at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1165) at javax.xml.ws.spi.ServiceLoaderUtil.nullSafeLoadClass(ServiceLoaderUtil.java:90) at javax.xml.ws.spi.ServiceLoaderUtil.safeLoadClass(ServiceLoaderUtil.java:123) at javax.xml.ws.spi.ServiceLoaderUtil.newInstance(ServiceLoaderUtil.java:101) ... 27 common frames omitted

错误2:

Caused by: java.lang.ClassNotFoundException: com.sun.xml.bind.api.JAXBRIContext at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:445) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:587) at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:149) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) ... 36 common frames omitted

好了,直接上我这边调整过后能够解决问题的包吧。

com.sun.xml.messaging.saaj saaj-impl 1.5.2 javax.xml.bind jaxb-api 2.3.1 com.sun.xml.bind jaxb-impl 3.0.2 javax.xml jaxb-impl 2.1 javax.xml.ws jaxws-api 2.3.1 javax.activation activation 1.1.1 com.sun.xml.ws rt 2.3.1 runtime com.fasterxml.woodstox woodstox-core com.sun.xml.messaging.saaj saaj-impl

我这里在rt里面排除了woodstox是因为有漏洞,然后排除saaj-impl是因为我需要手动import一个1.5.2版本的,因为只有这个版本能适配当前环境。

最终加入了这些包,我webService的调用能够正常的初始化了。但是在调用过程中又遇到了问题

升级问题6 反射的JVM参数

先说说遇到的Exception:

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module XXXXX at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) at java.base/java.lang.reflect.Method.setAccessible(Method.java:193) at com.sun.xml.bind.v2.runtime.reflect.opt.Injector$1.run(Injector.java:107) at com.sun.xml.bind.v2.runtime.reflect.opt.Injector$1.run(Injector.java:104) at java.base/java.security.AccessController.doPrivileged(AccessController.java:318) at com.sun.xml.bind.v2.runtime.reflect.opt.Injector.(Injector.java:103) ... 64 common frames omitted

 查了是说异常是由Java 9及以上版本中引入的Java Platform Module System引起的,特别是强封装的实现。它仅在特定条件下允许access,最突出的条件是:

类型必须是公共的必须导出拥有的软件包

对于反射,导致异常的代码尝试使用相同的限制。 更确切地说,异常是由对 setAccessible 的调用引起的。

这种setAccessible又不是我当前应用用到的,是在我调用webService的外部stub在使用。我也没法改代码。于是找解决办法。有些SB说,只要还原回JDK1.8就解决这个问题了。我他猫的要还原回1.8我还升级成17干毛?最后还是功夫不负有心人。两个JVM参数能够解决这个问题 ​​​​

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

--add-opens java.base/java.lang.reflect=ALL-UNNAMED

 在启动springboot应用前设置上面两个JVM参数。应用顺利能够通过刚才的错误了。

升级收尾工作

其实至此为止,差不多项目的问题都被很好的解决了。我以为就这么OK了,结果想不到我升级到springboot3.0.12后,跑了下Prisma Report,发现又给我引入了两个漏洞。是因为相关的依赖也做了版本升级,所以导致了:

spring-webmvc升级到了6.0.13,但是6.0.13有漏洞,需要6.0.14

spring-boot本生的3.0.12有一个漏洞,(注意,不是spring-boot-parent)需要升级到3.0.13

所以又升级了一个springboot主版本的小版本到3.0.13,想想应该没有啥问题了吧。

结果扫描后, 又出问题了。

spring-boot-starter-web升级到了3.0.13,里面使用的logback相关的版本升级到了1.4.11

然后,1.4.11有漏洞,我勒个去。但是好在他是spring带入的东西,直接加properties

17 UTF-8 2.0 2.15.3 1.4.12

上面除了java版本和sourceEncoding,另外三个都是因为我需要修复漏洞而更改某个spring里面带的包所定的版本。加了这个版本号后,重新导入,springboot就会使用相关版本的包了。

总结

至此,我的升级之路就结束了。总结下来也算是一个收获。

因为毕竟这么多年只是编码,根本看不到这么多东西。特别也作为一个教训,就是不要只想到代码能够实现功能就行了,这个是对初级程序员的要求。也不要觉得代码实现功能后稍微优化一下就牛逼了,这个也仅仅是中级程序员的职责所在。

最主要的是我们是做商业软件,特别是在现今的社会,一旦涉及到钱的module,你但凡有任何一个漏洞被抓到了,那就意味着公司会遭受特别大的亏损。

所以,解决冲突,解决项目漏洞,是所有程序员应该有的一个基本的职业素养。

还有个感悟就是,工作10多年,从最开始使用JDK1.5, JDK1.6之后,后来就一直在使用JDK8,然后就停滞在这个版本上面了。而且经历了很多公司,使用的基本上都是JDK8。这是第一次为了解决一些漏洞强行切换到JDK17这么高级的版本上来。因为其实很多开源的项目或者框架,都基本上慢慢开始不支持JDK8了,他们会把自己的漏洞尽可能的在高版本的JDK上面做修复。

所以不要想当然的觉得,我做了10年JDK8了,各大公司仍然还在使用JDK8做开发,别人都没有这么纠结开源包的漏洞,为什么我们这么纠结。还是之前说的,要看项目的安全等级,也要看客户的要求。一切以这些为基础,我只是一个做开发的工具人而已。人要有自知之明!

另,推荐一篇文章,关于升级到Springboot3.x的算是比较官方的文章了,我也是升级完了才发现有这么一篇可以参考的东西。哎,失算,自己研究了好久!

      Spring-Boot-3.0-Migration-Guide

(*以上所有相关引用均出自模拟的一个复盘升级项目环境,

跟公司代码无任何关系,也未涉及到任何相关公司机密。仅供大家参考,请勿转载!谢谢!)



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3