GraalVM 原生镜像支持中文文档

本文为官方文档直译版本。原文链接

GraalVM 原生镜像支持中文文档

  • 引言
  • GraalVM 原生镜像介绍
    • 与 JVM 部署的主要区别
    • 了解 Spring Ahead-of-Time 处理
    • 源代码生成
      • 生成提示文件
      • 生成代理类
  • 开发您的第一个 GraalVM 原生应用程序
    • 应用样本
    • 使用构建包构建原生映像
      • 系统要求
      • 使用 Maven
      • 使用 Gradle
      • 运行示例
    • 使用原生构建工具构建原生映像
      • 先决条件
        • Linux 和 macOS
        • Windows
      • 使用 Maven
      • 使用 Gradle
      • 运行示例
  • 测试 GraalVM 原生镜像
    • 使用 JVM 测试超前处理
    • 使用原生构建工具进行测试
      • 使用 Maven
      • 使用 Gradle
  • 高级原生图像主题
    • 嵌套配置属性
    • 转换 Spring Boot 可执行 Jar
      • 使用构建包
      • 使用 GraalVM 原生镜像
    • 使用跟踪代理
      • 直接启动应用程序
    • 自定义提示
      • 测试自定义提示
    • 已知限制

引言

GraalVM 原生映像是独立的可执行文件,可通过提前处理已编译的 Java 应用程序生成。与 JVM 相比,本地映像通常内存占用更小,启动速度更快。

GraalVM 原生镜像介绍

GraalVM 本地镜像提供了一种部署和运行 Java 应用程序的新方法。与 Java 虚拟机相比,本地映像的内存占用更小,启动速度更快。
原生镜像非常适合使用容器镜像部署的应用程序,与 “功能即服务”(FaaS)平台相结合时尤其有趣。
与为 JVM 编写的传统应用程序不同,GraalVM 原生镜像应用程序需要提前处理才能创建可执行文件。这种超前处理包括从主入口点对应用程序代码进行静态分析。
GraalVM Native Image 是一个完整的、特定于平台的可执行文件。要运行原生映像,您不需要安装 Java 虚拟机。

如果你只是想开始尝试 GraalVM,可以跳到 “开发你的第一个 GraalVM 原生应用程序” 部分,稍后再返回本部分。

与 JVM 部署的主要区别

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

  • 应用程序的静态分析在构建时从主入口点开始执行。
  • 创建原生映像时无法触及的代码将被删除,不会成为可执行文件的一部分。
  • GraalVM 无法直接感知代码中的动态元素,因此必须了解反射、资源、序列化和动态代理。
  • 应用程序的类路径在构建时是固定的,不能更改。
  • 没有懒类库加载,可执行文件中的所有内容都将在启动时加载到内存中。
  • Java 应用程序的某些方面存在一些限制,无法完全支持。

除了这些差异之外,Spring 还使用了一种称为 Spring Ahead-of-Time 处理的流程,它带来了更多限制。请务必至少阅读下一节的开头部分,以了解这些限制。

GraalVM 参考文档中的《原生镜像兼容性指南》部分提供了有关 GraalVM 限制的更多详细信息。

了解 Spring Ahead-of-Time 处理

典型的 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

源代码生成

Spring 应用程序由 Spring Beans 组成。在内部,Spring Framework 使用两种不同的概念来管理 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 定义的性质,生成的确切代码可能会有所不同。

您可以在上面看到,生成的代码会创建与 @Configuration 类等价的 Bean 定义,但生成的方式是 GraalVM 可以直接理解的。
myConfiguration 类有一个 bean 定义,myBean 也有一个。当需要 myBean 实例时,会调用 BeanInstanceSupplier。该供应商将调用 myConfiguration Bean 上的 myBean() 方法。

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

Spring AOT 会为您的所有 Bean 定义生成类似的代码。当需要对 bean 进行后处理时(例如,调用 @Autowired 方法),它也会生成代码。此外,还将生成 ApplicationContextInitializer,Spring Boot 将使用它在 AOT 处理过的应用程序实际运行时初始化 ApplicationContext

生成提示文件

除了生成源文件,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 Ahead-of-Time引擎的工作原理有了一个很好的了解,我们可以看看如何创建一个应用程序。
构建 Spring Boot 本地镜像应用程序主要有两种方法:

  • 使用 Spring Boot 对云原生构建包的支持,生成包含原生可执行文件的轻量级容器。
  • 使用 GraalVM 原生构建工具生成本地可执行文件。

