java瘦身、升级graalvm_jdk17 graalvm
一.graalvm简介
传统的hotvm,需要先启动 jvm 再载入 java 代码,然后再即时的将 .class 字节码编译成机器码,交给机器执行。通过打包插件会将项目打成jar包,jar里是class文件,包含自定义代码和maven引用的所有包。
graalvm使用了 Ahead-of-time compile(提前编译),也就是说,他在编译期时,会把所有相关的东西,包含一个基底的 VM,一起编译成机器码,这个基底 VM 是 GraalVM 内部才有的东西,他只包含最基本的线程排成机制、垃圾回收,尽可能的缩小必要的 jvm 体积。通过graalvm打包插件,仅会把实际用到的代码打成一个可执行文件。
graalvm aot功能,运行时省去了加载class这个过程,同时只把相关代码编译,避免传统打包将所有依赖的包都编译,所以省去了类加载的内存空间。相对于传统jar包运行,graalvm aot省近一半内存占用。
二.版本选择
项目大多使用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中添加插件,注意修改启动类
org.springframework.boot spring-boot-maven-plugin 3.3.10 org.graalvm.buildtools native-maven-plugin com.example.demo.DemoApplication -H:+AddAllCharsets --initialize-at-run-time=org.apache.logging.log4j.LogManager --initialize-at-run-time=org.apache.logging.log4j.util.ProviderUtil --initialize-at-run-time=org.apache.logging.log4j.spi.Provider org.apache.maven.plugins maven-surefire-plugin true spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false
执行mvn clean package 打jar包
在target目录下,执行java -agentlib:native-image-agent=config-output-dir= -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
以demo为例,parent包含三个module,authenticate依赖authorize,authorize依赖common。
在common和authorize pom中使用以下插件,其他插件都删除
org.apache.maven.pluginsmaven-compiler-plugin${java.version}${java.version}
在authenticate pom中使用以下插件,注意修改为自己工程的启动类全路径
security_online_server main/java target/classes main/java false main/resources true *.ico */*.ico */*/*.ico */*/*/.ico main/resources false *.ico */*.ico */*/*.ico */*/*/*.ico org.springframework.boot spring-boot-maven-plugin 3.3.10 org.apache.maven.plugins maven-resources-plugin 3.1.0 UTF-8 cer keystore org.apache.maven.plugins maven-surefire-plugin true org.graalvm.buildtools native-maven-plugin com.hollysys.security.authenticate.SecurityServerApplication -H:+AddAllCharsets --initialize-at-run-time=org.apache.logging.log4j.LogManager --initialize-at-run-time=org.apache.logging.log4j.util.ProviderUtil --initialize-at-run-time=org.apache.logging.log4j.spi.Provider spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot false
然后先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\": \"\", \"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的依赖都删除,包括第三方依赖。只使用
org.projectlombok lombok org.springframework.boot spring-boot-starter-logging 3.3.10 compile
其中spring-boot-starter-logging在spring-boot-starter-web中自带。
所有日志注解改为@Slf4j
dsl类没有
虽然工程没用到,但提示java.lang.TypeNotPresentException: Type com.querydsl.core.types.Path not present。
在依赖中添加
io.github.openfeign.querydsl
querydsl-jpa
6.2.1
compile
执行扫描
json
fastjson在打包时有问题,需要改为gson或jackson。
最好使用jackson,因为springboot HttpMessageConverter默认使用jackson处理请求,spring很多地方都使用MappingJackson2HttpMessageConverter,resttemplate也使用MappingJackson2HttpMessageConverter。而用gson与MappingJackson2HttpMessageConverter有下面的问题。
如果改为gson,接口响应结构直接使用gson的JsonObject时,可能出现com.fasterxml.jackson.databind.exc.InvalidDefinitionException,因为springboot响应序列化HttpMessageConverter默认使用jackson(MappingJackson2HttpMessageConverter),jackson序列化gson结构不兼容,可以把springboot默认的改为GsonHttpMessageConverter,参考Spring Boot 3.x特性-JSON(gson,jackson,json-b,fastjson)_springboot3 jackson-CSDN博客
如果响应结构包含Instant类,GsonHttpMessageConverter默认无法序列化,需要自定义InstantTypeAdapter配置给gson,手动配置GsonHttpMessageConverter
@Configurationpublic class GsonConfig { /** * 获取令牌接口,返回属性带有Instant * @return */ @Bean public Gson gson() { return new GsonBuilder() .registerTypeAdapter(Instant.class, new InstantTypeAdapter()) .setDateFormat(\"yyyy-MM-dd\'T\'HH:mm:ss\'Z\'\") // 可选:统一日期格式 .create(); } /** * 使用gson作为mvc 请求响应的序列化工具,在配置文件preferred-json-mapper项配置 * @param gson * @return */ @Bean public GsonHttpMessageConverter gsonHttpMessageConverter(Gson gson) { GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); converter.setGson(gson); return converter; }}public class InstantTypeAdapter extends TypeAdapter { @Override public void write(JsonWriter out, Instant value) throws IOException { // 序列化为 ISO-8601 字符串(如 \"2025-05-12T08:30:00Z\") out.value(value.toString()); } @Override public Instant read(JsonReader in) throws IOException { // 从字符串反序列化 return Instant.parse(in.nextString()); }}
获取配置和环境变量
在执行文件同目录下新建文件夹config,放入application.yml,启动graalvm执行文件会读取application.yml。
@Value(${}) 可以生效,这种普通注入属性值可以,但根据@Conditional生成bean不可以。
因为普通配置的@Value在运行时通过Spring的Environment抽象层获取,而GraalVM在构建时虽然内联了初始值,但Spring Boot在启动后仍会重新加载外部配置文件(如config目录下的application.yml),覆盖内联值。然而,条件装配的注解(如@Conditional)在AOT阶段已经被解析,并生成对应的Bean定义代码,这些代码在运行时不会重新执行条件判断,导致修改后的配置无法影响Bean的存在性。
内部类
下面代码AccountDetailOrg作为内部类,在agent扫描时即使执行相关代码,生成的reflect文件中虽然有AccountDetailOrg类,但可能缺少orgId、orgName的get set方法,需要手动在reflect文件加。
@Datapublic class AccountDetail { private String id; private String userName; @Data public static class AccountDetailOrg{ private String orgId; private String orgName; } @Data public static class AccountDetailGroup{ private String groupId; private String groupName; }}
手动添加方法后的文件如下,添加了get、set方法
{ \"name\":\"com.hollysys.security.authorize.dto.AccountDetail$AccountDetailOrg\", \"allDeclaredFields\":true, \"queryAllDeclaredMethods\":true, \"queryAllDeclaredConstructors\":true, \"methods\":[ {\"name\":\"\",\"parameterTypes\":[] }, { \"name\": \"setOrgId\", \"parameterTypes\": [\"java.lang.String\"] }, { \"name\": \"getOrgId\", \"parameterTypes\": [] }, { \"name\": \"setOrgName\", \"parameterTypes\": [\"java.lang.String\"] }, { \"name\": \"getOrgName\", \"parameterTypes\": [] } ]},
sqlite arm
arm下graalvm aot打包 使用sqlite
在resouce-config.json加以下内容,把sqlite arm的文件扫描到
{ \"pattern\": \".*/libsqlitejdbc\\\\.so$\" }, { \"pattern\": \".*/Linux/aarch64/.*\" },
maven版本
org.xerial
sqlite-jdbc
3.36.0
jvm参数设置
graalvm默认使用串行回收器,在执行文件后可以直接加参数,如./security-online-authenticate -XX:MaximumHeapSizePercent=100 ,这样可以将最大堆内存设置为物理
spring激活配置文件
jar包方式可以在运行时修改spring.profiles.active。但graalvm不行,在打包时就要确定spring.profiles.active是生产环境的值,不可以在打包后再修改,因为这个参数已经被打入可执行文件当中。
接口RequestParam参数
x86 打包graalvm,有时不识别 “@RequestParam String name” 这个name参数,会识别问arg1。所以要明确参数名称,如@RequestParam(value=\"name\") String name