Spring对GraalVM 本地镜像的支持

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:latestpaketobuildpacks/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-parentorg.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.nativeplugins 块。

只要应用了 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.nativeplugins块。

要使用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;
    }

}

当使用记录时,必须用@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

只要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(上述命令使用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提供了构建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本身不包含第三方库的提示,而是依赖于可达性元数据项目。

  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值