本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

GraalVM Native Image 是独立的可执行文件,可以通过提前处理编译好的Java应用程序来生成。一般来说,Native Image的内存占用较小,启动速度也比JVM的同类产品快。

1. GraalVM 原生镜像的介绍

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

它们非常适用于使用容器镜像部署的应用程序,当与 "功能即服务"(FaaS)平台结合时,尤其令人感兴趣。

与为JVM编写的传统应用程序不同,GraalVM Native Image 应用程序需要提前处理,以创建可执行文件。这种超前处理包括从主入口点静态地分析你的应用程序代码。

GraalVM本地镜像是一个完整的、针对平台的可执行文件。你不需要为了运行一个本地镜像而去运行一个Java虚拟机。

如果你只是想开始使用GraalVM并进行实验,你可以跳到 “开发你的第一个 GraalVM 原生应用程序” 部分,以后再回到这部分。

1.1. 与JVM部署的主要区别

GraalVM原生镜像是提前制作的,这意味着原生和基于JVM的应用程序之间存在一些关键的差异。 主要的区别是。

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

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

  • GraalVM不能直接知道你的代码中的动态元素,必须被告知反射、资源、序列化和动态代理的情况。

  • 应用程序的classpath在构建时是固定的,不能改变。

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

  • 围绕Java应用程序的某些方面有一些限制,不完全支持。

GraalVM参考文档中的 Native Image Compatibility Guide(原生镜像兼容指南) 部分提供了关于GralVM限制的更多细节。

1.2. 了解Spring的提前处理(Ahead-of-Time Processing)

典型的Spring Boot应用程序是相当动态的,配置是在运行时进行的。事实上,Spring Boot自动配置的概念在很大程度上取决于对运行时状态的反应,以便正确配置。

虽然可以告诉GraalVM关于应用程序的这些动态方面,但这样做会破坏静态分析的大部分好处。因此,在使用Spring Boot创建本地镜像时,我们假设了一个封闭的世界,应用程序的动态方面受到了限制。

一个封闭世界的假设意味着以下限制。

  • classpath是固定的,在构建时完全定义。

  • 在你的应用程序中定义的Bean不能在运行时改变,也就是说。

    • Spring 的 @Profile 注解和 profile 的特定配置,有一些限制

    • 不支持创建Bean时发生变化的属性(例如,@ConditionalOnProperty.enable 属性)。

