GraalVM本地镜像是通过提前处理编译的Java应用程序生成的独立可执行文件。本地镜像通常比它们的JVM对应物具有更小的内存占用和更快的启动速度。

1. GraalVM本地镜像介绍

GraalVM本地映像提供了一种新的部署和运行Java应用程序的方式。与Java虚拟机相比,本地映像可以以更小的内存占用和更快的启动时间运行。

它们非常适合使用容器映像部署的应用程序,尤其在与“函数即服务”(FaaS)平台结合时尤为有趣。

与为JVM编写的传统应用程序不同,GraalVM本地映像应用程序需要提前处理才能创建可执行文件。这种提前处理涉及从应用程序的主入口点静态分析您的应用程序代码。

GraalVM本地映像是一个完整的、特定于平台的可执行文件。您无需在运行本地映像时提供Java虚拟机。

如果您只是想开始并尝试使用GraalVM,可以直接跳转到“开发您的第一个GraalVM本地应用程序”部分,稍后再回到本部分。

1.1. 与JVM部署的主要区别

GraalVM本地映像是提前生成的,这意味着本地应用程序与基于JVM的应用程序之间存在一些关键区别。主要区别包括:

  • 在构建时从main入口点执行应用程序的静态分析。

  • 在创建本地映像时无法到达的代码将被删除,并不会成为可执行文件的一部分。

  • GraalVM不直接了解代码的动态元素,必须告知有关反射、资源、序列化和动态代理。

  • 应用程序类路径在构建时固定,无法更改。

  • 没有延迟类加载,可执行文件中的所有内容都将在启动时加载到内存中。

  • 对于一些Java应用程序的某些方面存在一些限制,不完全支持。

除了这些区别外,Spring使用一种称为Spring提前处理的过程,这会带来进一步的限制。请确保至少阅读下一节的开头部分以了解这些限制。

GraalVM参考文档的本地映像兼容性指南部分提供了有关GraalVM限制的更多详细信息。

1.2. 理解Spring的预先处理

典型的Spring Boot应用程序非常动态,配置是在运行时执行的。事实上,Spring Boot自动配置的概念严重依赖于对运行时状态的反应,以便正确配置事物。

虽然可以告诉GraalVM关于应用程序的这些动态方面,但这样做会使大部分静态分析的好处付诸东流。因此,当使用Spring Boot创建本机映像时,假定为封闭世界,并限制应用程序的动态方面。

封闭世界的假设意味着,除了GraalVM本身创建的限制之外,还有以下限制:

  • 应用程序中定义的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

1.2.1. 源代码生成

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();
    }

}

通过解析@Configuration类并查找@Bean方法来创建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;

/**
 * {@link MyConfiguration}的bean定义。
 */
public class MyConfiguration__BeanDefinitions {

    /**
     * 获取'myConfiguration'的bean定义。
     */
    public static BeanDefinition getMyConfigurationBeanDefinition() {
        Class<?> beanType = MyConfiguration.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(MyConfiguration::new);
        return beanDefinition;
    }

    /**
     * 获取'myBean'的bean实例供应商。
     */
    private static BeanInstanceSupplier<MyBean> getMyBeanInstanceSupplier() {
        return BeanInstanceSupplier.<MyBean>forFactoryMethod(MyConfiguration.class, "myBean")
            .withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(MyConfiguration.class).myBean());
    }

    /**
     * 获取'myBean'的bean定义。
     */
    public static BeanDefinition getMyBeanBeanDefinition() {
        Class<?> beanType = MyBean.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(getMyBeanInstanceSupplier());
        return beanDefinition;
    }

}
生成的确切代码可能会因您的bean定义的性质而有所不同。

您可以看到上面生成的代码创建了等效的bean定义,与@Configuration类相同,但以GraalVM可以理解的直接方式。

有一个myConfiguration bean的bean定义,还有一个myBean。当需要myBean实例时,将调用BeanInstanceSupplier。此供应商将在myConfiguration bean上调用myBean()方法。

在Spring AOT处理期间,应用程序启动到可用bean定义的阶段。在AOT处理阶段不会创建bean实例。

Spring AOT将为所有bean定义生成类似上述代码。当需要进行bean后处理时(例如,调用@Autowired方法),还将生成代码。将生成一个ApplicationContextInitializer,Spring Boot将用它来初始化ApplicationContext,当实际运行经过AOT处理的应用程序时。

