背景
随着公司业务的不断扩展,新技术的更新换代,企业内部免不了会对软硬件进行升级,淘汰老旧的组件和实现方案,更新一波技术栈。这不,最近我们公司就面临这么一个难题:旧版本的组件上发现漏洞,为了修复系统漏洞,不得不对它进行版本升级。而升级某个组件时,又会因为版本不兼容,配置更新等原因,必须同步调整其他的组件或配置。因此,看似简单的升级一个组件,却像是推倒了多米诺骨牌似的,越改越多,越理越乱。而受影响的项目又多,每个项目都得有技术人员花费大量时间去处理这种廉价而繁琐的事情,事倍而功半。公司的CTO知道这事后,觉得长痛不如短痛,被动防守不如主动出击,咱干脆借着处理这件事,来一次架构升级,把SpringBoot改成SpringBoot3,JVM从1.8升到17,把公司该升的组件都升了。
而不才,就悲催荣幸的被委予此项重任。
框架升级
一、需求调研
框架是为了业务需求而服务的,一个好的框架能为整个研发团队降本增效,用更便利,更优雅的方式实现需求。 因此,在升级框架之前,必须得进行一轮需求调研。我联系了公司几个产品线的Leader,通过收集当前产品线所用框架中包含的组件,以及公司的业务诉求,推导出了新框架的建设清单:
新框架技术栈 | 版本 | 类别 | 说明 | 当前现状 |
JDK | 17 | 后端 | Java开发环境 | JDK 1.8 |
Spring Boot | 3.1.9 | 后端 | 后端研发框架 | Spring Boot 2.1.6.RELEASE |
Spring Cloud Alibaba | 2022.0.0.0 | 后端 | 微服务框架 | Spring Cloud 2.1.2.RELEASE |
Nacos | 2.3.2 | 中间件 | 配置中心 & 注册中心 | Apollo 1.1.0+Eureka2.1.2.RELEASE |
RocketMQ | 5.1.0 | 中间件 | 消息队列 | 不涉及 |
Sentinel | 1.8.6 | 开源基础组件 | 服务保障/容错 | 不涉及 |
XXL-JOB | 2.4.0 | 中间件 | 定时任务 | XXL-JOB 2.4.1-SNAPSHOT |
Spring Cloud Gateway | 4.0.6 | 中间件 | 服务网关 | 3.0.6 |
seata | 1.6.1 | 中间件 | 分布式事务 | 不涉及 |
DM | V8 | 数据库 | 数据库服务器 | MYSQL |
Druid | 1.2.22 | 基础组件 | JDBC 连接池、监控组件 | Druid 1.1.10 |
Dynamic DataSource | 4.3.0 | 基础组件 | 动态数据源,可以实现数据源动态切换 | 不涉及 |
Redis | 6.2.7 | 数据库 | key-value 数据库 | 6.2.7 |
redisson | 3.24.3 | 工具 | Redis 客户端 | redisson 3.11.2 |
hibernate-validator | 8.0.1 | 基础组件 | 参数校验组件 | hibernate-validator 6.0.17 Final |
flowable | 7.0.0 | 基础组件 | 工作流引擎 | flowable 6.8.0 |
knife4j | 4.4.0 | 基础组件 | Swagger 增强 UI 实现 | Swagger1.7.0 |
Apache SkyWalking | 9.7.0 | 中间件 | 分布式应用追踪系统 | 不涉及 |
Plumelog | 3.5 | 中间件 | 统一日志平台 | Logstash 5.3 |
fastjson2 | 2.0.50 | 基础组件 | JSON 工具库 | fastjson2 1.2.13 |
MapStruct | 1.5.5.Final | 基础组件 | Java Bean 转换 | MapStruct 1.5.2.Final |
Project Lombok | 1.18.30 | 基础组件 | 消除冗长的 Java 代码 | Project Lombok 1.18.8 |
JUnit | 4.13.2 | 基础组件 | Java 单元测试框架 | 4.12 |
mockito | 5.7.0 | 基础组件 | Java Mock 框架 | 不涉及 |
Mybatis-Plus | 3.5.5 | 基础组件 | 数据库操作组件 | Mybatis-Plus 3.4.6,tk.mybatis 2.1.5 |
Nginx | 1.24 | 中间件 | 反向代理,负载均衡,请求转发 | Nginx 1.13.7 |
poi | 5.2.5 | 基础组件 | 文档在线处理组件 | poi 4.1.0 |
easyexcel | 3.3.4 | 基础组件 | excel在线处理组件 | easyexcel 2.1.6 |
codec | 1.17.0 | 基础组件 | 数据编码解码组件 | 不涉及 |
hutool | 5.8.27 | 基础组件 | 开源工具包 | hutool 5.3.0 |
commons-net | 3.11.1 | 基础组件 | Ftp连接组件 | 不涉及 |
beanutils | 1.9.4 | 基础组件 | Java基础类库 | beanutils 1.9.3 |
aviator | 5.4.1 | 基础组件 | 表达式处理组件 | 不涉及 |
jjwt | 0.12.5 | 基础组件 | 数字签名组件 | jwt 0.9.0 |
OAuth2 | 6.3.1 | 中间件 | 认证服务 | OAuth2 2.3.3.RELEASE |
minio | 8.5.10 | 基础组件 | 对象存储服务器 | 与新框架一致 |
pagehelper | 2.1.0 | 基础组件 | 分页组件 | 与新框架一致 |
在规划这些组件/依赖的版本时,我直接根据名字去https://mvnrepository.com/中查询当前组件的最高版本,毕竟JDK,Springboot都是用的比较新的版本,完全Hold住……于是后面就踩坑了,至于是什么坑,请看下章分解。
二、父工程搭建
有了上面的清单,就可以搭建父工程了,父工程其实很简单,一个pom.xml足矣。而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>
<artifactId>leixi-hub-parent</artifactId>
<packaging>pom</packaging>
<name>leixi-hub-parent</name>
<description>leixi-hub 父工程</description>
<groupId>com.leixi.hub</groupId>
<version>${leixi-hub.version}</version>
<properties>
<!-- 按字母顺序 -->
<apm-toolkit-trace.version>6.5.0</apm-toolkit-trace.version>
<leixi-hub.version>1.0.0-SNAPSHOT</leixi-hub.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
<commons-io.version>2.15.0</commons-io.version>
<commons-lang.version>2.6</commons-lang.version>
<commons-codec.version>1.17.0</commons-codec.version>
<dm8.version>8.1.1.193</dm8.version>
<druid.version>1.2.22</druid.version>
<discovery.version>6.21.0</discovery.version>
<dynamic.datasource.version>4.3.0</dynamic.datasource.version>
<ehcache.version>2.10.6</ehcache.version>
<fastjson.version>2.0.50</fastjson.version>
<grpc-spring-boot.version>3.0.0.RELEASE</grpc-spring-boot.version>
<grpc-bom.version>1.60.1</grpc-bom.version> <!--用于构建分布式系统中的服务和客户端-->
<protobuf-bom.version>3.25.2</protobuf-bom.version> <!--是一个 Maven BOM(Bill of Materials)文件,用于管理 Protocol Buffers(protobuf),-->
<hutool.version>5.8.27</hutool.version>
<kaptcha.version>2.3.2</kaptcha.version>
<java.version>17</java.version>
<jdom.version>1.1</jdom.version> <!--操作xml的组件,考虑去掉-->
<jakarta-persistence-api.version>3.1.0</jakarta-persistence-api.version> <!--一个 Java 规范,用于定义和管理对象关系映射?-->
<jackson.version>2.16.1</jackson.version> <!--用于提供对 JSR-310(Java 日期和时间 API)的支持-->
<junit.version>4.13.2</junit.version>
<jetcache.version>2.7.5</jetcache.version>
<knife4j.version>4.4.0</knife4j.version>
<lombok.version>1.18.30</lombok.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
<micrometer.version>1.12.2</micrometer.version> <!-- micrometer-core 是一个用于度量和监控应用程序的 Java 库。收集和报告与应用程序性能相关的指标和度量-->
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<mybatis-spring.version>3.0.2</mybatis-spring.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<poi.version>5.2.5</poi.version>
<page-helper.version>2.1.0</page-helper.version>
<plumelog.version>3.5.3</plumelog.version>
<pinyin4j.version>2.5.0</pinyin4j.version> <!--用于将汉字转换为拼音-->
<!-- 消息队列 -->
<rocketmq-spring.version>2.2.3</rocketmq-spring.version>
<rocketmq.version>5.1.0</rocketmq.version>
<rocketmq.spring.client.version>5.0.5</rocketmq.spring.client.version>
<redisson.version>3.24.3</redisson.version>
<spring-boot.version>3.1.9</spring-boot.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0</spring.cloud.alibaba.version>
<springdoc.openapi.version>2.2.0</springdoc.openapi.version> <!-- 用于在 Spring Web MVC 中生成和展示 OpenAPI 文档的库-->
<tika.version>1.21</tika.version> <!--用于提取和解析各种文档格式的内容-->
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> <!--用于在多线程环境中传递线程本地变量的工具类-->
<!-- Job 定时任务相关 -->
<xxl-job.version>2.4.0</xxl-job.version>
<sentinel-core.version>1.8.6</sentinel-core.version>
<hibernate-validator.version>8.0.1.Final</hibernate-validator.version>
<easyexcel.version>3.3.4</easyexcel.version>
<commons-net.version>3.11.1</commons-net.version>
<aviator.version>5.4.1</aviator.version>
<jjwt.version>0.12.5</jjwt.version>
<minio.version>8.5.5</minio.version>
<mockito-core.version>5.12.0</mockito-core.version>
<flowable-engine.version>7.0.0</flowable-engine.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>${apm-toolkit-trace.version}</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${commons-beanutils.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<version>${dm8.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${dynamic.datasource.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>${tika.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>${jdom.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-bom</artifactId>
<version>${grpc-bom.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-bom</artifactId>
<version>${protobuf-bom.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>${grpc-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>${grpc-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>${grpc-spring-boot.version}</version>
</dependency>
<!-- 适配grpc-spring-boot-starter -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client-java</artifactId>
<version>${rocketmq.spring.client.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.openapi.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<!-- 解决 ThreadLocal 父子线程的传值问题 -->
<version>${transmittable-thread-local.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${page-helper.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>${jakarta-persistence-api.version}</version>
</dependency>
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>${jetcache.version}</version>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>${ehcache.version}</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>${pinyin4j.version}</version>
</dependency>
<dependency>
<groupId>com.plumelog</groupId>
<artifactId>plumelog-logback</artifactId>
<version>${plumelog.version}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${sentinel-core.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>${aviator.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito-core.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.flowable/flowable-engine -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-engine</artifactId>
<version>${flowable-engine.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
其实在搭建父工程时,我并不太理解这个父工程的意义,感觉有它没它区别不大。但是在撰写这篇博客的背景章节时,我幡然醒悟,父工程不就是整理了一套可以搭配起来使用的组件及版本清单吗?有了父工程做背书,子工程只需要根据自己的需求引入父工程指定的依赖即可,而不用担心引入的依赖会不会和其他依赖冲突,会不会因此导致服务无法启动,会不会引入漏洞,这不就从根本上简化了操作吗?
三、子工程调试
创建好了父工程,并不代表它一定是可用的。上文已经说过,在整理依赖组件时,所有的版本都是能取最新取最新,但有可能很多最新版的依赖相互之间并不兼容,所以还需要进行一定的调试。于是我先对父工程进行mvn install,再引用父工程创建了一个子工程,如下:
主要是编写上图中的pom文件,注意要引用父工程,并把所有的依赖包都转过来。有条件的还可以再写个增删改查功能,如下:
<!--CommonMapper.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.leixi.leixihub.dao.CommonMapper">
<select id="getDataBySql" resultType="java.util.Map">
${sql}
</select>
<update id="updateDataBySql">
${sql}
</update>
</mapper>
//这里是 CommonMapper.java
package com.leixi.leixihub.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
*
* @author leixiyueqi
* @since 2024/8/5 19:39
*/
@Mapper
public interface CommonMapper extends BaseMapper {
List<Map<String, Object>> getDataBySql(@Param("sql") String sql);
void updateDataBySql(@Param("sql") String sql);
}
// 这里是Controller方法
@GetMapping("/getDataBySql")
public Object getDataBySql(@RequestParam(value = "sql") String sql) {
return commonMapper.getDataBySql(sql);
}
四、接口测试
有了这些代码和依赖,只要项目能成功启动,增删改查能顺利执行,就说明这个架子初步搭成了。
五、依赖包冲突解决和版本调整
虽说项目可以起来,但这并不代表这个父工程是安全可用的,主要原因有两个:
1)引用的依赖包也不一定是安全,可能存在漏洞。
2)各依赖包之间可能存在依赖冲突的问题。
咱们基于以上两点,对这份依赖文件进行一次复查,逐个解决和排查问题。
1、对于存在漏洞,或者依赖的资源有漏洞的包,需要更换其版本,或者更换其依赖的子包的版本,或者替换另一个jar包。对于这些有问题的包,Idea的pom文件里都会有较明显的提示,一般标黄底的都是有些毛病的,如下图:
2、检查和处理依赖冲突,点击Idea2023右上角的图标,可以查看当前有冲突的依赖信息:
如下图,minio-8.5.5和jetcache-starter-redis-2.7.5都依赖checker-qual包,但是使用的版本不一样:
这种情况下,有两种解决方案,
1) 降低jetcache-starter-redis的版本号,或者提升minio的版本号,让它们依赖的checker-qual变得一样,可以在https://mvnrepository.com/中查询各包依赖的版本号,如下:
2)排除minio中低版本的checker-qual依赖,这需要更改pom.xml里的配置,如下:
六、回写父工程
根据上面的方法把依赖信息调整之后,一定要记得把相关变化都更新到父工程leixi-hub-parent里,避免每个子类都要进行类似的配置和调整。测试证明,只要父工程配好了<exclusions>,子工程里不用做这些配置,也不会显示冲突项。
SpringBoot2转3的方法
除了上文中说到了升级相关环境、依赖的版本,在对实际工程进行升级时,尤其是对以前的SpringBoot2.X升级到3.X时,还需要注意以下方面:
1. 升级 JDK 17
Spring Boot 3.0 需要 Java 17 作为最低版本。如果当前使用的是 Java 8 或 Java 11,则需要在 Spring Boot 迁移之前升级 JDK。
2. 升级到 Spring Boot 3
查看项目及其依赖项的状态后,要升级到 Spring Boot 3.0 的最新维护版本。对于不需要父工程的项目,可以这么写:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
3. 配置属性迁移
在 Spring Boot 3.0 中,一些配置属性被重命名/删除,开发人员需要相应地更新其 application.properties/application.yml。为了快速实现这一点,Spring Boot 提供了一个 spring-boot-properties-migrator 模块。咱可以通过将以下内容添加到 Maven pom.xml 来添加迁移器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
4. 升级到 Jakarta EE
由于 Java EE 已更改为 Jakarta EE,Spring Boot 3.x 的所有依赖项 API 也从 Java EE 升级为 Jakarta EE。代码中需要将所有 javax 的 imports 都替换为 jakarta。具体如下:
javax.persistence.* -> jakarta.persistence.*
javax.validation.* -> jakarta.validation.*
javax.servlet.* -> jakarta.servlet.*
javax.annotation.* -> jakarta.annotation.*
javax.transaction.* -> jakarta.transaction.*
5. 调整ConstructorBinding注解
@ConstructorBinding 在 @ConfigurationProperties 类的类型级别不再需要,应将其删除。
当一个类或记录有多个构造函数时,它仍然可以在构造函数上使用,以指示应使用哪一个构造函数进行属性绑定。
6. 尾部斜杠URL匹配更改
从 Spring Framework 6.0 开始,尾部斜杠匹配配置选项已为 deprecated,其默认值设置为 false。这意味着以前,以下控制器将匹配GET /health和GET /health/
@RestController
public class HealthController {
@GetMapping("/health")
public String health() {
return "Application is Working";
}
}
7. RestTemplate 调整
Spring Framework 6.0 中已删除对 Apache HttpClient 的支持,现在由 org.apache.httpcomponents.client5:httpclient5 取代。如果 HTTP 客户端行为存在问题,则 RestTemplate 可能会回退到 JDK 客户端。org.apache.httpcomponents:httpclient 可以由其他依赖项传递传递,因此在应用程序可能依赖此依赖项而不声明它。
下面是迁移后的RestTemplate示例:
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.util.Timeout;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
*
* @author leixiyueqi
* @since 2024/8/5 21:39
*/
@Configuration
public class RestTemplateConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return buildTemplate(10000, 10000,3);
}
private RestTemplate buildTemplate(long requestTimeout, long connectTimeout, int retryTimes) {
final SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactoryBuilder.create()
.build();
final PoolingHttpClientConnectionManager manager = PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslConnectionSocketFactory)
.build();
final CloseableHttpClient closeableHttpClient = HttpClients.custom()
.setDefaultRequestConfig(RequestConfig.custom().setConnectionRequestTimeout(Timeout.ofMilliseconds(requestTimeout))
.setConnectTimeout(Timeout.ofMilliseconds(connectTimeout)).build())
//.setRetryStrategy(new DefaultHttpRequestRetryStrategy(retryTimes, NEG_ONE_SECOND))
.setConnectionManager(manager)
.build();
final HttpComponentsClientHttpRequestFactory componentsClientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
componentsClientHttpRequestFactory.setHttpClient(closeableHttpClient);
final RestTemplate restTemplate = new RestTemplate(componentsClientHttpRequestFactory);
return restTemplate;
}
}
8. 升级 Spring Security
Spring Boot 3.0 已升级到 Spring Security 6.0。因此,WebSecurityConfigurerAdapter 已被弃用。 Spring鼓励用户转向基于组件的安全配置。可使用 Spring Security lambda DSL 和方法 HttpSecurity#authorizeHttpRequests 来定义自己的授权规则。
下面是使用 WebSecurityConfigurerAdapter 的示例配置,它通过 HTTP Basic 保护所有端点:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
}
}
展望未来,推荐的方法是注册一个 SecurityFilterChain bean:
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
}
9. Spring Kafka 模板升级
KafkaTemplate 方法现在返回 CompleteableFuture 而不是 ListenableFuture,后者已被弃用。Spring Boot 2.x 中带有 ListenableFuture 的 Kafka 模板:
private RoutingKafkaTemplate routingKafkaTemplate;
public void send(){
ListenableFuture<SendResult<Object,Object>> future = routingKafkaTemplate.send("Message","topic");
future.addCallback(new ListenableFutureCallback<>() {
@Override
public void onFailure(Throwable ex) {
log.error(ex);
}
@Override
public void onSuccess(SendResult<Object, Object> result) {
log.info("success");
}
});
}
Spring Boot 3.x 中带有 CompletableFuture 的 Kafka 模板:
private RoutingKafkaTemplate routingKafkaTemplate;
public void send() {
CompletableFuture<SendResult<Object, Object>> future = routingKafkaTemplate.send("Message", "topic");
future.thenAccept(log::info)
.exceptionally(exception -> {
log.error(exception);
return null;
});
}
10. Spring Doc OpenAPI 升级
springdoc-openapi用于为Spring Boot 项目自动生成 API 文档。 springdoc-openapi的工作原理是在运行时检查应用程序,以根据 spring 配置、类结构和各种注释推断 API 语义。对于 spring-boot 3 支持,请确保使用 springdoc-openapi v2。对于 WebMVC 项目,需要在 pom.xml. 文件中包含以下依赖项。
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
对于 WebFlux 项目,您需要在 pom.xml. 文件中包含以下依赖项。
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.3.0</version>
</dependency>
参考资料
以下是相关参考资料,感谢大佬们的倾情整理。
参考0:jdk8升级JDK17避坑指南
参考1:重磅!Spring Boot 2.7 正式发布
参考2:hutool-希望Hutool能支持下JDK8~JDK17的所有版本
参考3:一文详解|从JDK8飞升到JDK17,再到未来的JDK21
参考4:从 Java 8 升级到 Java 17 踩坑全过程,建议收藏!
参考5:老卫waylau-JDK
参考6:java - Spring Boot 2.x 到 3.2 的全面升级指南
后记
抛开过程中的困难和曲折不说,这是一次酣畅淋漓,难得且难忘的一次机会,一个中小型公司终其一生,能有几次这么升级框架的机会?又有多少程序员能有这样的经历?讲真,我是很感激老大能给我这个机会的。通过这次升级,我对于SpringBoot项目的整体架构,父子工程关系等都有了更深的认识,对依赖整理,漏洞排除也有了相关的积累。苦点累点没什么,获得的成就感却是满满的,将来,公司所有的项目,都将在整理的工程基础上进行建设,我不就是名副其实的奠基人了吗?(可把我给牛批坏了!)