## 版本更新说明 * SpringBlade 4.0 版本已适配 Spring Boot 3 和 JDK 17,并从 Swagger 2 迁移到 OpenAPI 3。此次更新包含重大更改,属于破坏性更新。 * **若从SpringBlade 4.0版本开始使用,本章节可以直接跳过** * 若之前版本的工程已处于生产阶段或接近开发完成,请在升级前仔细权衡。 * 接下来,我们将详细列出工程升级的步骤及注意事项。 ## 项目编译级别升级为JDK17 ### 下载安装jdk17 1. 下载openjdk17,前往下载地址找到符合自己机器的版本并安装配置环境变量,三种推荐版本选其一或公司选型的Jdk均可 2. 下载地址(Azul Zulu):[https://www.azul.com/downloads/?version=java-17-lts&package=jdk#zulu](https://www.azul.com/downloads/?version=java-17-lts&package=jdk#zulu) 3. 下载地址(Eclipse Temurin™):[https://adoptium.net/zh-CN/temurin/releases/?version=17](https://adoptium.net/zh-CN/temurin/releases/?version=17) 4. 下载地址(Amazon Corretto):[https://aws.amazon.com/cn/corretto/?filtered-posts.sort-by=item.additionalFields.createdDate&filtered-posts.sort-order=desc](https://aws.amazon.com/cn/corretto/?filtered-posts.sort-by=item.additionalFields.createdDate&filtered-posts.sort-order=desc) #### 配置工程编译级别为jdk17 1. 打开工程后选择 `Project Structure` ![](https://img.kancloud.cn/9a/b8/9ab85f1f200efe2b17201b02ee0eac3f_790x1142.png) 2. 依次配置版本为JDK17 ![](https://img.kancloud.cn/7e/9c/7e9c047abba0643fd6675eb573990e06_1886x3158.png) 3. 打开设置找到Java Compiler设置为17 ![](https://img.kancloud.cn/a3/86/a38666eeb964d8dbc473a9c27c09ef49_1994x1424.png) 4. 打开设置找到Maven Runner设置为17 ![](https://img.kancloud.cn/80/0e/800e80444c73ccaa97fff09038cdd545_1994x1424.png) ## API升级 1. Spring Boot 3 与 JDK17开始,由 Java EE 升级为 Jakarta EE,我们需要对工程源码进行全局替换。 ~~~ javax.validation     →     jakarta.validation javax.servlet        →     jakarta.servlet javax.annotation     →     jakarta.annotation javax.transaction    →     jakarta.transaction javax.persistence    →     jakarta.persistence ~~~ 2. 由于部分api已经过期删除但业务代码用到相关api,我们需要重新引入 (Tool工程已经处理,业务代码若需要则加上对应依赖) ~~~ <!-- jakarta --> <dependency>    <groupId>jakarta.servlet</groupId>    <artifactId>jakarta.servlet-api</artifactId> </dependency> <!-- javax --> <dependency>    <groupId>javax.xml.bind</groupId>    <artifactId>jaxb-api</artifactId> </dependency> <dependency>    <groupId>com.sun.xml.bind</groupId>    <artifactId>jaxb-core</artifactId> </dependency> <dependency>    <groupId>com.sun.xml.bind</groupId>    <artifactId>jaxb-impl</artifactId> </dependency> <dependency>    <groupId>javax.activation</groupId>    <artifactId>activation</artifactId> </dependency> ~~~ 3. 原 `spring.redis` 配置升级为 `spring.data.redis` ,以下为最新版本配置 ~~~ spring: data:   redis:     host: 127.0.0.1     port: 6379     password:     database: 0     ssl:       enabled: false ~~~ 4. 针对反射工具类进行全局替换 ~~~ BeanUtil.copy    →     BeanUtil.copyProperties ~~~ ![](https://img.kancloud.cn/9a/ff/9aff6dc699bfd22ef016b089bfb26b66_1708x1066.png) 5. 工程pom.xml版本升级 ~~~ <revision>3.4.0.RELEASE</revision> ​ <java.version>1.8</java.version> <maven.plugin.version>3.8.1</maven.plugin.version> <maven.flatten.version>1.2.2</maven.flatten.version> ~~~ 【替换为】 ~~~ <revision>4.0.0.RELEASE</revision> ​ <java.version>17</java.version> <maven.plugin.version>3.11.0</maven.plugin.version> <maven.flatten.version>1.3.0</maven.flatten.version> ~~~ 6. 序列化加上注解 `@Serial` ~~~ package org.springblade.system.entity; ​ import java.io.Serial; import java.io.Serializable; ​ public class Entity implements Serializable { ​ @Serial private static final long serialVersionUID = 1L; } ~~~ ## Swagger升级 1. 由原本的Swagger2 改用 OpenAPI3。Knife4J采用支持OpenAPI3的版本,我们需要对依赖、注解、引用进行全局替换 2. Knife4j前端ui依赖 ~~~ <dependency>    <groupId>com.github.xiaoymin</groupId>    <artifactId>knife4j-openapi2-ui</artifactId> </dependency> ~~~ 【替换为】 ~~~ <dependency>    <groupId>com.github.xiaoymin</groupId>    <artifactId>knife4j-openapi3-ui</artifactId> </dependency> ~~~ 3. Knife4j核心依赖 ~~~ <dependency>    <groupId>com.github.xiaoymin</groupId>    <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> </dependency> ~~~ 【替换为】 ~~~ <dependency>    <groupId>com.github.xiaoymin</groupId>    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> </dependency> ~~~ 4. SpringFox移除改为新版依赖 ~~~ <dependency>    <groupId>io.springfox</groupId>    <artifactId>springfox-swagger2</artifactId>    <exclusions>        <exclusion>            <groupId>io.swagger</groupId>            <artifactId>swagger-models</artifactId>        </exclusion>    </exclusions> </dependency> <dependency>    <groupId>io.swagger</groupId>    <artifactId>swagger-models</artifactId> </dependency> ~~~ 【替换为】 ~~~ <dependency>    <groupId>io.swagger.core.v3</groupId>    <artifactId>swagger-annotations</artifactId> </dependency> ~~~ 5. Swagger2注解全局替换为OpenAPI3(Swagger3)注解 * 全局替换如下注解(箭头前为swagger2注解需要被全局替换为箭头后的openapi3注解) * 具体参数使用对比如下(快捷替换可以将`@Api(value`全局替换为`@Tag(name`,其他的以此类推) ~~~ @Api(value = "xx", tags = "xxx") → @Tag(name = "xx", description = "xxx")   @ApiIgnore → @Hidden (若是在方法入参则将@Hidden改为@Parameter(hidden = true)替换)   @ApiModel(value = "xx", description = "xx") → @Schema(description = "xx")   @ApiModelProperty(hidden = true) → @Schema(accessMode = READ_ONLY) @ApiModelProperty(value = "xx") → @Schema(description = "xx")   @ApiImplicitParams → @Parameters   @ApiImplicitParam → @Parameter   @ApiOperation(value = "xx", notes = "xx") → @Operation(summary = "xx", description = "xx")   @ApiParam("xx") → @Parameter(name = "xx") @ApiParam(value = "xx") → @Parameter(name = "xx")   @ApiResponse(code = 404, message = "msg") → @ApiResponse(responseCode = "404", description = "msg") ~~~ * 较为复杂的组合式注解前后对比如下 ~~~ @ApiImplicitParams({  @ApiImplicitParam(name = "category", value = "公告类型", paramType = "query", dataType = "integer"),  @ApiImplicitParam(name = "title", value = "公告标题", paramType = "query", dataType = "string")   }) @ApiOperation(value = "分页", notes = "传入notice") ~~~ ​ ⬇ ~~~ @Parameters({  @Parameter(name = "category", description = "公告类型", in = ParameterIn.QUERY, schema = @Schema(type = "integer")),  @Parameter(name = "title", description = "公告标题", in = ParameterIn.QUERY, schema = @Schema(type = "string"))   }) @Operation(summary = "分页", description = "传入notice") ~~~ * 剩余无法全局替换的再手动处理 * 比如我们可以先全局替换`@ApiImplicitParam`为`@Parameter` * 接着全局替换`paramType = "query", dataType = "integer"`为`in = ParameterIn.QUERY, schema = @Schema(type = "integer")` * 经过两波替换,剩下一个`, value = "`再全局替换为`, description = "` * 前面带一个逗号可以避免将 `@ApiOperation(value`的value值替换为`description`,因为`@ApiOperation`对应的是`summary` 6. 包路径全局替换 ~~~ import springfox.documentation.annotations.ApiIgnore; → import io.swagger.v3.oas.annotations.Hidden; ​ import io.swagger.annotations.ApiModelProperty; → import io.swagger.v3.oas.annotations.media.Schema;   import io.swagger.annotations.ApiModel; → import io.swagger.v3.oas.annotations.media.Schema;   import io.swagger.annotations.ApiImplicitParams; → import io.swagger.v3.oas.annotations.Parameters; ​ import io.swagger.annotations.ApiImplicitParam; → import io.swagger.v3.oas.annotations.Parameter; ​ import io.swagger.annotations.ApiOperation; → import io.swagger.v3.oas.annotations.Operation; ​ import io.swagger.annotations.ApiParam; → import io.swagger.v3.oas.annotations.Parameter; ​ import io.swagger.annotations.Api; → import io.swagger.v3.oas.annotations.tags.Tag; ​ import io.swagger.annotations.ApiResponse; → import io.swagger.v3.oas.annotations.responses.ApiResponse; ​ import io.swagger.annotations.*;  → import io.swagger.v3.oas.annotations.*; ~~~ 7. Swagger2配置类升级为OpenAPI3写法 * 原写法如下 ~~~ public class SwaggerConfiguration { ​ @Bean public Docket authDocket() { return docket("授权模块", Collections.singletonList(AppConstant.BASE_PACKAGES + ".modules.auth")); } ​ @Bean public Docket sysDocket() { return docket("系统模块", Arrays.asList(AppConstant.BASE_PACKAGES + ".modules.system", AppConstant.BASE_PACKAGES + ".modules.resource")); } ​ @Bean public Docket flowDocket() { return docket("工作流模块", Collections.singletonList(AppConstant.BASE_PACKAGES + ".flow")); } ​ } ~~~ * 新写法如下 ~~~ public class SwaggerConfiguration { ​ @Bean public GroupedOpenApi authApi() { return GroupedOpenApi.builder() .group("授权模块") .packagesToScan(AppConstant.BASE_PACKAGES + ".modules.auth") .build(); } ​ @Bean public GroupedOpenApi sysApi() { return GroupedOpenApi.builder() .group("系统模块") .packagesToScan(AppConstant.BASE_PACKAGES + ".modules.system", AppConstant.BASE_PACKAGES + ".modules.resource") .build(); } ​ @Bean public GroupedOpenApi flowApi() { // 创建并返回GroupedOpenApi对象 return GroupedOpenApi.builder() .group("工作流模块") .packagesToScan(AppConstant.BASE_PACKAGES + ".flow") .build(); } ​ } ~~~ 8. Swagger文档访问地址,springcloud与springboot地址通用 * 文档地址:[http://localhost/doc.html](http://localhost/doc.html) 9. 由于需要同时生效两套swagger的ui,我们删除了blade-swagger服务,重新改为springcloud聚合文档的实现方式。文档默认使用knife4j的ui,地址:[http://localhost/doc.html](http://localhost/doc.html),若需要使用swagger-ui,在blade-gateway的pom.xml加入如下配置,然后访问[http://localhost/swagger-ui.html](http://localhost/swagger-ui.html) 便可 ~~~ <dependency>    <groupId>org.springdoc</groupId>    <artifactId>springdoc-openapi-starter-webflux-ui</artifactId> </dependency> ~~~ 10. springboot无需操作,直接访问 [http://localhost/swagger-ui.html](http://localhost/swagger-ui.html) 便可 ### Mybatis-Plus升级 1. mybatis-plus启动依赖 ~~~ <dependency>    <groupId>com.baomidou</groupId>    <artifactId>mybatis-plus-boot-starter</artifactId>    <version>${mybatis.plus.version}</version> </dependency> ~~~ 【替换为】 ~~~ <dependency>    <groupId>com.baomidou</groupId>    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>    <version>${mybatis.plus.version}</version> </dependency> ~~~ 2. dynamic-datasource启动依赖 ~~~ <dependency>    <groupId>com.baomidou</groupId>    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>    <version>${mybatis.plus.dynamic.version}</version> </dependency> ~~~ 【替换为】 ~~~ <dependency>    <groupId>com.baomidou</groupId>    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>    <version>${mybatis.plus.dynamic.version}</version> </dependency> ~~~ 3. 由于mybatis-plus依赖的mybatis-spring版本会导致工程无法启动,需要将mybatis-plus指定适配版本 ~~~ <dependency>    <groupId>org.mybatis</groupId>    <artifactId>mybatis-spring</artifactId>    <version>3.0.3</version> </dependency> ~~~ 4. 以上升级均在tool工程处理完毕,大家业务工程无需处理,若有自定义二开tool工程的情况请知悉 ### Druid升级 1. druid启动依赖 ~~~ <dependency>    <groupId>com.alibaba</groupId>    <artifactId>druid-spring-boot-starter</artifactId> </dependency> ~~~ 【替换为】 ~~~ <dependency>    <groupId>com.alibaba</groupId>    <artifactId>druid-spring-boot-3-starter</artifactId> </dependency> ~~~ 2. 以上升级均在tool工程处理完毕,大家业务工程无需处理,若有自定义二开tool工程的情况请知悉 ### Ureport升级 1. 由于ureport还未支持springboot3与jdk17,bladex官方对其进行了升级适配,直接升级tool工程便可以无缝使用 2. 具体源码请看:https://gitee.com/smallc/blade-tool/tree/master/blade-core-report ## 反射 1. 升级到Java17后,如果未进行模块化命名,使用反射则会报如下错误:`module java.base does not "opens java.lang" to unnamed module)` 2. 从Java 9开始引入的模块系统增加了对JVM内部API的封装,以促进Java平台的封装性和安全性。默认情况下,Java核心库中的很多内部API和类都不再对外公开,除非显式地使用`--add-opens`选项来开放它们。 3. 为了解决老版本源码使用反射但未模块化到问题,可以通过在Java命令行中添加`--add-opens`参数来实现,格式如下: ~~~ java --add-opens java.base/java.lang=ALL-UNNAMED -jar your-application.jar ~~~ ## JDK17+ZGC JDK 17引入了很多重要的特性,其中之一是Z Garbage Collector (ZGC) 的正式支持。ZGC是一种可伸缩的垃圾收集器,旨在减少应用程序停顿时间,无论是在小型还是大型堆上。它最初在JDK 11中作为实验性特性引入,但到了JDK 17,ZGC成为了正式支持的特性。 #### 1\. ZGC的主要特点包括: * **低延迟**:ZGC的设计目标是将停顿时间限制在几毫秒内,甚至在大堆上也是如此。 * **可伸缩性**:ZGC旨在处理从几百兆字节到几个太字节大小的堆,而不会显著增加停顿时间。 * **无内存抖动**:ZGC几乎不产生内存抖动,这对于需要稳定和可预测性能的应用程序来说非常重要。 #### 2\. 使用ZGC 在JDK 17中使用ZGC非常简单。只需要在启动Java应用程序时通过命令行参数指定使用ZGC即可。以下是一个示例命令行,它启动了一个使用ZGC的Java应用程序: ~~~ java -XX:+UseZGC -jar your-application.jar ~~~ 这个命令告诉JVM使用ZGC作为垃圾收集器。请注意,使用ZGC可能需要您根据应用程序的具体情况调整其他JVM参数,以获得最佳性能。 #### 3.示例 假设有一个简单的Java应用程序,它在运行过程中不断创建新对象,模拟了一个典型的业务应用负载。在使用ZGC的情况下,可以观察到即使在高负载下,应用程序的停顿时间也保持在很低的水平。 ~~~ public class ZGCDemo {    public static void main(String[] args) {        List<Object> list = new ArrayList<>();        while (true) {            list.add(new Object());            if (list.size() > 10000) {                list.clear();           }       }   } } ~~~ 在这个示例中,我们不断地创建并清理对象,以模拟一个持续的负载。使用ZGC时,即使这种类型的工作负载可能导致大量的垃圾收集活动,应用程序的停顿时间也可以保持在极低的水平。 请注意,虽然ZGC能够显著减少停顿时间,但选择合适的垃圾收集器还需考虑应用程序的具体需求和工作负载特性。在某些情况下,其他垃圾收集器(如G1 GC)可能更适合您的应用程序。 #### 4\. 推荐使用的ZGC配置 ~~~ # 垃圾收集器 # 使用ZGC垃圾收集器 JAVA_OPTS="-XX:+UseZGC" # 解锁诊断VM选项 JAVA_OPTS="-XX:+UnlockDiagnosticVMOptions" # 设置ZGC统计信息的记录间隔(秒) JAVA_OPTS="-XX:ZStatisticsInterval=10"   # 内存 # 设置JVM最小堆大小为512m JAVA_OPTS="-Xms512m" # 设置JVM最大堆大小为1024m JAVA_OPTS="-Xmx1024m" # 允许未命名模块访问java.base模块中java.lang包的所有成员 JAVA_OPTS="--add-opens=java.base/java.lang=ALL-UNNAMED" # 允许未命名模块访问java.base模块中java.util包的所有成员 JAVA_OPTS="--add-opens=java.base/java.util=ALL-UNNAMED" # 将java.base模块中sun.security.ssl包的所有成员导出给所有未命名模块 JAVA_OPTS="--add-exports=java.base/sun.security.ssl=ALL-UNNAMED" # 允许未命名模块访问java.base模块中sun.security.ssl.internal.ssl包的所有成员 JAVA_OPTS="--add-opens=java.base/sun.security.ssl.internal.ssl=ALL-UNNAMED"   # 日志 # 打印出JVM启动时接收的命令行标志 JAVA_OPTS="-XX:+PrintCommandLineFlags" # 配置垃圾收集日志,包括日志文件的位置、名称、记录的信息(时间、运行时间)、日志文件的数量和大小限制 JAVA_OPTS="-Xlog:gc*:file=${LOG_DIR}/${PROJ_NAME}-gc-%p.log:time,uptime:filecount=10,filesize=50M" # 在内存溢出时生成堆转储 JAVA_OPTS="-XX:+HeapDumpOnOutOfMemoryError" # 指定堆转储文件的路径和名称 JAVA_OPTS="-XX:HeapDumpPath=${LOG_DIR}/${PROJ_NAME}-`date +%s`-pid$$.hprof" # 指定JVM崩溃日志(如JVM错误日志)的路径和名称 JAVA_OPTS="-XX:ErrorFile=${LOG_DIR}/${PROJ_NAME}-`date +%s`-pid%p.log" ~~~ ## 工程启动准备 1. 由于升级了JDK17,Java 模块化系统(Java Module System)的安全限制导致的针对反射等场景有可能会出现如下错误: ~~~ Cause: java.lang.reflect.InaccessibleObjectException: Unable to make field protected java.lang.reflect.InvocationHandler java.lang.reflect.Proxy.h accessible: module java.base does not "opens java.lang.reflect" to unnamed module @223aa2f7 ~~~ 2. 这种情况,我们需要在启动时增加对应配置: ~~~ --add-opens java.base/java.lang.reflect=ALL-UNNAMED ~~~ 3. 具体操作如下 ![](https://img.kancloud.cn/61/1c/611c6b5c67966bdaaf5dfd338cb7e149_2326x1354.png) 4. jar包启动时也需要加入此配置,具体命令如下 ~~~ java --add-opens java.base/java.lang.reflect=ALL-UNNAMED -jar your-application.jar ~~~