虽然AOT生成的源代码可能会很冗长,但在调试应用程序时非常易读,有助于调试。使用Maven时,生成的源文件可以在target/spring-aot/main/sources中找到;使用Gradle时,可以在build/generated/aotSources中找到。

1.2.2. 提示文件生成

除了生成源文件外,Spring AOT引擎还将生成供GraalVM使用的提示文件。提示文件包含JSON数据,描述GraalVM应如何处理无法通过直接检查代码理解的内容。

例如,您可能在私有方法上使用Spring注解。Spring需要使用反射来调用私有方法,即使在GraalVM上也是如此。当出现这种情况时,Spring可以编写反射提示,以便GraalVM知道即使私有方法不会直接调用,它仍然需要在本机映像中可用。

提示文件生成在META-INF/native-image下,GraalVM会自动检测到这些文件。

使用Maven时,生成的提示文件可以在target/spring-aot/main/resources中找到;使用Gradle时,可以在build/generated/aotResources中找到。

1.2.3. 代理类生成

Spring有时需要生成代理类以增强您编写的代码。为此,它使用直接生成字节码的cglib库。

在JVM上运行应用程序时,代理类会动态生成。在创建本机映像时,这些代理需要在构建时创建,以便它们可以被GraalVM包含。

与源代码生成不同,生成的字节码在调试应用程序时并不特别有用。但是,如果需要使用诸如javap之类的工具检查.class文件的内容,可以在Maven的target/spring-aot/main/classes中找到它们;在Gradle的build/generated/aotClasses中找到。

2. 开发您的第一个GraalVM本地应用程序

现在我们已经对GraalVM原生映像和Spring提前引擎的工作原理有了很好的概述,我们可以看看如何创建一个应用程序。

构建Spring Boot原生映像应用程序有两种主要方式:

  • 使用Spring Boot对Cloud Native Buildpacks的支持来生成一个包含本机可执行文件的轻量级容器。

  • 使用GraalVM原生构建工具生成本机可执行文件。

开始一个新的原生Spring Boot项目的最简单方法是访问 start.spring.io,添加“GraalVM原生支持”依赖项并生成项目。包含的HELP.md文件将提供入门提示。

2.1. 示例应用程序

我们需要一个示例应用程序,以便用来创建我们的原生映像。对于我们的目的,简单的“Hello World!” Web应用程序在“getting-started.html”部分中介绍的内容就足够了。

简而言之,我们的主要应用程序代码如下:

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原生映像一起使用。

2.2. 使用构建包构建原生映像

Spring Boot直接为Maven和Gradle提供了原生映像的构建包支持。这意味着您只需输入一个命令,就可以快速将一个合理的映像放入本地运行的Docker守护程序中。生成的映像不包含JVM,而是静态编译的本机映像,这导致映像更小。

用于映像的构建器是paketobuildpacks/builder-jammy-tiny:latest。它的占用空间小,攻击面减小,但您也可以使用paketobuildpacks/builder-jammy-base:latestpaketobuildpacks/builder-jammy-full:latest来在映像中添加更多工具(如果需要)。

2.2.1. 系统要求

应安装Docker。有关更多详细信息,请参阅获取Docker。如果您使用Linux,请配置它以允许非root用户

您可以运行docker run hello-world(无需sudo)来检查Docker守护程序是否按预期可达。查看MavenGradle Spring Boot插件文档以获取更多详细信息。
在macOS上,建议将分配给Docker的内存增加至至少8GB,并可能添加更多CPU。有关更多详细信息,请参阅此Stack Overflow答案。在Microsoft Windows上,请确保启用Docker WSL 2后端以获得更好的性能。

2.2.2. 使用Maven

要使用Maven构建本机映像容器,您应确保您的pom.xml文件使用spring-boot-starter-parentorg.graalvm.buildtools:native-maven-plugin。您的<parent>部分应如下所示:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.3</version>
</parent>

此外,您还应在<build> <plugins>部分中包含以下内容:

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
</plugin>

spring-boot-starter-parent声明了一个native配置文件,配置了需要运行以创建本机映像的执行。您可以使用命令行上的-P标志来激活配置文件。

如果您不想使用spring-boot-starter-parent,则需要为Spring Boot插件的process-aot目标和原生构建工具插件的add-reachability-metadata目标配置执行。
native配置文件的 spring-boot:build-image目标:

