使用GraalVM编译为可执行native程序
注:以下仅为个人经验,可能会有错误的认知,欢迎大家指出
dependency:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>xxx</groupId>
<artifactId>xxx</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>xxx</name>
<description>xxx</description>
<properties>
<java.version>21</java.version>
<spring.boot.version>3.4.1</spring.boot.version>
<hutool.version>5.8.29</hutool.version>
<QLExpress.version>3.3.4</QLExpress.version>
<junit.version>4.13.2</junit.version>
<springdoc.version>2.6.0</springdoc.version>
<fastjson2.version>2.0.54</fastjson2.version>
<quartz.version>2.5.0</quartz.version>
</properties>
<dependencies>
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>compile</scope>
</dependency>
<!--引入Netty-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- Spring Boot Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.lmax/disruptor -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>${QLExpress.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>23.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.22</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
</dependencies>
拉取包含maven的graalvm镜像:
docker pull vegardit/graalvm-maven:21.0.2
运行镜像:
docker run --platform linux/arm64 --name graalvm-native-image -v $(pwd):/Advantech-EMS -w /Advantech-EMS -it --rm --entrypoint /bin/bash vegardit/graalvm-maven:21.0.2
使用graalvm将jar包编译为可执行文件:
docker run --platform linux/arm64 --name graalvm-native-image -v $(pwd):/work_dir -w /Advantech-EMS -it --rm --entrypoint /bin/bash vegardit/graalvm-maven:21.0.2
编译原生镜像
mvn -X clean -DskipTests -Pnative native:compile
编译容器镜像
mvn -X clean -DskipTests -Pnative spring-boot:build-image
springboot3.x默认集成了native支持,直接配置maven插件编译即可
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<configuration>
<!--<jvmArguments>-agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image</jvmArguments>-->
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- 打包过程忽略Junit测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<fork>true</fork>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.advantech.ems.AdvantechEmsApplication</mainClass>
<!--<addClasspath>true</addClasspath>-->
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.15.0</version>
<configuration>
<complianceLevel>21</complianceLevel>
<source>21</source>
<target>21</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes><!--扫描目录下的xml文件-->
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>application.yml</include>
</includes>
</resource>
</resources>
</build>
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<mainClass>com.advantech.ems.AdvantechEmsApplication</mainClass>
</configuration>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
生成编译配置
java -agentlib:native-image-agent=config-output-dir=src\main\resources\META-INF\native-image --add-opens=java.base/java.util=ALL-UNNAMED -Xms128m -Xmx1536m -XX:MaxDirectMemorySize=1024m -XX:+UseZGC -XX:+HeapDumpOnOutOfMemoryError -jar .\target\your_jar.jar
注:这里的jar包是普通mvn clean package编译生成的
编译参数
新增配置文件native-image.properties
initialize-at-run-time的参数,我是在编译的时候报错,根据报错提示(会提示建议XXX类添加到initialize-at-run-time),一点点添加完善的
Args=--initialize-at-run-time=org.springframework.boot.loader.nio.file.NestedFileSystemProvider,org.slf4j,org.apache,com.sun.jmx.mbeanserver.JmxMBeanServer,javax.management.MBeanServerFactory,io.netty.buffer.AbstractReferenceCountedByteBuf,com.fasterxml.jackson.databind.ObjectMapper,org.springframework.boot.logging.LoggingSystem,io.netty.buffer.UnpooledUnsafeDirectByteBuf,io.netty.buffer.PooledByteBufAllocator,io.netty.buffer.ByteBufUtil,io.netty.handler.codec.http2.Http2CodecUtil,io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf,io.netty.buffer.AbstractReferenceCountedByteBuf,io.netty.buffer.UnpooledHeapByteBuf,io.netty.buffer.UnpooledUnsafeHeapByteBuf,io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf,io.netty.buffer.UnpooledUnsafeDirectByteBuf,io.netty.buffer.PooledByteBufAllocator,io.netty.buffer.UnpooledDirectByteBuf \
--trace-class-initialization=io.netty.buffer.UnpooledUnsafeDirectByteBuf,io.netty.buffer.UnpooledDirectByteBuf,io.netty.buffer.AbstractReferenceCountedByteBuf,io.netty.buffer.UnpooledUnsafeHeapByteBuf,io.netty.buffer.UnpooledHeapByteBuf,io.netty.handler.codec.http2.Http2CodecUtil,io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf,io.netty.buffer.PooledByteBufAllocator,io.netty.buffer.ByteBufUtil,io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf\
--enable-url-protocols=resource,file,http,https \
--no-fallback \
--verbose \
-H:ConfigurationFileDirectories=target/classes/META-INF/native-image
native编译遇到的坑
-
必须#生成编译配置#后在进行编译,自动生成jni-config.json/predefined-classes-config.json/proxy-config.json/reflect-config.json/resource-config.json/serialization-config.json
-
log4j2对GraalVM支持不友好,换为springboot自带的logback进行日志记录
-
netty的websocket client会自动使用DNS解析器,会报错:
java.lang.NoClassDefFoundError: Could not initialize class io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder- 需要手动配置自定义DNS解析器
public class EMSDnsResolver extends AddressResolverGroup<InetSocketAddress> {
private static final Logger log = LoggerFactory.getLogger(EMSDnsResolver.class);
@Override
protected AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) {
// 创建一个自定义的 NameResolver
NameResolver<InetAddress> nameResolver = new NameResolver<InetAddress>() {
@Override
public Future<InetAddress> resolve(String inetHost) {
return null;
}
@Override
public Future<InetAddress> resolve(String inetHost, Promise<InetAddress> promise) {
log.info("Resolving domain: {}", inetHost); // 打印域名
try {
// 自定义 DNS 解析逻辑
/*if ("example.com".equals(inetHost)) {
// 将 "example.com" 解析为固定 IP
InetAddress resolvedAddress = InetAddress.getByName("192.168.1.100");
return promise.setSuccess(resolvedAddress);
}*/
// 默认解析逻辑
InetAddress resolvedAddress = InetAddress.getByName(inetHost);
return promise.setSuccess(resolvedAddress);
} catch (UnknownHostException e) {
promise.setFailure(e);
return promise;
}
}
@Override
public Future<List<InetAddress>> resolveAll(String inetHost) {
return null;
}
@Override
public Promise<List<InetAddress>> resolveAll(String inetHost, Promise<List<InetAddress>> promise) {
log.info("Resolving all addresses for domain: {}", inetHost); // 打印域名
try {
// 自定义 DNS 解析逻辑
/*if ("example.com".equals(inetHost)) {
// 将 "example.com" 解析为固定 IP
List<InetAddress> resolvedAddresses = List.of(InetAddress.getByName("192.168.1.100"));
return promise.setSuccess(resolvedAddresses);
}*/
// 默认解析逻辑
List<InetAddress> resolvedAddresses = List.of(InetAddress.getByName(inetHost));
return promise.setSuccess(resolvedAddresses);
} catch (UnknownHostException e) {
promise.setFailure(e);
return promise;
}
}
@Override
public void close() {
// 清理资源
}
};
// 创建 InetSocketAddressResolver
return new InetSocketAddressResolver(executor, nameResolver);
}
}
@Bean
public HttpClient httpClient() {
return HttpClient.create()
.resolver(new EMSDnsResolver()); // 使用默认的地址解析器,禁用 DNS 解析
}
//创建websocket client
WebSocketClient client = new ReactorNettyWebSocketClient(httpClient, builderSupplier);
-
GraalVM应用不支持动态代理(cglib和jdk动态代理)
- 禁用动态代理:spring.aop.proxy-target-class=false或@EnableAspectJAutoProxy(proxyTargetClass = false)
- 换用静态AOP工具aspectj代替spring aop
- 禁止使用@Lazy注解(@Lazy注解会强制使用代理)
- 禁止使用Lombok(主要是@Builder注解会使用动态代理)
- fastjson2禁止ASM(会在运行时动态生成类来创建对象),通过在configure类中配置
System.setProperty("fastjson2.creator", "lambda");更改序列化方式
-
caffeine在运行时会动态生成类,需要将这些类手动标记,不然会报错:
java.lang.ClassNotFoundException: com.github.benmanes.caffeine.cache.SSMSA- 具体有哪些类,请参考生成的reflect-config.json中的
com.github.benmanes.caffeine.cache.*,不同版本的caffeine,可能需要手动配置不同版本的类
- 具体有哪些类,请参考生成的reflect-config.json中的
package com.advantech.ems.config.disruptor.cache;
import org.springframework.aot.hint.*;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import java.util.List;
/**
* @author Yukui.He
* @version 1.0
* @description: TODO
* @date 2025/2/6 15:49
*/
@ImportRuntimeHints(RuntimeHintsConfig.SsmsRuntimeHintsRegistrar.class)
@Configuration
class RuntimeHintsConfig {
static class SsmsRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection().registerType(
TypeReference.of("com.github.benmanes.caffeine.cache.PS"),
MemberCategory.PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
hints.reflection().registerType(
TypeReference.of("com.github.benmanes.caffeine.cache.PSW"),
MemberCategory.PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
hints.reflection().registerType(
TypeReference.of("com.github.benmanes.caffeine.cache.PSWMS"),
MemberCategory.PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
hints.reflection().registerType(
TypeReference.of("com.github.benmanes.caffeine.cache.SSSMSW"),
MemberCategory.PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
hints.reflection().registerType(
TypeReference.of("com.github.benmanes.caffeine.cache.SSMSA"),
MemberCategory.PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
hints.reflection().registerType(
TypeReference.of("com.github.benmanes.caffeine.cache.SSSW"),
MemberCategory.PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
}
}
}
5. 在native应用中,fastjosn反序列化内部类会失败(网上有解决方法说改为静态内部类,但我改完后依然报错)
1. 解决方法:不使用内部类
6. quartz自定义job初始化问题(报错:No default constructor found)
1. 即使已添加无参构造函数,并在reflect-config.json中配置,但依然会报错
2. 解决:自定义JobFactory,不使用反射创建job
public class PrototypeSpringBeanJobFactory extends AdaptableJobFactory {
private final AutowireCapableBeanFactory beanFactory;
public PrototypeSpringBeanJobFactory(AutowireCapableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Class<? extends Job> jobClass = bundle.getJobDetail().getJobClass();
if (GeneralActionJob.class == jobClass)
return new GeneralActionJob();
else if (TimePeriodActionJob.class == jobClass)
return new TimePeriodActionJob();
else {
// 从容器中获取原型 Bean
Object job = beanFactory.getBean(bundle.getJobDetail().getJobClass());
beanFactory.autowireBean(job); // 自动注入依赖
return job;
}
}
}
// 手动指定JobFactory
StdSchedulerFactory stdSchedulerFactory = new StdSchedulerFactory();
stdSchedulerFactory.initialize(quartzFactoryProperties);
Scheduler scheduler = stdSchedulerFactory.getScheduler();
scheduler.setJobFactory(prototypeSpringBeanJobFactory);