为什么要升级
凡是因果,为什么吃饱了撑着要从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.13 |
spring-expression | 5.1.8.RELEASE | 5.3.28 |
spring-beans | 5.1.8.RELEASE | 5.3.28 |
spring-webmvc | 5.1.8.RELEASE | 5.3.28 |
thymeleaf-spring5 | 3.0.11.RELEASE | 3.0.15.RELEASE |
spring-boot-autoconfigure | 2.1.6.RELEASE | 2.7.13 |
spring-core | 5.1.8.RELEASE | 5.3.28 |
spring-expression | 5.1.8.RELEASE | 5.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
(*以上所有相关引用均出自模拟的一个复盘升级项目环境,
跟公司代码无任何关系,也未涉及到任何相关公司机密。仅供大家参考,请勿转载!谢谢!)