> 技术文档 > java瘦身、升级graalvm_jdk17 graalvm

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