当这些限制到位后,Spring就有可能在构建时进行提前处理,并生成GraalVM可以使用的额外资源。一个经过Spring AOT处理的应用程序通常会生成。

  • Java 源代码

  • 字节码(用于动态代理等)

  • GraalVM的JSON hint文件。

    • 资源 hint(resource-config.json)

    • 反射 hint(reflect-config.json)

    • 序列化 hint(serialization-config.json)。

    • Java代理 hint (proxy-config.json)

    • JNI hint (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 process 将把上面的配置类转换为这样的代码。

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定义的性质。

你可以看到,上面生成的代码创建了与 @Configuration 类相当的Bean定义,但是是以一种GraalVM可以理解的直接方式。

有一个针对 myConfiguration Bean的Bean定义,还有一个针对 myBean 的定义。当需要一个 myBean 实例时,会调用一个 BeanInstanceSupplier。这个supplier将调用 myConfiguration Bean上的 myBean() 方法。

在Spring AOT处理过程中,你的应用程序被启动到可以使用Bean定义(bean definition)的程度。在AOT处理阶段,不会创建bean实例。

Spring AOT将为你的所有Bean定义生成这样的代码。当需要对Bean进行后处理(post-processing)时,它也会生成代码(例如,调用 @Autowired 方法)。一个 ApplicationContextInitializer 也将被生成,当AOT处理的应用程序实际运行时,Spring Boot将使用它来初始化 ApplicationContext

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

1.2.2. Hint 文件的生成

除了生成源文件,Spring AOT引擎还将生成hint文件,供GraalVM使用。hint文件包含JSON数据,描述了GraalVM应该如何处理它无法通过直接检查代码来理解的东西。

例如,你可能在一个私有方法上使用Spring注解。为了调用私有方法,Spring需要使用反射,即使是在GraalVM上。当出现这种情况时,Spring可以编写一个反射hint,这样GraalVM就知道即使没有直接调用该私有方法,它仍然需要在本地镜像中可用。

Hint文件是在 META-INF/native-image 下生成的,GraalVM会自动接收这些文件。

使用Maven时,生成的hint文件可以在 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 Native Images 和Spring超前引擎(ahead-of-time engine)的工作方式有了一个很好的概述,我们可以看看如何创建一个应用程序。

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

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

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

启动一个新的本地Spring Boot项目的最简单方法是进入 https://start.springboot.io,添加 “GraalVM Native Support” 依赖并生成项目。包含的 HELP.md 文件将提供入门提示。

2.1. 示例应用

我们需要一个可以用来创建原生镜像的示例应用程序。就我们的目的而言,“getting-started.html” 部分所涉及的简单的 "Hello World" web应用就足够了。

回顾一下,我们的 main application 代码看起来是这样的。

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. 使用Buildpacks构建原生镜像

Spring Boot包括直接针对Maven和Gradle的原生镜像的buildpack支持。这意味着你只需输入一条命令,就能迅速将一个合理的镜像放入本地运行的Docker守护程序中。由此产生的镜像不包含JVM,而是静态地编译本地镜像。这导致了更小的镜像。

用于镜像的构建器是 paketobuildpacks/builder:tiny。它占用空间小,攻击面(surface attack)小,但你也可以使用 paketobuildpacks/builder:basepaketobuildpacks/builder:full ,以便在需要时在镜像中提供更多工具。

2.2.1. 系统要求

应该安装Docker,更多细节见 Get Docker。如果你是在Linux上,将其 配置为允许非root用户

你可以运行 docker run hello-world(不用 sudo)来检查Docker守护进程是否能达到如期效果。查看 MavenGradle Spring Boot插件文档,了解更多细节。
在macOS上,建议将分配给Docker的内存增加到至少 8GB,并可能增加更多的CPU。更多细节请参见 Stack Overflow 的回答 。在Microsoft Windows上,确保启用 Docker WSL 2 backend 以获得更好的性能。

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.0-SNAPSHOT</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 goal 和 Native Build Tools 插件的 add-reachability-metadata goal 配置 executions。

为了构建镜像,你可以在 native 配置文件激活的情况下运行 spring-boot:build-image goal。

$ mvn -Pnative spring-boot:build-image

2.2.3. 使用 Gradle

当应用GraalVM Native Image插件时,Spring Boot Gradle插件会自动配置AOT任务。你应该检查你的Gradle构建中是否包含一个 plugins 块,其中包括 org.graalvm.buildtools.native

只要应用 了 org.graalvm.buildtools.native 插件,bootBuildImage 任务就会生成一个原生镜像,而不是JVM的。你可以用以下方式运行该任务

$ gradle bootBuildImage

2.2.4. 运行该例子

一旦你运行了相应的构建命令,就应该有一个Docker镜像了。你可以使用 docker run 启动你的应用程序。

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

你应该看到类似以下的输出。

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

如果你打开web浏览器,访问 localhost:8080,你应该看到以下输出。

Hello World!

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

2.3. 使用 Native Build Tools 构建原生镜像

如果你想不使用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或 22.3 版本的 Liberica Native Image Kit、Visual Studio Build Tools和Windows SDK。由于 Windows相关命令行的最大长度,请确保使用x64 Native Tools Command Prompt,而不是普通的Windows命令行来运行Maven或Gradle插件。

2.3.2. 使用 Maven

buildpack 支持一样,你需要确保使用 spring-boot-starter-parent 来继承 native 配置文件(profile),并确保使用 org.graalvm.buildtools:native-maven-plugin 插件。

native 配置文件激活时,你可以调用 native:compile goal 来触发 native-image 编译。

$ mvn -Pnative native:compile

target 目录中可以找到原生镜像的可执行文件。

2.3.3. 使用 Gradle

当Native Build Tools 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.0-SNAPSHOT)
....... . . .
....... . . . (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有足够的hint,以确保可以产生有效的原生镜像。

3.1. 用JVM测试提前处理(Ahead-of-time Processing)

当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. 使用 Native Build Tools 进行测试

GraalVM Native Build Tools 包括在本地镜像中运行测试的功能。当你想深入测试你的应用程序的内部结构在GraalVM本地镜像中的工作情况时,这可能会很有帮助。

生成包含要运行的测试的本地映像可能是一个耗时的操作,所以大多数开发人员可能更愿意在本地使用JVM。然而,作为CI管道的一部分,它们可能非常有用。例如,你可以选择每天运行一次本地测试。

Spring框架包括对运行测试的提前支持。所有常见的Spring测试功能都可以与原生镜像测试一起使用。例如,你可以继续使用 @SpringBootTest 注解。你还可以使用Spring Boot test slices,只测试你的应用程序的特定部分。

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.0-SNAPSHOT</version>
</parent>

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

如果你不想使用 spring-boot-starter-parent,你就需要为Spring Boot插件的 process-test-aot goal 和 Native Build Tools 插件的 test goal 配置 executions。

为了建立镜像和运行测试,使用 nativeTest 配置文件激活的 test goal。

$ mvn -PnativeTest test

3.2.2. 使用 Gradle

当应用 GraalVM Native Image 插件时,Spring Boot Gradle 插件会自动配置AOT测试任务。你应该检查你的Gradle构建包含一个 plugins 块,其中包括 org.graalvm.buildtools.native

要使用Gradle运行原生测试,你可以使用 nativeTest 任务。

$ gradle nativeTest

4. 原生镜像的高级主题

4.1. 嵌套的配置特性(Configuration Properties)

反射提示(Reflection hint)是由Spring的超前引擎(ahead-of-time engine)为配置属性自动创建的。然而,不属于内部类的嵌套配置属性必须用 @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 产生配置属性。如果没有嵌套字段上的 @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;
    }

}