启动新原生 Spring Boot 项目的最简单方法是访问 start.spring.io,添加 “GraalVM Native Support” 依赖关系并生成项目。随附的 HELP.md 文件将提供入门提示。

应用样本

我们需要一个可以用来创建原生图像的示例应用程序。就我们的目的而言,“getting-started.html” 部分所介绍的简单的 “Hello World!” 网络应用程序就足够了。
概括地说,我们的主应用程序代码如下:

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

使用构建包构建原生映像

Spring Boot 包含直接为 Maven 和 Gradle 原生镜像提供的 buildpack 支持。这意味着您只需键入一条命令,就能在本地运行的 Docker 守护进程中快速获取合理的映像。生成的映像不包含 JVM,而是静态编译本地映像。这样一来,映像的体积就更小了。

镜像使用的生成器是 paketobuildpacks/builder-jammy-tiny:late。它占用空间小,减少了攻击面,但如果需要,你也可以使用 paketobuildpacks/builder-jammy-base:latestpaketobuildpacks/builder-jammy-full:latest 在镜像中提供更多工具。

系统要求

应安装 Docker。详情请参阅获取 Docker。如果使用 Linux,请将其配置为允许非 root 用户访问。

你可以运行 docker run hello-world(不带 sudo)来检查 Docker 守护进程是否能如期到达。详情请查阅 Maven 或 Gradle Spring Boot 插件文档。

在 macOS 上,建议将分配给 Docker 的内存至少增加到 8GB,还可以增加 CPU。更多详情,请参阅 Stack Overflow 答案。在 Microsoft Windows 上,确保启用 Docker WSL 2 后端,以获得更好的性能。

使用 Maven

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

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>3.2.1version>
parent>

此外,您还应在 部分中添加此功能:

<plugin>
    <groupId>org.graalvm.buildtoolsgroupId>
    <artifactId>native-maven-pluginartifactId>
plugin>

spring-boot-starter-parent 声明了一个本地配置文件,该配置文件配置了创建本地映像时需要运行的执行程序。你可以使用命令行上的 -P 标志激活配置文件。

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

要构建镜像,可以在激活原生配置文件的情况下运行 spring-boot:build-image 目标:

$ mvn -Pnative spring-boot:build-image

使用 Gradle

当应用 GraalVM Native Image 插件时,Spring Boot Gradle 插件会自动配置 AOT 任务。您应该检查 Gradle 构建是否包含 org.graalvm.buildtools.native 的插件块。
只要应用了 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.1)
....... . . .
....... . . . (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 本地镜像工具包页面手动下载,也可以使用 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 原生映像工具包 22.3 版、Visual Studio 编译工具和 Windows SDK。由于 Windows 相关命令行的最大长度限制,请确保使用 x64 Native Tools Command Prompt 而不是常规的 Windows 命令行来运行 Maven 或 Gradle 插件。

使用 Maven

与 buildpack 支持一样,你需要确保使用 spring-boot-starter-parent 来继承本地配置文件,并使用 org.graalvm.buildtools:native-maven-plugin 插件。
原生配置文件激活后,你可以调用 native:compile 目标来触发native-image编译:

$ mvn -Pnative native:compile

原生镜像可执行文件可在target目录下找到。

使用 Gradle

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

$ gradle nativeCompile

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

运行示例

至此,应用程序应该可以正常运行了。现在,您可以直接运行应用程序来启动它:

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

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

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::  (v3.2.1)
....... . . .
....... . . . (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 来开发大部分单元测试和集成测试。这将有助于缩短开发人员的构建时间,并允许您使用现有的集成开发环境集成。有了 JVM 上广泛的测试覆盖范围,您就可以将原生镜像测试重点放在可能存在差异的地方。
对于原生镜像测试,您通常需要确保以下方面正常工作:

  • Spring AOT 引擎可以处理您的应用程序,并以 AOT 处理模式运行。
  • GraalVM 有足够的提示来确保生成有效的本地映像。

使用 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 进行构建,以激活原生配置文件。对于 Gradle,您需要确保您的构建包含 org.graalvm.buildtools.native 插件。

如果您的应用程序在启动时将 spring.aot.enabled 属性设置为 true,那么您就有更大的把握相信它在转换为原生映像时会正常工作。
您还可以考虑针对运行中的应用程序运行集成测试。例如,你可以使用 Spring WebClient 来调用应用程序的 REST 端点。或者可以考虑使用 Selenium 等项目来检查应用程序的 HTML 响应。

使用原生构建工具进行测试

GraalVM 原生构建工具包含在本地镜像中运行测试的功能。当你想深入测试应用程序的内部功能是否能在 GraalVM 原生镜像中正常工作时,这将非常有用。
生成包含要运行的测试的原生镜像可能是一项耗时的操作,因此大多数开发人员可能更倾向于在本地使用 JVM。不过,作为 CI 管道的一部分,它们还是非常有用的。例如,您可能会选择每天运行一次本地测试。
Spring Framework 支持提前运行测试。所有常用的 Spring 测试功能都适用于原生镜像测试。例如,你可以继续使用 @SpringBootTest 注解。您还可以使用 Spring Boot 测试片仅测试应用程序的特定部分。
Spring Framework 的原生测试支持以如下方式工作:

  • 对测试进行分析,以发现所需的任何 ApplicationContext 实例。
  • 对每个应用上下文进行超前处理并生成资产。
  • 创建原生镜像,生成的资产由 GraalVM 处理。
  • 本机镜像还包括配置了已发现测试列表的 JUnit TestEngine。
  • 启动原生镜像,触发引擎,引擎将运行每个测试并报告结果。

使用 Maven

要使用 Maven 运行本地测试,请确保您的 pom.xml 文件使用了 spring-boot-starter-parent 。您的 部分应如下所示:

<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>3.2.1version>
parent>

spring-boot-starter-parent 声明了一个 nativeTest 配置文件,用于配置运行原生测试所需的执行程序。可以使用命令行中的 -P 标志激活配置文件。

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

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

$ mvn -PnativeTest test

使用 Gradle

当应用 GraalVM Native Image 插件时,Spring Boot Gradle 插件会自动配置 AOT 测试任务。你应该检查你的 Gradle 构建是否包含 org.graalvm.buildtools.native 插件块。
要使用 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.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;
    }

    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 来注解数据类的参数:

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
)