$ mvn -Pnative spring-boot:build-image

2.2.3. 使用Gradle

当应用GraalVM原生映像插件时,Spring Boot Gradle插件会自动配置AOT任务。您应检查您的Gradle构建是否包含一个包含org.graalvm.buildtools.nativeplugins块。

org.graalvm.buildtools.native插件, bootBuildImage任务将生成一个本机映像而不是JVM映像。您可以使用以下命令运行该任务:

$ gradle bootBuildImage

2.2.4. 运行示例

docker run启动应用程序:

$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v3.2.3)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 0.08 seconds (process running for 0.095)
启动时间因机器而异,但应比在JVM上运行的Spring Boot应用程序快得多。
localhost:8080,您应该看到以下输出:

Hello World!
ctrl-c

2.3. 使用本地构建工具构建本地镜像

如果您想直接生成本地可执行文件而不使用Docker,您可以使用GraalVM本地构建工具。本地构建工具是由GraalVM为Maven和Gradle提供的插件。您可以使用它们执行各种GraalVM任务,包括生成本地镜像。

2.3.1. 先决条件

要使用本地构建工具构建本地镜像,您需要在您的计算机上安装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)
Windows

在Windows上,按照这些说明安装GraalVM或Liberica Native Image Kit版本为22.3,Visual Studio Build Tools和Windows SDK。由于与Windows相关的命令行最大长度,请确保使用x64本机工具命令提示符而不是常规Windows命令行来运行Maven或Gradle插件。

2.3.2. 使用Maven

构建包支持一样,您需要确保在项目中使用spring-boot-starter-parent以继承native配置文件,并且使用org.graalvm.buildtools:native-maven-plugin插件。

激活native配置文件后,您可以调用native:compile目标来触发native-image编译:

$ mvn -Pnative native:compile

本地镜像可执行文件可以在target目录中找到。

2.3.3. 使用Gradle

当将本地构建工具Gradle插件应用于您的项目时,Spring Boot Gradle插件将自动触发Spring AOT引擎。任务依赖关系会自动配置,因此您只需运行标准的nativeCompile任务来生成本地镜像:

$ gradle nativeCompile

本地镜像可执行文件可以在build/native/nativeCompile目录中找到。

2.3.4. 运行示例

此时,您的应用程序应该可以正常工作。您现在可以直接运行应用程序:

Maven
$ target/myproject
Gradle
$ build/native/nativeCompile/myproject

您应该会看到类似以下的输出:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v3.2.3)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 0.08 seconds (process running for 0.095)
启动时间因计算机而异,但应比在JVM上运行的Spring Boot应用程序快得多。

如果您在Web浏览器中打开localhost:8080,您应该会看到以下输出:

Hello World!

要优雅地退出应用程序,请按ctrl-c

3. 测试 GraalVM 本地镜像

在编写本地镜像应用程序时,我们建议尽可能继续使用 JVM 来开发大部分单元测试和集成测试。这将有助于减少开发人员构建时间,并允许您使用现有的 IDE 集成。通过在 JVM 上进行广泛的测试覆盖,然后可以将本地镜像测试重点放在可能有所不同的领域。

对于本地镜像测试,通常需要确保以下方面正常工作:

  • Spring AOT 引擎能够处理您的应用程序,并且将在 AOT 处理模式下运行。

  • GraalVM 具有足够的提示,以确保可以生成有效的本地镜像。

3.1. 使用 JVM 进行预处理测试

当一个 Spring Boot 应用程序运行时,它会尝试检测是否作为本地镜像运行。如果作为本地镜像运行,它将使用在构建时由 Spring AOT 引擎生成的代码初始化应用程序。

如果应用程序在常规 JVM 上运行,则会忽略任何 AOT 生成的代码。

由于 native-image 编译阶段可能需要一段时间才能完成,有时在 JVM 上运行应用程序但使用 AOT 生成的初始化代码是很有用的。这样做有助于您快速验证 AOT 生成的代码中没有错误,并且在最终将应用程序转换为本地镜像时没有遗漏。

要在 JVM 上运行 Spring Boot 应用程序并使用 AOT 生成的代码,可以将 spring.aot.enabled 系统属性设置为 true

例如:

$ java -Dspring.aot.enabled=true -jar myapplication.jar
您需要确保要测试的 jar 包含 AOT 生成的代码。对于 Maven,这意味着您应该使用 -Pnative 进行构建以激活 native 配置文件。对于 Gradle,您需要确保您的构建包含 org.graalvm.buildtools.native 插件。

如果您的应用程序以 spring.aot.enabled 属性设置为 true 启动,则可以更有信心地确保在转换为本地镜像时它能正常工作。

您还可以考虑针对正在运行的应用程序运行集成测试。例如,您可以使用 Spring 的 WebClient 调用应用程序的 REST 端点。或者您可以考虑使用类似 Selenium 这样的项目来检查应用程序的 HTML 响应。

3.2. 使用本地构建工具进行测试

GraalVM 本地构建工具包括在本地镜像内部运行测试的能力。当您希望深度测试应用程序内部在 GraalVM 本地镜像中工作时,这将非常有帮助。

生成包含要运行的测试的本地镜像可能是一个耗时的操作,因此大多数开发人员可能更喜欢在本地使用 JVM。但是,作为 CI 流水线的一部分,它们可能非常有用。例如,您可以选择每天运行一次本地测试。

Spring Framework 包括对运行测试的预处理支持。所有常见的 Spring 测试功能都适用于本地镜像测试。例如,您可以继续使用 @SpringBootTest 注解。您还可以使用 Spring Boot 的 测试切片 来测试应用程序的特定部分。

Spring Framework 的本地测试支持工作方式如下:

  • 分析测试以发现将需要的任何 ApplicationContext 实例。

  • 对这些应用程序上下文应用预处理,并生成资产。

  • 创建一个本地镜像,其中包含由 GraalVM 处理的生成的资产。

  • 本地镜像还包括配置了一组发现的测试的 JUnit TestEngine

  • 启动本地镜像,触发引擎运行每个测试并报告结果。

3.2.1. 使用 Maven

要使用 Maven 运行本地测试,请确保您的 pom.xml 文件使用 spring-boot-starter-parent。您应该有一个看起来像这样的 <parent> 部分:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.3</version>
</parent>

spring-boot-starter-parent 声明了一个配置了运行本地测试所需执行的 nativeTest 配置文件。您可以使用命令行上的 -P 标志来激活配置文件。

如果您不想使用 spring-boot-starter-parent,则需要为 Spring Boot 插件的 process-test-aot 目标和本地构建工具插件的 test 目标配置执行。

要构建镜像并运行测试,请使用激活 nativeTest 配置文件的 test 目标:

$ mvn -PnativeTest test

3.2.2. 使用 Gradle

当应用 GraalVM 本地镜像插件时,Spring Boot Gradle 插件会自动配置 AOT 测试任务。您应该检查您的 Gradle 构建是否包含一个包含 org.graalvm.buildtools.nativeplugins 块。

要使用 Gradle 运行本地测试,可以使用 nativeTest 任务:

$ gradle nativeTest

4. 高级本地镜像主题

4.1. 嵌套配置属性

Reflection hints are automatically created for configuration properties by the Spring ahead-of-time engine. Nested configuration properties which are not inner classes, however, 必须使用@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();

    // getters / setters...

    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;

    // getters / setters...

    public int getNumber() {
        return this.number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

}

上面的示例为my.properties.namemy.properties.nested.number生成配置属性。如果在nested字段上没有@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;
    }

    // getters / setters...

    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,否则属性将无法绑定。

4.2. 转换Spring Boot可执行Jar

只要Jar包含AOT生成的资源,就可以将Spring Boot可执行jar转换为本机镜像。这可能有多种用途,包括:

  • 您可以保留常规的JVM流水线,并在CI/CD平台上将JVM应用程序转换为本机镜像。

  • 由于native-image不支持交叉编译,您可以保留一个OS中立的部署工件,稍后将其转换为不同的OS架构。

您可以使用Cloud Native Buildpacks或使用GraalVM附带的native-image工具将Spring Boot可执行jar转换为本机镜像。

您的可执行jar必须包含AOT生成的资源,如生成的类和JSON提示文件。

4.2.1. 使用Buildpacks

Spring Boot应用程序通常通过Maven(mvn spring-boot:build-image)或Gradle(gradle bootBuildImage)集成使用Cloud Native Buildpacks。但是,您也可以使用pack将经过AOT处理的Spring Boot可执行jar转换为本机容器镜像。