当使用 record 时,你必须用 @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 来注解数据类(data class)的参数。

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
)
请在所有情况下使用 public 的getter和setter,否则属性将不能被绑定。

4.2. 转换 Spring Boot可执行Jar

只要Spring Boot 可执行的jar 包含AOT生成的资源,就可以将该jar转换为原生镜像。这在很多方面都很有用,包括。

  • 你可以保留你的常规JVM pipeline,并在你的CI/CD平台上将JVM应用程序变成一个原生镜像。

  • 由于 native-image 不支持交叉编译,你可以保留一个操作系统中立的部署工件,以后转换为不同的操作系统架构。

你可以使用 Cloud Native Buildpacks 将 Spring Boot 的可执行jar转换为原生镜像,或者使用GeralVM附带的 native-image 工具。

你的可执行jar必须包括AOT生成的资源,如生成的类和JSON hint文件。

4.2.1. 使用 Buildpacks

Spring Boot 应用程序通常通过Maven(mvn spring-boot:build-image)或Gradle(gradle bootBuildImage)集成使用 Cloud Native Buildpacks。 不过,你也可以使用 pack,把经过AOT处理的Spring Boot可执行jar变成一个原生容器镜像。

首先,确保有一个Docker守护程序(详情见 Get Docker)。如果你是在Linux上,将其 配置为允许非root用户

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

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

$ pack build --builder paketobuildpacks/builder: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.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中。只有在需要覆盖可及性元数据(reachability metadata)时才会包含它。
native-image -cp 标志不接受通配符。你需要确保所有的jar都被列出(上面的命令使用 findtr 来做这个)。

4.3. 使用追踪代理(Tracing Agent)

GraalVM 原生镜像 tracing agent 允许你在JVM上拦截反射、资源或代理的使用,以便生成相关的hint。Spring 应该会自动生成大部分的hint,但 tracing agent 可以用来快速识别缺少的条目。

当使用代理生成原生镜像的hint时,有几种方法。

  • 直接启动应用程序并“行使”它。

  • 运行应用测试以“行使”应用程序。

当一个库或一个模式不被Spring识别时,第一个选项对于识别缺失的hint很有意思。

对于可重复的设置来说,第二个选项听起来更有吸引力,但默认情况下,生成的hint将包括测试基础设施所需的任何东西。当应用程序真正运行时,其中一些将是不必要的。为了解决这个问题,代理支持一个访问过滤文件,它将导致某些数据从生成的输出中排除。

4.3.1. 直接启动应用程序

使用以下命令来启动附有原生镜像跟踪代理(tracing agent)的应用程序。

$ java -Dspring.aot.enabled=true \
    -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ \
    -jar target/myproject-0.0.1-SNAPSHOT.jar

现在你可以“行使”你希望有hint的代码路径,然后用 ctrl-c 停止应用程序。

在应用程序关闭时,原生镜像追踪代理(tracing agent)将把hint文件写入给定的配置输出目录。你可以手动检查这些文件,或者将它们作为原生镜像构建过程的输入。要使用它们作为输入,请将它们复制到 src/main/resources/META-INF/native-image/ 目录中。下次你构建原生镜像时,GraalVM 将考虑这些文件。

有更多的高级选项可以在原生镜像追踪代理(tracing agent)上设置,例如按调用者类别过滤记录的hint,等等。进一步阅读,请看 官方文档

4.4. 自定义Hint

如果你需要为反射、资源、序列化、代理使用等提供自己的hint,你可以使用 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 注解的application类)上使用 @ImportRuntimeHints 来激活这些hint。

如果你有需要进行绑定的类(大部分在序列化或反序列化JSON时需要),你可以在任何Bean上使用 @RegisterReflectionForBinding。大多数hint都是自动推断的,例如在接受或从 @RestController 方法返回数据时。但是当你直接使用 WebClientRestTemplate 工作时,你可能需要使用 @RegisterReflectionForBinding

4.4.1. 测试自定义Hint

RuntimeHintsPredicates API可以用来测试你的hint。该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社区正在通过为尚未发布自己的项目提供 可达性元数据(reachability metadata) 来提供帮助。Spring本身并不包含对第三方库的hint,而是依赖于可达性元数据项目。

如果你在为Spring Boot应用程序生成本地镜像时遇到问题,请查看 Spring Boot wiki 的 Spring Boot with GraalVM 页面。你也可以向GitHub上的 spring-aot-smoke-tests 项目提交问题,该项目用于确认常见的应用程序类型是否按预期运行。

如果你发现某个库不能与GraalVM一起使用,请在 可及性元数据(reachability metadata)项目 中提出issue。

5. 接下来读什么

如果你想进一步了解我们的构建插件所提供的提前处理,请参阅 MavenGradle 插件文档。要了解更多用于执行处理的API,请浏览Spring框架源代码中的 org.springframework.aot.generateorg.springframework.beans.factory.aot 包。

关于 Spring 和 GraalVM 的已知限制,请参见 Spring Boot wiki

下一节将继续介绍 Spring Boot CLI