请在所有情况下使用公共gettersetter,否则属性将无法绑定。

转换 Spring Boot 可执行 Jar

只要 Spring Boot 可执行 jar 包含 AOT 生成的资产,就可以将其转换为原生镜像。这样做有很多好处,包括

  • 您可以保留常规的 JVM 管道,并在 CI/CD 平台上将 JVM 应用程序转换为原生镜像。
  • 由于原生镜像不支持交叉编译,因此您可以保留一个操作系统中立的部署工件,之后再将其转换为不同的操作系统架构。

您可以使用云原生构建包或 GraalVM 随附的原生镜像工具将 Spring Boot 可执行 jar 转换为原生镜像。

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

使用构建包

Spring Boot 应用程序通常通过 Maven (mvn spring-boot:build-image) 或 Gradle (gradle bootBuildImage) 集成使用云原生构建包。不过,您也可以使用 pack 将 AOT 处理过的 Spring Boot 可执行 jar 变成原生容器映像。
首先,确保 Docker 守护进程可用(详情请参阅获取 Docker)。如果你使用的是 Linux 系统,请将其配置为允许非 root 用户使用。
还需要按照 buildpacks.io 上的安装指南安装软件包。
假设目标目录中有一个经过 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 即可通过这种方式生成镜像。

打包完成后,就可以使用 docker run 启动应用程序了:

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

使用 GraalVM 原生镜像

将经过 AOT 处理的 Spring Boot 可执行 jar 变为本地可执行文件的另一种方法是使用 GraalVM 原生镜像工具。为此,你需要在机器上安装 GraalVM 发行版。你可以在 Liberica 原生镜像工具包页面手动下载,也可以使用 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 中。只有在需要覆盖可达性元数据时才会包含它。

native-image -cp 标志不接受通配符。您需要确保列出所有 jar(上述命令使用 findtr 来做到这一点)。

使用跟踪代理

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 方法返回数据时。但是当您直接使用 WebClientRestClientRestTemplate 时,您可能需要使用 @RegisterReflectionForBinding

测试自定义提示

RuntimeHintsPredicates API 可用于测试您的提示。该 API 提供了构建谓词的方法,可用于测试 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 社区通过为尚未发布自己的项目提供可达性元数据来提供帮助。Spring 本身并不包含对第三方库的提示,而是依赖于可达性元数据项目。
如果您在为 Spring Boot 应用程序生成本地镜像时遇到问题,请查看 Spring Boot wiki 的 Spring Boot with GraalVM 页面。您还可以向 GitHub 上的 spring-aot-smoke-tests 项目提交问题,该项目用于确认常见应用类型是否按预期运行。
如果你发现某个库无法与 GraalVM 协同工作,请在可达性元数据项目中提出问题。

你可能感兴趣的:(spring,boot,spring,boot,后端,java)