java瘦身、升级graalvm
一.graalvm简介
传统的hotvm,需要先启动 jvm 再载入 java 代码,然后再即时的将 .class 字节码编译成机器码,交给机器执行。以在线权限为例,通过打包插件会将项目打成jar包,jar里是class文件,包含自定义代码和maven引用的所有包。
graalvm使用了 Ahead-of-time compile(提前编译),也就是说,他在编译期时,会把所有相关的东西,包含一个基底的 VM,一起编译成机器码,这个基底 VM 是 GraalVM 内部才有的东西,他只包含最基本的线程排成机制、垃圾回收,尽可能的缩小必要的 jvm 体积。通过graalvm打包插件,仅会把实际用到的代码打成一个可执行文件。
graalvm aot功能,运行时省去了加载class这个过程,同时只把相关代码编译,避免传统打包将所有依赖的包都编译,所以省去了类加载的内存空间。
二.版本选择
项目大多使用springboot,需要使用spring的打包插件,springboot2.x的打包插件是实验性的,所以使用springboot3正式版。
springBoot3.3.10:当前最新版本是3.4.5,选择稍早一些的3.3.10稳定版。
oracle jdk17:
-
springboot3.x只支持jdk17-23,当前jdk长期支持稳定版有8、11、17、21,17发布于2021年,21发布于2023年,选择较早的17。
-
openjdk17的graalvm只免费使用Serial GC回收器,g1回收器是商用收费,oracle g1是免费的,所以选择oracle。
三.升级过程
升级jdk17
电脑有多个jdk版本,推荐使用sdkman控制多版本。
使用sdk安装graalvmjdk17:sdk install java 22.3.r17-nik
升级springboot3.3.10
参考Springboot2.x升级到3.x的经验分享 - 盗梦笔记 - 博客园
打包graalvm
操作步骤
推荐在ubuntu进行打包更简单,需要安装graalvm相关文件,在windows安装过程复杂。
在安装好graalvm jdk之后,安装native-image和相关工具,参考ubuntu 安装 graalvm
在pom中添加插件,注意修改启动类
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>3.3.10</version></plugin><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><mainClass>com.example.demo.DemoApplication</mainClass><buildArgs><buildArg>-H:+AddAllCharsets</buildArg><buildArg>--initialize-at-run-time=org.apache.logging.log4j.LogManager</buildArg><buildArg>--initialize-at-run-time=org.apache.logging.log4j.util.ProviderUtil</buildArg><buildArg>--initialize-at-run-time=org.apache.logging.log4j.spi.Provider</buildArg></buildArgs></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><configuration><skip>true</skip></configuration></plugin></plugins></build><repositories><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></repository></repositories><pluginRepositories><pluginRepository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></pluginRepository></pluginRepositories>
执行mvn clean package 打jar包
在target目录下,执行java -agentlib:native-image-agent=config-output-dir=<跟踪代理文件输出路径> -jar <jar 包路径>
这个命令会启动jar包,启动后,把所有代码都执行一遍。如果有部分代码没有执行、这部分有用到新类,则生成的配置文件就会缺少未执行的这部分的类反射信息,后续graalvm包运行后调用这部分功能会报错。
把jar运行停掉,就会生成配置文件,如下图,reflect-config文件是反射信息,resource-config是资源文件信息。如。当然这些文件可以手动修改,如果扫描不全面,可以手动加。
在工程resource目录下新建 META-INF/native-image 文件夹,并且将所有生成的配置文件移入。
在工程中自己添加 mvnw命令和目录
执行打包 ./mvnw -Pnative native:compile
或者 ./mvnw -Pnative native:compile -Dmaven.repo.local=/home/melo/apache-maven-3.9.9-bin/repository (指定本地仓库,mvnw用不上默认mvn配置,防止多module找不到)
打包很占用内存,需要6-10G内存,打包结束后会生成一个可执行文件,可直接运行。
事务、反射、动态代理
spring事务基于动态代理,所有动态代理和反射要提前在reflect文件中指定要反射哪些类,graalvm打包才会将类包含。
运行期间如果出现java.lang.TypeNotPresentException、 java.lang.ClassNotFoundException就是此类问题,需要在java -agentlib:native-image-agent生成配置文件期间,尽量把所有代码运行。如果运行后还报错,就手动加入反射配置。
maven多module
以权限为例,parent包含三个module,authenticate依赖authorize,authorize依赖common。
在common和authorize pom中使用以下插件,其他插件都删除
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>${java.version}</source><target>${java.version}</target></configuration></plugin></plugins></build>
在authenticate pom中使用以下插件,注意修改为自己工程的启动类全路径
<build><finalName>security_online_server</finalName><sourceDirectory>main/java</sourceDirectory><outputDirectory>target/classes</outputDirectory><resources><resource><directory>main/java</directory><filtering>false</filtering></resource><resource><directory>main/resources</directory><filtering>true</filtering><excludes><exclude>*.ico</exclude><exclude>*/*.ico</exclude><exclude>*/*/*.ico</exclude><exclude>*/*/*/.ico</exclude></excludes></resource><resource><directory>main/resources</directory><filtering>false</filtering><includes><include>*.ico</include><include>*/*.ico</include><include>*/*/*.ico</include><include>*/*/*/*.ico</include></includes></resource></resources><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>3.3.10</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.1.0</version><configuration><encoding>UTF-8</encoding><nonFilteredFileExtensions><nonFilteredFileExtension>cer</nonFilteredFileExtension><nonFilteredFileExtension>keystore</nonFilteredFileExtension></nonFilteredFileExtensions></configuration></plugin><!--添加配置跳过测试--><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><configuration><skipTests>true</skipTests></configuration></plugin><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><configuration><mainClass>com.hollysys.security.authenticate.SecurityServerApplication</mainClass><buildArgs><buildArg>-H:+AddAllCharsets</buildArg><buildArg>--initialize-at-run-time=org.apache.logging.log4j.LogManager</buildArg><buildArg>--initialize-at-run-time=org.apache.logging.log4j.util.ProviderUtil</buildArg><buildArg>--initialize-at-run-time=org.apache.logging.log4j.spi.Provider</buildArg></buildArgs></configuration></plugin></plugins></build><repositories><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></repository></repositories><pluginRepositories><pluginRepository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></pluginRepository></pluginRepositories>
然后先maven install,确保authorize和common都发布到本地mavne仓库,打graalvm包时才能找到
mybatis
参考Mybatis 使用 GraalVM 构建本地映像_mybatis graalvm-CSDN博客
注意MyBatisNativeConfiguration和@MapperScan(basePackages = "com.xxx.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
mybatis-plus
参考springboot3.2.4+Mybatis-plus在graalvm21环境下打包exe_graalvm打包springboot mybatis plus-CSDN博客
除以上配置外,在reflect-config.json加上
{"name": "org.apache.ibatis.binding.MapperProxy","methods": [{"name": "<init>", "parameterTypes": ["org.apache.ibatis.session.SqlSession", "java.lang.Class"]}]},
在resource-config.json中加上
{"pattern": ".*/.*Mapper\\.xml"},
用来扫描Mapper.xml,否则运行mybatis dao层会报错 Invalid bound statement
log4j2
log4j2明确不支持graalvm,把工程中所有带log4j和logging的依赖都删除,包括第三方依赖。只使用
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId><version>3.3.10</version><scope>compile</scope></dependency>
其中spring-boot-starter-logging在spring-boot-starter-web中自带。
所有日志注解改为@Slf4j
dsl类没有
虽然工程没用到,但提示java.lang.TypeNotPresentException: Type com.querydsl.core.types.Path not present。
在依赖中添加
<dependency>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>6.2.1</version>
<scope>compile</scope>
</dependency>
执行扫描