GraalVM编译Springboot3

819 阅读3分钟

使用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编译生成的

image.png

编译参数

新增配置文件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编译遇到的坑

  1. 必须#生成编译配置#后在进行编译,自动生成jni-config.json/predefined-classes-config.json/proxy-config.json/reflect-config.json/resource-config.json/serialization-config.json

  2. log4j2对GraalVM支持不友好,换为springboot自带的logback进行日志记录

  3. netty的websocket client会自动使用DNS解析器,会报错:java.lang.NoClassDefFoundError: Could not initialize class io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder

    1. 需要手动配置自定义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);
  1. GraalVM应用不支持动态代理(cglib和jdk动态代理)

    1. 禁用动态代理:spring.aop.proxy-target-class=false或@EnableAspectJAutoProxy(proxyTargetClass = false)
    2. 换用静态AOP工具aspectj代替spring aop
    3. 禁止使用@Lazy注解(@Lazy注解会强制使用代理)
    4. 禁止使用Lombok(主要是@Builder注解会使用动态代理)
    5. fastjson2禁止ASM(会在运行时动态生成类来创建对象),通过在configure类中配置System.setProperty("fastjson2.creator", "lambda");更改序列化方式
  2. caffeine在运行时会动态生成类,需要将这些类手动标记,不然会报错: java.lang.ClassNotFoundException: com.github.benmanes.caffeine.cache.SSMSA

    1. 具体有哪些类,请参考生成的reflect-config.json中的com.github.benmanes.caffeine.cache.*,不同版本的caffeine,可能需要手动配置不同版本的类
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);