本文完整代码链接 https://github.com/coralloc8/springboot3-native.git
Graalvm基础自行去官网了解 https://www.graalvm.org/latest/docs/getting-started/
此文章包含如下功能节点,在下述功能节点上做AOT编译(基本能满足日常开发):
- Opendoc:API接口文档,替代swagger
- Webflux:响应式编程
- ReactiveTransactionManager:异步事务、事务切面
- R2dbc、proxy:异步jdbc链接,SQL日志自定义输出
- Mapstruct:替代反射的方式做数据对象转换。dto转bo、vo转dto之类
- Graalvm Feature:自定义Feature,运行时注册相关的类
- Jackson:数据序列化/反序列化时类型转换,枚举转换之类
- Aop:实现全局的日志打印
- ErrorWebExceptionHandler:webflux 全局异常处理 可支持函数式编程方式
- WebFilter:自定义各种filter,MDC,reactor数据透传请求上下文
- Redis:异步redis读取 (2024-06-05 新增)
本文只对如何将springboot3.x项目进行aot编译说明,Graalvm版本 jdk17。
项目对springboot的版本依赖如下:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.0.0-RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
如果项目的父类不是spring-boot-starter-parent的话
<parent>
<groupId>org.spring.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
那么在项目中则需要添加如下profiles (spring-boot-starter-parent项目中默认已经配置下面的profiles)
<!-- 自定义profile -->
<profiles>
<profile>
<id>native</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>add-reachability-metadata</id>
<goals>
<goal>add-reachability-metadata</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-aot</id>
<goals>
<goal>process-aot</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
<profile>
<id>nativeTest</id>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>native-test</id>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-test-aot</id>
<goals>
<goal>process-test-aot</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>
Graalvm aot 编译目前对于springcloud有很多的限制
如官方所说,采用bootstrap.yml的方式走不通。因此得采用spring.config.import的方式来引入naco中的配置文件。
# 远端配置
spring:
config:
import:
- optional:nacos:${spring.cloud.nacos.config.prefix}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}?preference=remote
preference的方式来自nacos官方 https://github.com/alibaba/spring-cloud-alibaba/pull/2459
需要在nacos配置文件中配置 spring.cloud.nacos.config.preference=remote
配置成preference=remote
的话此时配置文件的优先级会变成 nacos>application-{active}.yml>application.yml
nacos的配置文件 (命名空间ID:spring-native 配置文件名:confi-dev.yaml)
server:
port: 8090
coral:
datasource:
url: 192.168.29.66:3306
coral.native:
username: xiaoxiao23
sex: male男a
Graalvm编译不支持 lambda表达式。因此需要自行实现graalvm的feature来支持。
public class RuntimeRegistrationFeature implements Feature {
private static final String PACKAGE_NAME = "com.coral.test.spring.natives";
@Override
public void duringSetup(DuringSetupAccess access) {
// 扫描指定包下IService IRepository的字类(实现类),然后全部注册到graalvm Lambda 序列化中
RuntimeRegistrationFeature.findClasses(PACKAGE_NAME, Set.of(IService.class, IRepository.class))
.forEach(RuntimeSerialization::registerLambdaCapturingClass);
RuntimeSerialization.register(SerializedLambda.class, IGetter.class);
}
/**
* 找到某个包下面指定的父类的所有子类
*
* @param packageName 包名
* @param superClasses 父类
* @return 子类集合
*/
private static Set<Class<?>> findClasses(String packageName, Set<Class<?>> superClasses) {
Set<Class<?>> classes = new HashSet<>();
for (Class<?> superClass : superClasses) {
classes.addAll(ClassUtil.scanPackageBySuper(packageName, superClass));
}
return classes;
}
}
Graalvm不支持java中的反射操作,需要自行通过 native-image-agent
来对java jar生成 native-image映射文件。
常规操作是先将 普通的springboot项目打包成 jar可执行文件,然后再用以下命令来生成 native-image 文件
java -agentlib:native-image-agent=config-output-dir=./config -jar target/spring-native-test-1.0.0-SNAPSHOT.jar
config-output-dir 为生成的文件所保存的目录。
执行完上述命令后,jar文件应该正常运行起来了,此时访问 http://localhost:8090/doc.html
进入到项目api文档。最保险的做法是将文档中的所有api接口调用一遍,包括进入到首页时的一些静态文件,强制刷新首页。这样才能让native探针充分捕捉到项目所需要的native-image文件。操作完成后,jar进程就可以杀掉了。
此时查看 config目录,应该会发现生成了一系列文件。
这些文件中需要说明的是jni文件中需要增加如下配置,不然编译过程中会报dns空值异常。
{
"name":"sun.net.dns.ResolverConfigurationImpl",
"fields":[{"name":"os_nameservers"}, {"name":"os_searchlist"}]
}
本文生成native-image映射文件采用的另外一种方式。
通过spring-boot-maven-plugin插件来配置生成映射文件。如此只需要执行
spring-boot:start spring-boot:stop即可。
start启动项目后,访问项目doc.html接口文档,仍然需要将文档中的所有api接口调用一遍,包括进入到首页时的一些静态文件,强制刷新首页。让native探针充分捕捉到项目所需要的native-image文件。
stop最终杀掉项目。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>
-Dfile.encoding=UTF-8
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/jdk.internal.access=ALL-UNNAMED
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/
</jvmArguments>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
上述两种方式生成的映射文件最终都需要放在项目 resources目录下。在resources目录下新建 META-INF/native-image
目录,然后将上述生成的映射文件全部放进去。
springboot process-aot编译也会生成native-image映射文件。保存目录在META-INF/native-image/{groupId}/{artifactId}/
下。因此在native-maven-plugin
插件中新增如下编译配置。
<buildArg>
-H:ReflectionConfigurationResources=META-INF/native-image/reflect-config.json,META-INF/native-image/com.coral.test/spring-native-test/reflect-config.json
-H:ResourceConfigurationResources=META-INF/native-image/resource-config.json,META-INF/native-image/com.coral.test/spring-native-test/resource-config.json
-H:DynamicProxyConfigurationResources=META-INF/native-image/proxy-config.json,META-INF/native-image/com.coral.test/spring-native-test/proxy-config.json
-H:JNIConfigurationResources=META-INF/native-image/jni-config.json
</buildArg>
接下来正式开始AOT编译。
步骤执行完后会发现在target目录下生成了jar文件和exe文件
instrument.dll
文件是因为引入了nacos。所以生成了该文件。
本文的项目中已经配置完整的编译。因此可以采用如下命令一键编译。
mvn -Pnative -Pdev native:compile-no-fork -f pom.xml
在编译过程中会报各种类文件编译时机不对的问题。有的类需要在build阶段,有的类需要在run阶段初始化。类似下面配置。哪个类有问题就增加哪个类。
如下述错误
Error: Classes that should be initialized at run time got initialized during image building:
ch.qos.logback.core.util.StatusPrinter was unintentionally initialized at build time. To see why ch.qos.logback.core.util.StatusPrinter got initialized use --trace-class-initialization=ch.qos.logback.core.util.StatusPrinter
ch.qos.logback.classic.Logger was unintentionally initialized at build time. To see why ch.qos.logback.classic.Logger got initialized use --trace-class-initialization=ch.qos.logback.classic.Logger
ch.qos.logback.classic.Level was unintentionally initialized at build time. To see why ch.qos.logback.classic.Level got initialized use --trace-class-initialization=ch.qos.logback.classic.Level
ch.qos.logback.core.status.StatusBase was unintentionally initialized at build time. To see why ch.qos.logback.core.status.StatusBase got initialized use --trace-class-initialization=ch.qos.logback.core.status.StatusBase
ch.qos.logback.core.util.Loader was unintentionally initialized at build time. To see why ch.qos.logback.core.util.Loader got initialized use --trace-class-initialization=ch.qos.logback.core.util.Loader
ch.qos.logback.core.CoreConstants was unintentionally initialized at build time. To see why ch.qos.logback.core.CoreConstants got initialized use --trace-class-initialization=ch.qos.logback.core.CoreConstants
ch.qos.logback.core.status.InfoStatus was unintentionally initialized at build time. To see why ch.qos.logback.core.status.InfoStatus got initialized use --trace-class-initialization=ch.qos.logback.core.status.InfoStatus
org.slf4j.LoggerFactory was unintentionally initialized at build time. To see why org.slf4j.LoggerFactory got initialized use --trace-class-initialization=org.slf4j.LoggerFactory
To see how the classes got initialized, use --trace-class-initialization=ch.qos.logback.core.util.StatusPrinter,ch.qos.logback.classic.Logger,ch.qos.logback.classic.Level,ch.qos.logback.core.status.StatusBase,ch.qos.logback.core.util.Loader,ch.qos.logback.core.CoreConstants,ch.qos.logback.core.status.InfoStatus,org.slf4j.LoggerFactory
<buildArg>
<!-- logback -->
--initialize-at-build-time=ch.qos.logback.core.model.processor.DefaultProcessor$1
--initialize-at-build-time=ch.qos.logback.core.model.processor.ChainedModelFilter$1
--initialize-at-build-time=ch.qos.logback.core.model.processor.ImplicitModelHandler$1
--initialize-at-build-time=ch.qos.logback.core.CoreConstants
--initialize-at-build-time=ch.qos.logback.core.subst.Token
--initialize-at-build-time=ch.qos.logback.core.subst.Parser$1
--initialize-at-build-time=ch.qos.logback.core.subst.NodeToStringTransformer$1
--initialize-at-build-time=ch.qos.logback.core.util.Duration
--initialize-at-build-time=ch.qos.logback.core.util.Loader
--initialize-at-build-time=ch.qos.logback.core.util.StatusPrinter
--initialize-at-build-time=ch.qos.logback.core.status.StatusBase
--initialize-at-build-time=ch.qos.logback.core.status.InfoStatus
--initialize-at-build-time=ch.qos.logback.core.status.WarnStatus
--initialize-at-build-time=ch.qos.logback.classic.Level
--initialize-at-build-time=ch.qos.logback.classic.Logger
--initialize-at-build-time=ch.qos.logback.classic.model.processor.LogbackClassicDefaultNestedComponentRules
<!-- slf4j -->
--initialize-at-build-time=org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.slf4j.MDC,org.slf4j.impl.StaticLoggerBinder
<!-- netty -->
--initialize-at-run-time=io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler,io.netty.handler.codec.http2.Http2ServerUpgradeCodec
<!-- -->
--initialize-at-run-time=sun.net.dns.ResolverConfigurationImpl
--initialize-at-build-time=cn.hutool.core.util.ClassLoaderUtil
--initialize-at-build-time=cn.hutool.core.convert.BasicType
--initialize-at-build-time=cn.hutool.core.util.CharsetUtil
<!-- features -->
--features=com.coral.test.spring.natives.core.feature.RuntimeRegistrationFeature
</buildArg>
最终生成的完整编译文件为
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<!-- imageName用于设置生成的二进制文件名称 -->
<imageName>${project.artifactId}</imageName>
<!-- mainClass用于指定main方法类路径 -->
<mainClass>com.coral.test.spring.natives.MainApplication</mainClass>
<!-- <useArgFile>true</useArgFile>-->
<skip>false</skip>
<!-- 如果要启用调试信息的生成,请在插件配置中提供以下内容-->
<debug>false</debug>
<!--启用详细输出-->
<verbose>false</verbose>
<!--
-H:ConfigurationFileDirectories=:指定配置文件的直接目录,多个项目之间用逗号分隔。在该目录中按默认方式的命名的 json 配置文件都可以被自动识别。
-H:ConfigurationResourceRoots=:指定配置资源的根路径,多个项目之间用逗号分隔。配置文件不仅可以被当作外部文件读取,也可以被当作 resource 资源读取。这种方式适用于读取存放在 jar 文件中的配置文件。
-H:XXXConfigurationFiles=:指定某一种类型的配置文件,多个项目之间用逗号分隔。这里的 XXX 可以是 Reflection、DynamicProxy、Serialization、SerializationDeny、Resource、JNI 或 PredefinedClasses。
-H:XXXConfigurationResources=:指定某一种类型的配置资源的路径,多个项目之间用逗号分隔。这里的 XXX 可以是 Reflection、DynamicProxy、Serialization、SerializationDeny、Resource、JNI 或 PredefinedClasses。
-->
<buildArgs combine.children="append">
<buildArg>
--verbose
-Djavax.xml.accessExternalDTD=all
-Dfile.encoding=UTF-8
-H:+AddAllCharsets
-H:+ReportExceptionStackTraces
<!-- -H:+PrintClassInitialization-->
--allow-incomplete-classpath
--report-unsupported-elements-at-runtime
</buildArg>
<!-- 反射 -->
<buildArg>
-H:ReflectionConfigurationResources=META-INF/native-image/reflect-config.json,META-INF/native-image/com.coral.test/spring-native-test/reflect-config.json
-H:ResourceConfigurationResources=META-INF/native-image/resource-config.json,META-INF/native-image/com.coral.test/spring-native-test/resource-config.json
-H:DynamicProxyConfigurationResources=META-INF/native-image/proxy-config.json,META-INF/native-image/com.coral.test/spring-native-test/proxy-config.json
-H:JNIConfigurationResources=META-INF/native-image/jni-config.json
</buildArg>
<buildArg>
<!-- logback -->
--initialize-at-build-time=ch.qos.logback.core.model.processor.DefaultProcessor$1
--initialize-at-build-time=ch.qos.logback.core.model.processor.ChainedModelFilter$1
--initialize-at-build-time=ch.qos.logback.core.model.processor.ImplicitModelHandler$1
--initialize-at-build-time=ch.qos.logback.core.CoreConstants
--initialize-at-build-time=ch.qos.logback.core.subst.Token
--initialize-at-build-time=ch.qos.logback.core.subst.Parser$1
--initialize-at-build-time=ch.qos.logback.core.subst.NodeToStringTransformer$1
--initialize-at-build-time=ch.qos.logback.core.util.Duration
--initialize-at-build-time=ch.qos.logback.core.util.Loader
--initialize-at-build-time=ch.qos.logback.core.util.StatusPrinter
--initialize-at-build-time=ch.qos.logback.core.status.StatusBase
--initialize-at-build-time=ch.qos.logback.core.status.InfoStatus
--initialize-at-build-time=ch.qos.logback.core.status.WarnStatus
--initialize-at-build-time=ch.qos.logback.classic.Level
--initialize-at-build-time=ch.qos.logback.classic.Logger
--initialize-at-build-time=ch.qos.logback.classic.model.processor.LogbackClassicDefaultNestedComponentRules
<!-- slf4j -->
--initialize-at-build-time=org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.slf4j.MDC,org.slf4j.impl.StaticLoggerBinder
<!-- netty -->
--initialize-at-run-time=io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler,io.netty.handler.codec.http2.Http2ServerUpgradeCodec
<!-- -->
--initialize-at-run-time=sun.net.dns.ResolverConfigurationImpl
--initialize-at-build-time=cn.hutool.core.util.ClassLoaderUtil
--initialize-at-build-time=cn.hutool.core.convert.BasicType
--initialize-at-build-time=cn.hutool.core.util.CharsetUtil
<!-- features -->
--features=com.coral.test.spring.natives.core.feature.RuntimeRegistrationFeature
</buildArg>
<buildArg>
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.nio=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/jdk.internal.access=ALL-UNNAMED
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
</buildArg>
</buildArgs>
</configuration>
</plugin>
r2dbc的使用说明: https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#r2dbc.core
redis 编译相关
java.lang.UnsatisfiedLinkError: jdk.jfr.internal.JVM.getHandler(Ljava/lang/Class;)Ljava/lang/Object; [symbol: Java_jdk_jfr_internal_JVM_getHandler or Java_jdk_jfr_internal_JVM_getHandler__Ljava_lang_Class_2]
at org.graalvm.nativeimage.builder/com.oracle.svm.core.jni.access.JNINativeLinkage.getOrFindEntryPoint(JNINativeLinkage.java:152) ~[na:na]
at org.graalvm.nativeimage.builder/com.oracle.svm.core.jni.JNIGeneratedMethodSupport.nativeCallAddress(JNIGeneratedMethodSupport.java:53) ~[na:na]
at jdk.jfr@17.0.10/jdk.jfr.internal.JVM.getHandler(Native Method) ~[na:na]
at jdk.jfr@17.0.10/jdk.jfr.internal.Utils.getHandler(Utils.java:449) ~[na:na]
at jdk.jfr@17.0.10/jdk.jfr.internal.MetadataRepository.getHandler(MetadataRepository.java:174) ~[na:na]
at jdk.jfr@17.0.10/jdk.jfr.internal.MetadataRepository.register(MetadataRepository.java:135) ~[na:na]
at jdk.jfr@17.0.10/jdk.jfr.internal.MetadataRepository.register(MetadataRepository.java:130) ~[na:na]
at jdk.jfr@17.0.10/jdk.jfr.FlightRecorder.register(FlightRecorder.java:136) ~[na:na]
at io.lettuce.core.event.connection.JfrConnectionCreatedEvent.<clinit>(JfrConnectionCreatedEvent.java) ~[auth-service:6.3.1.RELEASE/12e6995]
at java.base@17.0.10/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[auth-service:na]
at java.base@17.0.10/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[auth-service:na]
at io.lettuce.core.event.jfr.JfrEventRecorder.createEvent(JfrEventRecorder.java:108) ~[na:na]
reactor.core.Exceptions 304 warn - throwIfFatal detected a jvm fatal exception, which is thrown and logged below:java.lang.NoClassDefFoundError: Could not initialize class io.lettuce.core.event.connection.JfrConnectionCreatedEvent
at java.base@17.0.10/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint 鈬?Handler com.coral.test.spring.natives.controller.EventLogController#findEventLog(String) [DispatcherHandler]
Original Stack Trace:
at java.base@17.0.10/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
at java.base@17.0.10/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at io.lettuce.core.event.jfr.JfrEventRecorder.createEvent(JfrEventRecorder.java:108)
解决方案 (2024-06-05 官方说jfr在windows上暂不支持)
2024-06-12 经测试在centos7上确实不会报此错误
https://github.com/oracle/graal/issues/8623
https://www.graalvm.org/latest/reference-manual/native-image/guides/build-and-run-native-executable-with-jfr/
https://github.com/oracle/graal/issues/5558
另外一种方式是将jfr功能禁用掉(2024-06-17)
https://github.com/redis/lettuce/wiki/Connection-Events#java-flight-recorder-events-since-61
io.lettuce.core.event.jfr.EventRecorderHolder类中该变量的
private static final String JFR_ENABLED_KEY = "io.lettuce.core.jfr";
private static final boolean JFR_ENABLED = Boolean.parseBoolean(SystemPropertyUtil.get(JFR_ENABLED_KEY, "true"));
因此需要设置
System.setProperty("io.lettuce.core.jfr", "false");