最近正在给自己的开源项目校园博客升级到 JDK17 以及 SpringBoot3,正好记录下升级和踩坑的过程,给大家提供一些解决方案的参考。
先说结论:非常推荐升级JDK17,成本低收益高。至于SpringBoot3.0,迁移成本比较高,坑也会比较多,但如果是新项目的话,还是可以试试的。
PS:项目原来的版本是 JDK8 + SpringBoot2.6。
1. 为什么要升级?
- JDK17和SpringBoot3也发布了一段时间了,自己对一些新特性也比较感兴趣,尤其是 Native Image 这个玩意。
- 自己手上刚好有校园博客这个项目,可以用来给进行升级,项目不复杂,但也算五脏俱全,全量升级既可以感受一下变化,也不会太费事。
- JDK17 是一个长期支持的版本(LTS),现在很多开源应用或者一些组件都在往这上面靠,并且大有一种最低支持 JDK17 的趋势。
- 自己在公司所接触到的项目也有一部分是使用的JDK17,并且整体有往这方面靠的趋势,新项目都会直接用JDK17。
总的来说就是 兴趣 + 资源 + 趋势。
2. 升级有什么好处?
先来看看 JDK8 -> JDK17 的好处。
- ZGC垃圾回收器,性能提升
- 可以使用 var 作为局部变量类型推断标识符
- 一个文件中可以包含多个public类
- switch 使用起来更加简洁,可以不用再break了。
- instanceof 增强
- 增加不可修改的数据类 record(感觉还是 kotlin 的 data class 好用)
- Text Blocks文本块
实用性很强,非常舒服。
再看看 SpringBoot3.0 的一些新特性。
- 更好的支持 Native Image,使用 GraalVM 构建原生镜像,可以提供显著的内存和启动性能改进
- 升级到 Spring6.0
- 升级到 Spring Security 6.0
- ......
好吧,感觉上是不如 JDK17 要更有性价比,如果对 Native Image 兴趣不大的话,建议不要升级SpringBoot3.x,因为升级SpringBoot的成本可要比升级JDK高多了。
3. 升级过程分享
以下的一切内容均基于我已有的项目【校园博客】进行升级和讲解,源码地址:github.com/stick-i/scb...
既然一切都是基于JDK17的,那我们就先升级JDK吧!
3.1. 升级JDK17
3.1.1. 下载安装
安装JDK17,这里我直接在IDEA里面下载安装了,很方便:
为了便于自己以后使用 Native Image,这里我直接下载了 GraalVM。
在IDEA中更新项目SDK和模块SDK:
3.1.2. Maven构建
更新Maven编译配置:
xml
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
Maven重新打包下:
这一步主要是为了更新下内部组件的 JDK 版本。
3.1.3. 启动服务
测试下有没有其他问题,启动所有微服务项目:
竟然一切正常,也可能与我的项目比较简单有一定的关系,所有服务都成功跑起来了。
3.1.4. 更新Dockerfile
原来使用的基础镜像是 java:8-alpine
,更新到了亚马逊的openjdk17版本 amazoncorretto:17-alpine
。
dockerfile
# 设置JAVA版本
FROM amazoncorretto:17-alpine
# 指定存储卷, 任何向/tmp写入的信息都不会记录到容器存储层
VOLUME /tmp
# 拷贝运行JAR包
ARG JAR_FILE
ADD ${JAR_FILE} app.jar
# 设置JVM运行参数,限定内存大小,并设置时区为东八区
ENV JAVA_OPTS="\
-server \
-Xms256m \
-Xmx512m \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-Duser.timezone=GMT+08 "
#空参数,方便创建容器时传参
ENV PARAMS=""
# 入口点, 执行JAVA运行命令
ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /app.jar $PARAMS"]
3.1.5. 源代码
看起来没什么问题了,先提交上JDK升级的代码,有需要的同学可以查看提交记录:
build(all): 全量升级到jdk17,更新dockerfile和pom文件。 by stick-i · Pull Request #198 · stick-i/scblogs
3.2. 升级SpringBoot3.2
为什么选择直接升级到 SpringBoot3.2 而不是 3.0呢?
主要是我开始升级的时候,SpringBoot已经更新到3.2了,而此时的3.0的生命周期已经过半了,目前也还没有推出3.0以上的LTS版本,这么看来我以后总还是要升级的,倒不如现在一起弄了。
3.2.1. 升级pom依赖
跟SpringBoot相关的依赖还是比较多的,尤其是依赖了SpringCore的三方依赖,肯定也是要统一进行升级的。
截至到我写这篇文章的时间,SpringBoot的最新GA版本为 3.2.1:
我选择相信Spring,直接升级最新版!
对应的SpringCloud版本为2023.0.0
其他主要依赖对应升级的情况:
依赖项 | 升级前版本 | 升级后版本 | 备注 |
---|---|---|---|
SpringBoot | 2.6.11 | 3.2.1 | 目前的最新版,要踩坑就踩最新的坑🤡 |
SpringCloud | 2021.0.4 | 2023.0.0 | 对应SpringBoot3.2.x |
SpringCloudAlibaba | 2021.0.4.0 | 2022.0.0 | 这个库还没出2023的版本,但是2022版也是基于SpringBoot3.0的,应该不会差太多 |
Mybatis-Plus | 3.5.3.1 | 3.5.5 | 注意 :artifactId 从mybatis-plus-boot-starter 改成了mybatis-plus-spring-boot3-starter |
druid | 1.2.11 | 1.2.20 | 注意 :artifactId 从druid-spring-boot-starter 改成了druid-spring-boot-3-starter |
对了,建议顺便升级下Maven。
3.2.2. 解决依赖异常
修改完pom文件之后刷新一下本地依赖包,噢呦,一堆报错:
我看了一下,就两个问题,分别是 mysql-connector-java 和 javax.servlet-api 这两个包的版本没有被指定,所以Maven找不到对应的包。
为什么没有指定呢?之前也没有指定,但是之前没有报错,说明这两个包之前是有被 spring-boot-starter-parent 所管理的,但是现在它不管了。
这得去看看SpringBoot3.0的迁移文档:github.com/spring-proj...
3.2.2.1. MySQL
在网页里搜索关键字 MySQL,这不就来了:
就是说 mysql:mysql-connector-java
这个包的坐标改成了 com.mysql:mysql-connector-j
,让我们更新的时候也顺带改一下。这个简单,全局搜索然后改一下就好了。
一改完,版本继承的小图标就出来了,不错不错。
3.2.2.2. javax -> jakarta
然后再搜一下关键字 javax,这不就又来了:
这个就稍微麻烦一点了,不仅Maven坐标从 jakarta.servlet:jakarta.servlet-api
改成了 javax.servlet:javax.servlet-api
,而且包名也从 javax.xxx.xxx 改成了 jakarta.xxx.xxx,所有导入了 javax 的包都得改。
先更新下pom文件:
然后再全局搜索 javax 替换下:
我试过了,升级完后唯一出现问题的地方就只有一处,但也很容易修改:
ResponseStatusException 中没有 getStatus() 这个方法了,我使用HttpStatus.valueOf(statusException.getStatusCode().value()) 代替了原来的方法。
做完上面这些后,我的项目已经可以成功编译了,但还不能正常的跑起来。
3.2.3. 配置文件属性迁移
SpringBoot3 更改了一些配置属性,例如:spring.redis.host
改为了spring.data.redis.host
。
这一变更几乎对所有项目都会有影响,要查看所有的变更项,可以在官方文档中进行查找:github.com/spring-proj...
但这太silly了,很显然官方也这么认为,所以给开发者提供了一个简单的迁移方法,引入 spring-boot-properties-migrator
,它会帮你自动检测配置文件中需要修改的地方:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
配置文件属性迁移完毕后,记得删除这里添加的 spring-boot-properties-migrator 依赖。
然后运行项目,当然你的项目大概率是运行不起来的,但别着急,看看你的控制台输出,有没有像我这样的输出内容:
上面的异常信息其实分为了两个部分,前面部分是需要进行修改的配置:
The use of configuration keys that have been renamed was found in the environment:
Property source 'bootstrapProperties-default-redis.yaml,DEFAULT_GROUP':
Key: spring.redis.host
Replacement: spring.data.redis.host
Key: spring.redis.password
Replacement: spring.data.redis.password
Key: spring.redis.port
Replacement: spring.data.redis.port
Each configuration key has been temporarily mapped to its replacement for your convenience. To silence this warning, please update your configuration to use the new keys.
它也给出了重命名之后的key,这里直接对着描述把自己的配置文件改改就好了,比较简单。
后面部分是说有一些配置已经被弃用了,但是它也给出了弃用的原因:
The use of configuration keys that are no longer supported was found in the environment:
Property source 'bootstrapProperties-default-springmvc.yaml,DEFAULT_GROUP':
Key: spring.mvc.throw-exception-if-no-handler-found
Reason: DispatcherServlet property is deprecated for removal and should no longer need to be configured
Property source 'bootstrapProperties-default-redis.yaml,DEFAULT_GROUP':
Key: spring.redis.lettuce.pool.max-active
Reason: none
Key: spring.redis.lettuce.pool.max-idle
Reason: none
Key: spring.redis.lettuce.pool.max-wait
Reason: none
Key: spring.redis.lettuce.pool.min-idle
Reason: none
Please refer to the release notes or reference guide for potential alternatives.
好吧,这里其实有点小坑,只有上面第一个Key给了弃用原因,说是DispatcherServlet属性已经被移除了。但是后面几个redis相关的Key都是没有给弃用原因的。
既然这样,那我只能自己去官方文档里找了:github.com/spring-proj...
全局搜索下 lettuce.pool
,你别说,还真让我找到了:
明明也没有弃用,就是把redis前面加个data罢了,看来 spring-boot-properties-migrator
也偶有瞎说的情况啊。
再次提醒:配置文件属性迁移完毕后,记得删除之前添加的 spring-boot-properties-migrator 依赖。
3.2.4. ES版本兼容
如果你的es客户端版本和es服务端版本一致(均为8.x),可以直接跳过这部分内容。
项目里使用了 spring-boot-starter-data-elasticsearch
,升级SpringBoot3.x 之后,这个依赖的版本也·提高了,对应ES的版本是8.x,而我服务器使用的ES版本是7.x,所以有一些不兼容的问题,启动时出现异常:
Caused by: java.lang.RuntimeException: node: http://xxxxxx, status: 200, [es/indices.exists] Missing [X-Elastic-Product] header. Please check that you are connecting to an Elasticsearch instance, and that any networking filters are preserving that header.
本来想通过降低 elasticsearch-rest-client 的版本来解决这个问题,但是降低之后又不能兼容 SpringBoot3 了,于是只能另辟蹊径了。
这个说起来比较麻烦,我在 stackoverflow 上找到一篇帖子,里面有对这个问题的描述,可以参考下:stackoverflow.com/questions/7...
它讲到了两个问题:
- 客户端向服务端发送了未知的 Content-Type ,因此其请求被拒绝并返回 406(其实是请求头 compatible-with 不受支持)
- 客户端需要验证 response 中是否具有 X-Elastic-Product=Elasticsearch 标头,但服务端并没有返回这个。
问题其实蛮清晰的,但是给出的解决方案让我不太满意,还需要自己重新去构建一个 RestClient,自己读取配置文件然后set进去,又得设置账号密码、又得解析Host的,这我可受不了。
于是经过我的一顿研究之后,我发现了 RestClientBuilderCustomizer
这个类:
翻译:回调接口,可以由希望通过RestClientBuilder进一步定制RestClient的bean实现,同时保留默认的自动配置。
只要用这个玩意,就可以在原有的 RestClient 基础上,进行一些定制化的操作,比如说解决上面那两个问题。于是乎,我就写了下面这一段代码:
java
/**
* Es 兼容性配置,添加响应头,兼容服务端版本
* <p>
* 如果客户端与服务端版本一致,可移除此配置。
*
* @author 阿杆
* @version 1.0
* @date 2024/1/25 22:29
*/
@Component
public class EsCompatibilityConfig implements RestClientBuilderCustomizer {
@Override
public void customize(RestClientBuilder builder) {
}
@Override
public void customize(HttpAsyncClientBuilder builder) {
// 添加响应头,兼容X-Elastic-Product
HttpResponseInterceptor httpResponseInterceptor =
(response, context) -> response.addHeader("X-Elastic-Product", "Elasticsearch");
builder.addInterceptorLast(httpResponseInterceptor);
// 自定义默认请求头,目的是禁用兼容性请求头 compatible-with
builder.setDefaultHeaders(List.of(new BasicHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())));
}
}
这段代码很简单,在构建 RestClient 的过程中插入了一段代码,修改了请求头和响应头,用来兼容ES版本。只需要把这个类注入到Spring Bean中,就可以被 ElasticsearchRestClientConfigurations 自动加载。
3.2.5. WARN trationDelegate$BeanPostProcessorChecker: is not eligible for getting processed....
如图所示,我的项目在升级到 SpringBoot3.x 后出现了大量的 WARN:
虽然不影响项目运行,但是看得我很不爽,那只能想想办法看怎么解决掉这个warn了。
随机截取的一段异常信息,其他的也都差不多:
2024-01-28T11:58:45.587+08:00 WARN 1228 --- [user-server] [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration' of type [org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected into a currently created BeanPostProcessor [lbRestClientPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies.
注意看上面的异常信息,有任何跟我项目有关的东西吗?没有吧
那有任何跟依赖冲突有关的东西吗?看上去也没有
那这个异常什么时候才开始有的?Spring整体升级之后
好,那既然这样,我们可以大胆的认为这是一个SpringBoot的bug。
一顿搜索之后,我在github上找到了这个 issue:github.com/spring-clou...
还真是Spring的bug,不过不是SpringBoot,而是SpringCloud的bug。
这位官方大佬也说了,将会在下一个版本(2023.0.1)中修复它,预计2月20日(今天是1月28日),不过他们会先发布新的Commons,用以修复这个bug。
在我看到这个issue的时候,新版的 SpringCloudCommons已经发布了:spring.io/blog/2024/0...
于是我对项目中的依赖进行替换,由于这个依赖是从其他Spring-Cloud的组件中自动继承过来的,所以我们只需要在依赖管理里面指定下版本就可以了。
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<version>4.1.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
添加完之后,果然没有再报warn了,之后等 SpringCloud2023.0.1 发布了,再做一下替换就好了。
3.2.6. 更新自动注入文件
SpringBoot2.7时已经提出使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
代替 spring.factories
:
我升级到 SpringBoot3.2 时,还是支持 spring.factories 的,但再过几个版本可能就不支持了,这边建议直接迁移下,这块几乎没什么成本的。
3.2.7. 源代码
这部分升级的改动有点多,以为已经搞好了,就提PR到main分支了,结果又蹦出来新的问题。
建议需要升级 SpringBoot3.x 的朋友,在看完这篇文章之后,还是再去把官方文档过一遍,看看有没有其他受影响的地方,这样稳妥一点。
代码已提交到GitHub:
源码建议单个 commit 结合 commit message 来查看,这样会更有条理,而不是整个 pr 一起看。
最后也附上一些我参考到的官方链接:
- SpringBoot3.0升级指南:github.com/spring-proj...
- 3.2发布记录:github.com/spring-proj...
- 3.0发布记录:github.com/spring-proj...
- 2.7发布记录:github.com/spring-proj...
- 3.0配置更新记录:github.com/spring-proj...
4. 后记
本来以为我这小项目简单升级下一两天就弄好了,结果前前后后搞了两周,尤其升级 SpringBoot 的时候,出了一顿问题,踩了不少坑。
看在作者这么认真的份上,建议关注趁早关注下,等我以后火了,在坐的各位就都是老粉了!