升级背景
传统的 Java 虚拟机通常使用 JIT(Just-in-Time)编译方式,所以项目在每次开始运行时都需要进行一次将字节码解释为机器码完成程序执行的流程,这种方式很大程度上拖慢了启动速度。为了提高项目的启动速度,我们考虑使用 AOT(Ahead-of-Time)方式进行编译,这种编译方式可以在程序运行之前直接将字节码解释为机器码,省去了运行前每次都需要解释的时间,所以这两种编译方式在代码编译和执行的时间点上有所区别。
JIT与AOT的对比
JIT(Just-in-Time)编译:JIT 编译器根据程序的运行时行为和环境进行优化,并将热点代码(频繁执行的代码)即时编译为本机机器码。
AOT(Ahead-of-Time)编译:AOT 编译器在编译阶段进行优化,根据目标平台的特性生成高效的本机代码,无需再进行即时编译。
主要区别:
一、JIT 编译是在运行时动态地将代码编译成机器码,而 AOT 编译是在程序运行之前将代码静态编译成机器码。
二、JIT 编译可以根据运行时行为进行即时优化,而 AOT 编译在编译阶段完成优化。
三、JIT 编译需要在程序运行时进行编译,会产生启动延迟和即时编译的开销,而 AOT 编译可以提供更快的启动时间和更稳定的性能。
GraalVM
GraalVM 是一个高性能的多语言虚拟机,GraalVM 的一个显著特点是它通过 JIT 和 AOT 的结合,提供了卓越的性能优化和灵活性。相比于其他支持AOT 编译的虚拟机,虽然也可以在一定程度上提前编译字节码(如在 JIT 编译之前进行 AOT 优化),但它们仍然依赖 JVM 环境,不能像 GraalVM 那样生成完全独立的本地二进制文件。而 GraalVM 专注于原生映像生成(通过 native-image 工具)提供了更强的 AOT 编译支持,能够将整个应用程序(包括所有依赖、第三方库和配置)编译成本地机器码,完全脱离 JVM 环境。所生成的二进制文件具有极低的内存占用和非常快速的启动速度。
所以我们选择使用GraalVM进行AOT编译,而GraalVM 目前支持的最低版本为 JDK11,所以如果依然坚持使用用 JDK8,GraalVM 的一切都用不了。而目前JDK最新LTS版本为JDK21,既然升级到任何JDK版本成本都相差无几,所以升级项目中我们选择直接升级到最新的LTS版JDK即JDK21。
升级流程
整体框架
Spring Boot
由于Spring Boot 从 3.1.0 版本开始支持 JDK 21,所以我们首先需要升级Spring Boot,这里选择使用目前较为主流的Spring Boot 3.3.0版本。
在Spring Boot 2.7 版本之后,自动装配机制有所变化,在Spring Boot 2.7 版本之前,自动装配机制通过 /META-INF/spring.factories 文件指定bean的全路径,一般是配置类。
在Spring Boot 3.0 版本之后,彻底废弃了spring.factories文件,而推荐使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports来自动装配。
Jakarta
javax 是一个旧版的 Java 命名空间,它包含了多个 Java 标准库的 API 包,在Web开发中主要用到最著名的库如 javax.servlet 。自从 2017 年,Oracle 将 Java EE(Java 企业版)移交给了 Eclipse 基金会并重命名为 Jakarta EE。从 Jakarta EE 9 开始,所有原来属于 javax.* 命名空间的 API 都迁移到了 jakarta.* 命名空间。而Spring Boot 3.x 及以后版本已经不再支持 javax.* 命名空间,而是使用 jakarta.* 命名空间。所以需要将先前 javax 的相关依赖替换为 jakarta 相关依赖,在项目中主要替换项为 javax.validation:validation-api 成jakarta.validation:jakarta.validation-api。
Swagger
介绍
在之前 JDK8 环境项目中,所使用 knife4j 为OpenAPI2规范,而自从Spring Boot 3 之后只支持OpenAPI3规范,我们的项目所使用Spring Boot版本为 3.3.0,所以必须升级knife4j为OpenAPI3规范,这里直接选用OpenAPI3规范的knife4j最新版本4.5.0版本。
流程
首先删除旧版本依赖并引入新版本依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
删除旧版本配置项并添加新版本配置项
springdoc:
#配置swagger ui的访问路径和排序方式
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha #按字母顺序排序
operations-sorter: alpha
api-docs:
path: /v3/api-docs #API文档的访问路径
group-configs:
- group: 'default' #API分组名称
paths-to-match: '/**' #匹配所有路径
packages-to-scan: com.xx #扫描的包,用于自动发现API
#knife4j增强配置
knife4j:
enable: true
setting:
language: zh_cn
删除旧版本配置类并添加新版本配置类
@Configuration
public class NewKnife4jConfig {
@Bean
public OpenAPI OpenAPI() {
return new OpenAPI()
.info(new Info()
.title("//title")
.version("1.0")
.description("//description");
}
@Bean
public GroupedOpenApi aAPI() {
return GroupedOpenApi.builder()
.group("a")
.pathsToMatch("/a/**")
.build();
}
@Bean
public GroupedOpenApi bAPI() {
return GroupedOpenApi.builder()
.group("b")
.pathsToMatch("/b/**")
.build();
}
}
将旧版本逐个注解替换为新版本注解(左旧右新)
@ApiModel(value="",description="") @Schema(name="",description="")
@ApiModelProperty(value="",required=true,dataType="") @Schema(description="",required=true,type="")
@Api(tags="") @Tag(name="")
@ApiOperation("") @Operation(summary="")
至此knife4j基本配置已经完成。
注意事项
按照上述基本配置完成之后,已经可以打开 localhost:8080/doc.html 查看接口文档,但是需要注意的是,在项目配置完成之后打开此页面会提示“knife4j文档请求异常”,这个是由于接口文档页面被拦截器所拦截没有放行,只需要在拦截器配置类中addInterceptors方法中添加以下排除路径即可。
registry.excludePathPatterns("/doc.html/**","v3/api-docs/**","/swagger-ui/index.html");
mariaDB4j
mariaDB4j是项目使用的内嵌的mariaDB数据库,可以使项目能够在不依赖系统安装的MariaDB或MySQL数据库的情况下,在Java应用内部直接运行MariaDB服务器。
在旧版本项目中mariaDB4j版本为2.5.3,mariaDB-java-client版本为2.7.5,实际上mariaDB4j和mariaDB-client旧版本和JDK21完全兼容不需要升级,为了更新数据库服务,还是选择使用较新版本。经测试mariaDB-java-client可以直接升级到最新的3.5.0版本,而mariaDB4j则不能直接使用最新版。
原因以及需求
一、重启复用数据
在项目关闭并再次启动时,我们希望mariaDB4j不删除上次启动所产生的数据目录以及其中的数据,而是支持重启后继续复用上次项目运行所产生的数据。在最新3.1.0版mariaDB4j中,如果我们在yml文件中如下指定数据文件路径:
mariaDB4j:
dataDir: ../
当结束项目并再次启动项目时则会报如下错误
ERROR : Data directory … is not empty. Only new or empty existing directories are accepted for --datadir
即数据目录不能再重复使用,具体原因可见
https://github.com/MariaDB4j/MariaDB4j/issues/926
推测是由于更换了mysql_install_db.exe,启动方式发生了变化,所以我们使用次新版本3.0.1即可解决此问题。
二、可以随项目启动项启动时自启
在 MariaDB4j 依赖源码中可以看到,MariaDB4jSpringService 类负责导入 yml 中的配置项,其继承了 MariaDB4jService 类,在此父类中有 @PostConstruct 方法调用了 start() 即启动数据库方法,所以可知在此 MariaDB4jSpringService 类在主程序启动项启动并完成依赖注入后,mariaDB4j 是可以自己启动的,但在更新 Spring Boot 到 3.3.0 之后,由于自动装配机制有所变化,在主程序启动后 mariaDB4j 并没有完成启动,原因经排查确定为 Spring 并没有扫描到 MariaDB4j 相关的依赖包。所以也没有对 mariaDB4j 依赖相关类进行bean处理。
解决方案:
1、直接在@SpringBootApplication(scanBasePackages={“ch.vorburger”}),中添加需要扫描的mariadb4j 依赖所需包,在 mariadb4j 3.0.1 中经测试可行。
2、在启动项中添加@Import(MariaDB4jSpringService.class),使 Spring可以显式的将此类进行依赖注入,在 mariadb4j 3.1.0 版本中经测试可行,而回退到 mariadb4j 3.0.1 版本中失灵。
3、最终方案,只扫描控制器包,而不扫描全部包,Spring Boot升级到3之后扫描方式发生了变化,导致了扫描全部包会产生多次扫描数据库发生冲突导致启动不成功,此外注解也发生了变化,需要再结合@ImportAutoConfiguration,将老@Configuration改为@AutoConfiguration。
测试用插件
主程序可以启动使用之后,后续需要进行ut测试安装载入插件jar包,所以需要将插件也相应升级到JDK21环境,由于插件的依赖继承自主程序,升级流程可直接参考主程序。
在接口测试时候,当传入MutipartFile类型参数和路径参数时,会报Name for argument of type […] not specified, and parameter name information not avail的错误。
错误原因:在参数上未显示使用注解来指定参数名,而编译时没有保留方法参数名,导致无法确定参数名,所以才报错。
解决办法:
方案1
在方法参数上添加注解
@PathVariable(name=“id”, required = true)
@RequestParam(name=“id”)
方案2
在全局设置中编译时保留方法参数名
①idea设置:File > Settings > Build, Execution, Deployment > Compiler > Java Compiler > Additional command line parameters: (输入框中填写 -parameters)
②pom文件设置,经测试pom文件配置后就不需要在idea里配置①
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs> <!-- 添加此项 -->
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
在升级之后测试接口时,当路径参数为空,之前的异常为HttpRequestMethodNotSupportedException变成了NoHandlerFoundException,而在之前全局异常处理器中并没有单独处理此类异常,所以需要添加此异常处理逻辑。
参考文献
双平台GraalVM编译二进制程序 - 言成言成啊
Name for argument of type [java.lang.String] not specified, and parameter name information not avail - 阿良~