首先确保Docker守护程序可用(有关更多详细信息,请参见获取Docker)。如果您在Linux上,请配置以允许非root用户

您还需要按照buildpacks.io上的安装指南安装pack

假设一个经过AOT处理的Spring Boot可执行jar构建为myproject-0.0.1-SNAPSHOT.jartarget目录中,运行:

$ 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

4.2.2. 使用GraalVM native-image

将经过AOT处理的Spring Boot可执行jar转换为本机可执行文件的另一种选项是使用GraalVM的native-image工具。为了使其工作,您需要在您的计算机上安装GraalVM发行版。您可以在Liberica Native Image Kit页面手动下载,或者您可以使用SDKMAN!等下载管理器。

假设一个经过AOT处理的Spring Boot可执行jar构建为myproject-0.0.1-SNAPSHOT.jartarget目录中,运行:

$ 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进行调整。
可能不会在您的jar中打包@META-INF/native-image/argfile。只有在需要覆盖可达性元数据时才会包含它。
native-image-cp标志不接受通配符。您需要确保列出所有jar文件(上面的命令使用findtr来执行此操作)。

4.3. 使用跟踪代理

GraalVM本机镜像跟踪代理允许您拦截JVM上的反射、资源或代理使用,以生成相关提示。Spring应该会自动生成大部分这些提示,但跟踪代理可用于快速识别缺失的条目。

在使用代理为本机镜像生成提示时,有几种方法:

  • 直接启动应用程序并对其进行操作。

  • 运行应用程序测试以对应用程序进行操作。

第一种选项对于识别当Spring无法识别库或模式时缺失的提示很有用。

第二种选项对于可重复设置更具吸引力,但默认情况下生成的提示将包括测试基础设施所需的任何内容。当应用程序真正运行时,其中一些将是不必要的。为解决这个问题,该代理支持一个访问过滤器文件,将导致某些数据被排除在生成的输出之外。

4.3.1. 直接启动应用程序

使用以下命令启动附加了本机镜像跟踪代理的应用程序:

$ 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将考虑这些文件。

本机镜像跟踪代理还支持更多高级选项,例如通过调用类过滤记录的提示等。有关更多信息,请参阅官方文档

4.4. 自定义提示

如果您需要为反射、资源、序列化、代理使用等提供自己的提示,您可以使用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) {
        // 为反射注册方法
        Method method = ReflectionUtils.findMethod(MyClass.class, "sayHello", String.class);
        hints.reflection().registerMethod(method, ExecutableMode.INVOKE);

        // 注册资源
        hints.resources().registerPattern("my-resource.txt");

        // 注册序列化
        hints.serialization().registerType(MySerializableClass.class);

        // 注册代理
        hints.proxies().registerJdkProxy(MyInterface.class);
    }

}

然后,您可以在任何@Configuration类(例如您的用@SpringBootApplication注解的应用程序类)上使用@ImportRuntimeHints来激活这些提示。

如果您有需要绑定的类(在大多数情况下在序列化或反序列化JSON时需要),您可以在任何bean上使用@RegisterReflectionForBinding。大多数提示都会自动推断,例如从@RestController方法接受或返回数据时。但是,当您直接使用WebClientRestClientRestTemplate时,您可能需要使用@RegisterReflectionForBinding

4.4.1. 测试自定义提示

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);
    }

}

4.5. 已知限制

GraalVM本机镜像是一项不断发展的技术,并非所有库都提供支持。GraalVM社区通过为尚未提供自己支持的项目提供可达性元数据来提供帮助。Spring本身不包含第三方库的提示,而是依赖于可达性元数据项目。

如果在为Spring Boot应用程序生成本机镜像时遇到问题,请查看Spring Boot维基的Spring Boot与GraalVM页面。您还可以向GitHub上的spring-aot-smoke-tests项目贡献问题,该项目用于确认常见应用程序类型是否按预期工作。

如果您发现某个库与GraalVM不兼容,请在可达性元数据项目上提出问题。

5. 接下来阅读什么

如果您想了解我们的构建插件提供的预先处理更多信息,请参阅MavenGradle插件文档。要了解更多有关执行处理的API,请浏览Spring Framework源代码中的org.springframework.aot.generateorg.springframework.beans.factory.aot包。

有关Spring和GraalVM的已知限制,请参阅Spring Boot wiki

下一节将介绍Spring Boot CLI