https://docs.spring.io/spring-boot/docs/3.2.0/reference/htmlsingle/#native-image
GraalVM 本地镜像(Native Images)是可以通过提前处理已编译的 Java 应用程序生成的独立可执行文件。与 JVM 对应的应用相比,本地镜像通常具有更小的内存占用,并且启动速度更快。
GraalVM 本地镜像介绍
GraalVM 本地镜像为部署和运行 Java 应用程序提供了新的方式。与传统的 Java 虚拟机相比,本地镜像可以在较小的内存占用下运行,并且启动速度更快。
它们非常适合使用容器镜像部署的应用程序,尤其是与“函数即服务”(FaaS)平台结合使用时,这一点尤为突出。
与传统的为 JVM 编写的应用程序不同,GraalVM 本地镜像应用程序需要提前处理以创建可执行文件。这种提前处理涉及从主入口点静态分析你的应用程序代码。
GraalVM 本地镜像是完整的、特定于平台的可执行文件。你无需携带 Java 虚拟机即可运行本地镜像。
与 JVM 部署的关键差异
由于 GraalVM 本地镜像是在提前处理时生成的,因此本地镜像和基于 JVM 的应用程序之间存在一些关键差异。主要的差异包括:
- 从
main
入口点在应用程序构建时对应用程序进行静态分析。 - 在创建本地镜像时无法访问的代码将被删除,不会成为可执行文件的一部分。
- GraalVM 并不直接了解代码的动态元素,必须告诉它关于反射、资源、序列化和动态代理的信息。
- 应用程序的类路径在构建时固定,不能更改。
- 没有延迟类加载,可执行文件中包含的所有内容将在启动时加载到内存中。
- Java 应用程序的某些方面存在一些限制,这些限制在 GraalVM 中可能无法得到完全支持。
除了上述差异之外,Spring 还使用了一个名为 Spring 提前处理(Spring Ahead-of-Time processing)的过程,这进一步施加了限制。
理解 Spring 提前处理
典型的 Spring Boot 应用程序相当动态,并且配置是在运行时执行的。事实上,Spring Boot 自动配置的概念在很大程度上依赖于对运行时状态的响应,以便正确配置。
尽管可以告诉 GraalVM 关于应用程序的这些动态方面,但这样做将失去静态分析的大部分好处。因此,当使用 Spring Boot 创建本地镜像时,会假设一个封闭的世界,并限制应用程序的动态方面。
封闭世界的假设意味着,除了 GraalVM 本身创造的限制之外,还有以下限制:
1)在你的应用程序中定义的 bean 不能在运行时更改,这意味着:
- Spring 的
@Profile
注解和特定于配置文件的配置存在限制。 - 如果 bean 被创建时属性会改变,那么这些属性将不受支持(例如,
@ConditionalOnProperty
和.enable
属性)。
当这些限制生效时,Spring 可以在构建期间执行提前处理,并生成 GraalVM 可以使用的额外资源。经过 Spring AOT 处理的应用程序通常会生成以下文件:
- Java 源代码
- 字节码(用于动态代理等)
- GraalVM JSON 提示文件:
资源提示(resource-config.json)
反射提示(reflect-config.json)
序列化提示(serialization-config.json)
Java 代理提示(proxy-config.json)
JNI 提示(jni-config.json)
源代码生成
Spring 应用程序由 Spring Bean 组成。在内部,Spring 框架使用两个不同的概念来管理 bean。有 bean 实例,它们是已经创建的实际实例,可以注入到其他 bean 中。还有 bean 定义,用于定义 bean 的属性和如何创建其实例。
如果我们考虑一个典型的 @Configuration
类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MyConfiguration {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
bean 定义是通过解析 @Configuration
类并找到 @Bean
方法来创建的。在上面的示例中,为名为 myBean
的单例 bean 定义了一个 BeanDefinition
。还为 MyConfiguration
类本身创建了一个 BeanDefinition
。
当需要 myBean
实例时,Spring 知道它必须调用 myBean()
方法并使用结果。当在 JVM 上运行时,@Configuration
类的解析发生在应用程序启动时,并使用反射调用 @Bean
方法。
在创建本地镜像时,Spring 的操作方式有所不同。它不是在运行时解析 @Configuration
类并生成 bean 定义,而是在构建时这样做。一旦发现了 bean 定义,它们就会被处理并转换为可以由 GraalVM 编译器分析的源代码。
Spring AOT 过程会将上面的配置类转换为类似以下的代码:
import org.springframework.beans.factory.aot.BeanInstanceSupplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
/**
* Bean definitions for {@link MyConfiguration}.
*/
public class MyConfiguration__BeanDefinitions {
/**
* Get the bean definition for 'myConfiguration'.
*/
public static BeanDefinition getMyConfigurationBeanDefinition() {
Class<?> beanType = MyConfiguration.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(MyConfiguration::new);
return beanDefinition;
}
/**
* Get the bean instance supplier for 'myBean'.
*/
private static BeanInstanceSupplier<MyBean> getMyBeanInstanceSupplier() {
return BeanInstanceSupplier.<MyBean>forFactoryMethod(MyConfiguration.class, "myBean")
.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(MyConfiguration.class).myBean());
}
/**
* Get the bean definition for 'myBean'.
*/
public static BeanDefinition getMyBeanBeanDefinition() {
Class<?> beanType = MyBean.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
beanDefinition.setInstanceSupplier(getMyBeanInstanceSupplier());
return beanDefinition;
}
}
注意:生成的确切代码可能会因你的 bean 定义的性质而有所不同。
从在上面看到,生成的代码以 GraalVM 可以理解的方式直接创建了与 @Configuration
类等价的 bean 定义。
有一个针对 myConfiguration
bean 的 bean 定义,以及一个针对 myBean
的 bean 定义。当需要 myBean
实例时,会调用一个 BeanInstanceSupplier
。这个supplier 将在 myConfiguration
bean 上调用 myBean()
方法。
注意:在 Spring AOT 处理过程中,应用程序将启动到 bean 定义可用的程度。在 AOT 处理阶段不会创建 bean 实例。
Spring AOT 会为你的所有 bean 定义生成类似这样的代码。当需要进行 bean post-processing时(例如,调用 @Autowired
方法),它也会生成代码。此外,还会生成一个ApplicationContextInitializer
,Spring Boot 在实际运行经过 AOT 处理的应用程序时将使用它来初始化 ApplicationContext
。
提示:尽管 AOT 生成的源代码可能很冗长,但它非常易于阅读,并且在调试应用程序时很有用。当使用 Maven 时,生成的源文件可以在 target/spring-aot/main/sources
中找到,而当使用 Gradle 时,可以在 build/generated/aotSources
中找到。
提示文件生成
除了生成源文件外,Spring AOT 引擎还会生成由 GraalVM 使用的提示文件。提示文件包含 JSON 数据,描述了 GraalVM 应该如何处理它无法通过直接检查代码来理解的内容。
例如,你可能会在一个私有方法上使用 Spring 注解。即使在 GraalVM 上,Spring 也需要使用反射来调用私有方法。当出现这种情况时,Spring 可以编写一个反射提示,以便 GraalVM 知道尽管私有方法没有被直接调用,但它仍然需要在本地镜像中可用。
提示文件会生成在 META-INF/native-image
目录下,并会被 GraalVM 自动拾取。
提示:当使用 Maven 时,生成的提示文件可以在 target/spring-aot/main/resources
中找到,当使用 Gradle 时,可以在 build/generated/aotResources
中找到。
代理类生成
Spring 有时需要生成代理类来为你编写的代码增强额外的功能。为此,它使用 cglib 库直接生成字节码。
当应用程序在 JVM 上运行时,代理类会随着应用程序的运行而动态生成。在创建本地镜像时,这些代理需要在构建时创建,以便它们可以由 GraalVM 包含。
注意:与源代码生成不同,生成的字节码在调试应用程序时不是特别有用。但是,如果你需要使用像 javap 这样的工具来检查 .class
文件的内容,你可以在 Maven 的 target/spring-aot/main/classes
和 Gradle 的 build/generated/aotClasses
中找到它们。
开发你的第一个 GraalVM 原生应用程序
现在已经对 GraalVM 原生镜像和 Spring 的提前编译引擎的工作原理有了全面的了解,接下来可以看看如何创建一个应用程序。
构建 Spring Boot 原生镜像应用程序主要有两种方式:
- 使用 Spring Boot 对 Cloud Native Buildpacks 的支持来生成包含原生可执行文件的轻量级容器。
- 使用 GraalVM 原生构建工具来生成原生可执行文件。
提示:开始一个新的原生 Spring Boot 项目的最简单方法是访问 start.spring.io,添加“GraalVM Native Support”依赖,并生成项目。包含的 HELP.md
文件将提供入门提示。
示例应用程序
需要一个示例应用程序来创建我们的原生镜像,看起来像这样:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class MyApplication {
@RequestMapping("/")
String home() {
return "Hello World!";
}
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
这个应用程序使用了 Spring MVC 和内嵌的 Tomcat,这两者都经过测试和验证,可以在 GraalVM 原生镜像中正常工作。
使用 Buildpacks 构建原生镜像
Spring Boot 直接为 Maven 和 Gradle 提供了原生镜像的 Buildpack 支持。这意味着你只需要输入一个命令,就可以快速地将一个合理的镜像放入你本地运行的 Docker 守护进程中。生成的镜像不包含 JVM,而是将原生镜像静态编译。这导致生成的镜像更小。
注意:用于构建镜像的构建器是 paketobuildpacks/builder-jammy-tiny:latest
。它的占用空间小,攻击面也较小,但如果你需要,你也可以使用 paketobuildpacks/builder-jammy-base:latest
或 paketobuildpacks/builder-jammy-full:latest
来使镜像中包含更多的工具。
系统要求
应该安装 Docker。如果你在 Linux 上,请配置它以允许非 root 用户使用。
注意:你可以运行 docker run hello-world
(不使用 sudo
)来检查 Docker 守护进程是否如预期那样可达。
提示:在 macOS 上,建议将分配给 Docker 的内存至少增加到 8GB,并可能还需要添加更多的 CPU。在 Microsoft Windows 上,请确保启用 Docker WSL 2 后端以获得更好的性能。
使用 Maven
要使用 Maven 构建原生镜像容器,请确保你的 pom.xml
文件使用 spring-boot-starter-parent
和 org.graalvm.buildtools:native-maven-plugin
。应该有一个像这样的 <parent>
部分:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
此外,还应该在 <build> <plugins>
部分中添加以下内容:
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
spring-boot-starter-parent
声明了一个native
profile ,用于配置创建原生镜像需要运行的执行项。可以使用命令行上的 -P
标志激活profile。
如果如不想使用 spring-boot-starter-parent
,将需要为 Spring Boot 插件的 process-aot
目标和 Native Build Tools 插件的 add-reachability-metadata
目标配置execution。
要构建镜像,可以运行带有激活的native
profile的 spring-boot:build-image
目标:
$ mvn -Pnative spring-boot:build-image
使用Gradle
当应用 GraalVM 原生镜像插件时,Spring Boot Gradle 插件会自动配置 AOT 任务。应该检查你的 Gradle build 是否包含一个包含 org.graalvm.buildtools.native
的plugins
块。
只要应用了 org.graalvm.buildtools.native
插件,bootBuildImage
任务就会生成一个原生镜像,而不是 JVM 镜像。可以使用以下命令运行该任务:
$ gradle bootBuildImage
运行示例
一旦你运行了适当的构建命令,就应该可以使用 Docker 镜像了。可以使用 docker run
启动你的应用程序:
$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT
你应该看到类似于以下的输出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 0.08 seconds (process running for 0.095)
注意:启动时间因机器而异,但应该比运行在JVM上的Spring Boot应用程序快得多。
如果你打开一个网页浏览器到localhost:8080
,你应该看到以下的输出:
Hello World!
要优雅地退出应用程序,请按ctrl-c
。
使用原生构建工具构建原生镜像
如果你想要直接生成一个原生可执行文件而不使用Docker,你可以使用GraalVM的原生构建工具。原生构建工具是GraalVM为Maven和Gradle提供的插件。你可以使用它们执行各种GraalVM任务,包括生成原生镜像。
先决条件
要使用原生构建工具构建原生镜像,你的机器上需要安装GraalVM分发版。你可以在Liberica Native Image Kit页面手动下载,或者使用SDKMAN!等下载管理器。
Linux 和 macOS
要在 macOS 或 Linux 上安装原生镜像编译器,推荐使用 SDKMAN!。你可以从 sdkman.io 获取 SDKMAN!,并使用以下命令安装 Liberica GraalVM 分发版:
$ sdk install java 22.3.r17-nik
$ sdk use java 22.3.r17-nik
通过检查 java -version
的输出来验证是否正确配置了版本:
$ java -version
openjdk version "17.0.5" 2022-10-18 LTS
OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode)
使用Maven
与构建包支持一样,你需要确保你正在使用spring-boot-starter-parent
以继承native
profile,并且正在使用org.graalvm.buildtools:native-maven-plugin
插件。
在激活native
profile后,你可以调用native:compile
目标来触发native-image
编译:
$ mvn -Pnative native:compile
本地镜像可执行文件可以在target
目录中找到。
使用Gradle
当将Native Build Tools Gradle插件应用到你的项目时,Spring Boot Gradle插件将自动触发Spring AOT引擎。任务依赖关系会自动配置,因此你只需运行标准的nativeCompile
任务即可生成本地镜像:
$ gradle nativeCompile
本地镜像可执行文件可以在build/native/nativeCompile
目录中找到。
运行示例
此时,你的应用程序应该可以工作了。现在可以通过直接运行它来启动应用程序:
$ target/myproject
你应该看到类似以下的输出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 0.08 seconds (process running for 0.095)
启动时间因机器而异,但它应该比在JVM上运行的Spring Boot应用程序快得多。
如果你在浏览器中打开localhost:8080
,应该看到以下输出:
Hello World!
要优雅地退出应用程序,请按ctrl-c
。
测试GraalVM本地镜像
在编写本地镜像应用程序时,建议您尽可能继续使用JVM来开发大多数单元和集成测试。这将有助于减少开发人员的构建时间,并允许使用现有的IDE集成。在JVM上具有广泛的测试覆盖率后,可以将本地镜像测试的重点放在可能有所不同的领域。
对于本地镜像测试,通常需要确保以下方面正常工作:
- Spring AOT引擎能够处理你的应用程序,并且它将以AOT处理模式运行。
- GraalVM有足够的提示来确保可以生成有效的本地镜像。
使用JVM测试提前处理(Testing Ahead-of-time Processing With the JVM)
当Spring Boot应用程序运行时,它会尝试检测是否作为本地镜像运行。如果它作为本地镜像运行,它将使用Spring AOT引擎在构建时生成的代码来初始化应用程序。
如果应用程序在常规JVM上运行,则会忽略任何AOT生成的代码。
由于本地镜像编译阶段可能需要一段时间才能完成,有时在JVM上运行应用程序但使用AOT生成的初始化代码会很有用。这样做有助于快速验证AOT生成的代码中没有错误,并且在你的应用程序最终转换为本地镜像时不会遗漏任何东西。
要在JVM上运行Spring Boot应用程序并使用AOT生成的代码,可以将spring.aot.enabled
系统属性设置为true
。
例如:
$ java -Dspring.aot.enabled=true -jar myapplication.jar
注意:需要确保你正在测试的jar包含AOT生成的代码。对于Maven,这意味着应该使用-Pnative
构建以激活native
profile。对于Gradle,需要确保你的构建包含org.graalvm.buildtools.native
插件。
如果你的应用程序在spring.aot.enabled
属性设置为true
的情况下启动,那么你将更有信心它在转换为本地镜像时能够正常工作。
还可以考虑针对正在运行的应用程序运行集成测试。例如,可以使用Spring WebClient
调用应用程序的REST端点。或者,可以考虑使用像Selenium这样的项目来检查应用程序的HTML响应。
使用Native Build Tools测试
GraalVM Native Build Tools包括在本地镜像内运行测试的能力。当想要深入测试应用程序内部在GraalVM本地镜像中的工作情况时,这会很有帮助。
生成包含要运行的测试的本地镜像可能是一个耗时的操作,因此大多数开发人员可能更愿意在本地使用JVM。然而,它们作为CI管道的一部分可能非常有用。例如,你可以选择每天运行一次本地测试。
Spring框架包括提前(ahead-of-time)支持运行测试。所有常见的Spring测试功能都适用于本地镜像测试。例如,可以继续使用@SpringBootTest
注解。还可以使用Spring Boot测试切片来仅测试应用程序的特定部分。
Spring框架的native 测试支持以以下方式工作:
- 分析测试以发现所需的任何
ApplicationContext
实例。 - 对这些应用程序上下文中的每一个应用提前处理,并生成资源。
- 创建一个本地镜像,其中生成的资源由GraalVM处理。
- 本地镜像还包括配置了已发现测试列表的JUnit
TestEngine
。 - 启动本地镜像,触发引擎运行每个测试并报告结果。
使用Maven
要使用Maven运行本地测试,请确保pom.xml
文件使用了spring-boot-starter-parent
。应该有一个如下所示的部分:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
spring-boot-starter-parent
声明了一个nativeTest
profile ,它配置了运行本地测试所需的执行任务。可以在命令行上使用-P
标志来激活这些profiles。
提示:如果不想使用spring-boot-starter-parent
,需要为Spring Boot插件的process-test-aot
目标和Native Build Tools插件的test
目标配置执行任务。
要构建镜像并运行测试,请在激活nativeTest
profile 的情况下使用test
目标:
$ mvn -PnativeTest test
使用Gradle
当应用GraalVM Native Image插件时,Spring Boot Gradle插件会自动配置AOT测试任务。应该检查你的Gradle build 是否包含一个含有org.graalvm.buildtools.native
的plugins
块。
要使用Gradle运行本地测试,可以使用nativeTest
任务:
$ gradle nativeTest
高级本地镜像主题
嵌套配置属性
Spring提前(ahead-of-time )编译引擎会自动为配置属性创建反射提示。然而,非内部类的嵌套配置属性必须用@NestedConfigurationProperty
注解,否则它们将无法被检测到,也将无法绑定。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties(prefix = "my.properties")
public class MyProperties {
private String name;
@NestedConfigurationProperty
private final Nested nested = new Nested();
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Nested getNested() {
return this.nested;
}
}
Nested
类如下:
public class Nested {
private int number;
public int getNumber() {
return this.number;
}
public void setNumber(int number) {
this.number = number;
}
}
上面的示例为my.properties.name
和my.properties.nested.number
生成配置属性。如果没有在嵌套字段上使用@NestedConfigurationProperty
注解,那么在本地镜像中my.properties.nested.number
属性将无法绑定。
当使用构造函数绑定时,必须用@NestedConfigurationProperty
注解字段:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties(prefix = "my.properties")
public class MyPropertiesCtor {
private final String name;
@NestedConfigurationProperty
private final Nested nested;
public MyPropertiesCtor(String name, Nested nested) {
this.name = name;
this.nested = nested;
}
public String getName() {
return this.name;
}
public Nested getNested() {
return this.nested;
}
}
当使用记录时,必须用@NestedConfigurationProperty
注解参数:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties(prefix = "my.properties")
public record MyPropertiesRecord(String name, @NestedConfigurationProperty Nested nested) {
}
当使用Kotlin时,需要用@NestedConfigurationProperty
注解数据类的参数:
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.NestedConfigurationProperty
@ConfigurationProperties(prefix = "my.properties")
data class MyPropertiesKotlin(
val name: String,
@NestedConfigurationProperty val nested: Nested
)
注意:请在所有情况下使用公共的getter
和setter
,否则属性将无法绑定。
转换Spring Boot可执行Jar
只要Jar包含AOT生成的资产,就有可能将Spring Boot可执行jar转换为本地镜像。这可能出于多种原因而有用,包括:
- 可以保持常规的JVM流程,并在CI/CD平台上将JVM应用程序转换为本地镜像。
- 由于native-image不支持交叉编译,你可以保留一个与操作系统无关的部署工件,稍后再将其转换为不同的操作系统架构。
可以使用Cloud Native Buildpacks或将GraalVM附带的native-image
工具将Spring Boot可执行jar转换为本地镜像。
注意:可执行jar必须包含AOT生成的资产,如生成的类和JSON提示文件。
使用Buildpacks
Spring Boot应用程序通常通过Maven(mvn spring-boot:build-image
)或Gradle(gradle bootBuildImage
)集成使用Cloud Native Buildpacks。然而,也可以使用pack
将AOT处理过的Spring Boot可执行jar转换为本地容器镜像。
首先,确保有可用的Docker守护进程。如果使用的是Linux,请配置它以允许非root用户。
还需要按照buildpacks.io上的安装指南安装pack
。
假设target
目录中有一个AOT处理过的Spring Boot可执行jar,名为myproject-0.0.1-SNAPSHOT.jar
,运行:
$ pack build --builder paketobuildpacks/builder-jammy-tiny \
--path target/myproject-0.0.1-SNAPSHOT.jar \
--env 'BP_NATIVE_IMAGE=true' \
my-application:0.0.1-SNAPSHOT
注意:不需要在本地安装GraalVM即可通过这种方式生成镜像。
一旦pack
完成,可以使用docker run
启动应用程序:
$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT
使用GraalVM native-image
将AOT处理过的Spring Boot可执行jar转换为本地可执行文件的另一个选项是使用GraalVM的native-image
工具。为此,需要在机器上安装GraalVM版本。可以在Liberica Native Image Kit页面手动下载,或者使用像SDKMAN!这样的下载管理器。
假设target
目录中有一个AOT处理过的Spring Boot可执行jar,名为myproject-0.0.1-SNAPSHOT.jar
,运行:
$ rm -rf target/native
$ mkdir -p target/native
$ cd target/native
$ jar -xvf ../myproject-0.0.1-SNAPSHOT.jar
$ native-image -H:Name=myproject @META-INF/native-image/argfile -cp .:BOOT-INF/classes:`find BOOT-INF/lib | tr '\n' ':'`
$ mv myproject ../
注意:这些命令适用于Linux或macOS机器,但你需要对Windows进行适配。
提示:@META-INF/native-image/argfile
可能没有打包在你的jar中。只有在需要可达性元数据覆盖时才会包含它。
注意:native-image -cp
标志不接受通配符。需要确保列出所有jar(上述命令使用find
和tr
来完成这一点)。
使用跟踪代理
GraalVM本地镜像跟踪代理允许在JVM上拦截反射、资源或代理的使用,以生成相关的提示。Spring应该自动生成这些提示的大部分,但跟踪代理可用于快速识别缺失的条目。
当使用代理为本地镜像生成提示时,有几种方法:
- 直接启动应用程序并运行它。
- 运行应用程序测试以运行应用程序。
第一种选项在Spring无法识别库或模式时,用于识别缺失的提示是有趣的。
第二种选项听起来对可重复设置更有吸引力,但默认情况下,生成的提示将包括测试基础设施所需的任何内容。当应用程序真正运行时,其中一些将是不必要的。为了解决这个问题,代理支持一个访问过滤器文件,这将导致某些数据从生成的输出中排除。
直接启动应用程序
使用以下命令启动应用程序,并附加本地镜像跟踪代理:
$ java -Dspring.aot.enabled=true \
-agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ \
-jar target/myproject-0.0.1-SNAPSHOT.jar
现在你可以运行您想要生成提示的代码路径,然后使用ctrl-c
停止应用程序。
应用程序关闭时,本地镜像跟踪代理将把提示文件写入给定的配置输出目录。你可以手动检查这些文件,或者将它们作为输入使用到本地镜像构建过程中。要将它们作为输入,请将它们复制到src/main/resources/META-INF/native-image/
目录中。下次构建本地镜像时,GraalVM将考虑这些文件。
本地镜像跟踪代理上可以设置更多高级选项,例如按调用者类过滤记录的提示等。
自定义提示
如果需要为反射、资源、序列化、代理使用等提供自己的提示,可以使用RuntimeHintsRegistrar
API。创建一个实现RuntimeHintsRegistrar
接口的类,然后对提供的RuntimeHints
实例进行适当的调用:
import java.lang.reflect.Method;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.util.ReflectionUtils;
public class MyRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register method for reflection
Method method = ReflectionUtils.findMethod(MyClass.class, "sayHello", String.class);
hints.reflection().registerMethod(method, ExecutableMode.INVOKE);
// Register resources
hints.resources().registerPattern("my-resource.txt");
// Register serialization
hints.serialization().registerType(MySerializableClass.class);
// Register proxy
hints.proxies().registerJdkProxy(MyInterface.class);
}
}
然后,可以在任何@Configuration
类(例如@SpringBootApplication
注解的应用程序类)上使用@ImportRuntimeHints
来激活这些提示。
如果需要绑定的类(在序列化或反序列化JSON时最常需要),可以在任何bean上使用@RegisterReflectionForBinding
。大多数提示都是自动推断的,例如在接受或返回@RestController方
法的数据时。但是,当直接使用WebClient
、RestClient
或RestTemplate
时,可能需要使用@RegisterReflectionForBinding
。
测试自定义提示
可以使用RuntimeHintsPredicates
API来测试你的提示。该API提供了构建Predicate
的方法,可以用于测试RuntimeHints
实例。
如果你使用AssertJ,测试将如下所示:
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.docs.nativeimage.advanced.customhints.MyRuntimeHints;
import static org.assertj.core.api.Assertions.assertThat;
class MyRuntimeHintsTests {
@Test
void shouldRegisterHints() {
RuntimeHints hints = new RuntimeHints();
new MyRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.resource().forResource("my-resource.txt")).accepts(hints);
}
}
已知限制
GraalVM本地镜像是一项不断发展的技术,并非所有库都提供支持。GraalVM社区正在通过为尚未自行提供reachability metadata 的项目提供帮助。Spring本身不包含第三方库的提示,而是依赖于可达性元数据项目。