1. 概述

在本文中,我们将了解本机映像以及如何从 Spring Boot 应用程序和 GraalVM 的本机映像构建器创建本机映像。 我们参考 Spring Boot 3,但我们将在本文末尾解决与 Spring Boot 2 的差异。

2. 原生镜像

本机映像是一种将 Java 代码构建为独立可执行文件的技术。 该可执行文件包括应用程序类、其依赖项中的类、运行时库类以及来自 JDK 的静态链接本机代码。 JVM 被打包到本机映像中,因此目标系统上不需要任何 Java 运行时环境,但构建工件是依赖于平台的。因此,我们需要为每个受支持的目标系统构建一个版本,当我们使用 Docker 等容器技术时,这会更容易,我们可以构建一个容器作为可以部署到任何 Docker 运行时的目标系统。

2.1. GraalVM 和本机映像生成器

通用递归应用和算法语言虚拟机 (Graal VM) 是为 Java 和其他 JVM 语言编写的高性能 JDK 发行版,并支持 JavaScript、Ruby、Python 和其他几种语言。它提供了一个 本机映像 构建器——一种从 Java 应用程序构建本机代码并将其与 VM 一起打包成独立可执行文件的工具。它受到 Spring Boot MavenGradle插件的官方支持,但有一些例外(最糟糕的是 Mockito 目前不支持本机测试)。

2.2.特殊功能

在构建原生镜像时,我们会遇到两个典型特征。

提前 (AOT) 编译 是将高级 Java 代码编译为本机可执行代码的过程。通常,这是由 JVM 的即时编译器 (JIT) 在运行时完成的,它允许在执行应用程序时进行观察和优化。在 AOT 编译的情况下,这个优势就消失了。

通常,在 AOT 编译之前,可以选择有一个称为 AOT 处理的 单独步骤,即从代码中收集元数据并将其提供给 AOT 编译器。分为这两个步骤是有意义的,因为 AOT 处理可以是特定于框架的,而 AOT 编译器则更为通用。下图给出了一个概览:

概述:本机构建步骤

Java 平台的另一个特点是只需将 JAR 放入类路径即可在目标系统上进行扩展。由于启动时进行反射和注释扫描,我们可以在应用程序中获得扩展行为。

不幸的是,这会减慢启动时间,并且不会带来任何好处,特别是对于云原生应用程序,甚至服务器运行时和 Java 基类都打包到 JAR 中。因此,我们省去了此功能,然后可以使用 封闭世界优化 来构建应用程序。

这两个功能都减少了运行时需要执行的工作量。

2.3.优点

本机映像提供各种优点,例如即时启动和减少内存消耗 。它们可以打包到轻量级容器映像中,以实现更快、更高效的部署,并且可以减少攻击面。

2.4.局限性

由于封闭世界优化,我们在编写应用程序代码和使用框架时必须注意一些限制。不久:

  • 类初始值设定项可以在构建时执行,以实现更快的启动和更好的峰值性能。但我们必须意识到,这可能会破坏代码中的一些假设,例如,加载一个必须在构建时可用的文件时。
  • 反射和动态代理在运行时非常昂贵,因此在封闭世界假设下在构建时进行了优化。在构建时执行时,我们可以在类初始值设定项中不受限制地使用它。任何其他用法都必须向 AOT 编译器声明,本机映像构建器尝试通过执行静态代码分析来实现该编译器。如果失败,我们必须提供此信息,例如通过配置文件
  • 这同样适用于所有基于反射的技术,例如 JNI 和序列化。
  • 此外,Native Image构建器提供了自己的原生接口,比JNI简单得多,开销也更低。
  • 对于本机映像构建,字节码在运行时不再可用,因此无法使用针对 JVMTI 的工具进行调试和监视。然后我们必须使用本机调试器和监视工具。

关于 Spring Boot,我们必须意识到配置文件、条件 bean 和 .enable 属性等功能在运行时不再完全支持 如果我们使用配置文件,则必须在构建时指定它们。

3. 基本设置

在构建本机映像之前,我们必须安装工具。

3.1. GraalVM 和本机镜像

首先,我们按照安装说明安装当前版本的 GraalVM 和 本机映像 生成器。 (Spring Boot 需要版本 22.3)我们应该确保安装目录可通过 GRAALVM_HOME 环境变量访问,并且 “<GRAALVM_HOME>/bin” 已添加到 PATH 变量中。

3.2.本机编译器

在构建过程中,本机映像构建器调用特定于平台的本机编译器。因此,我们需要这个本机编译器,遵循我们平台的“先决条件”说明。这将使构建平台依赖。我们必须意识到,只能在特定于平台的命令行中运行构建。例如,使用 Git Bash 在 Windows 上运行构建将不起作用。我们需要使用 Windows 命令行。

3.3.码头工人

作为先决条件,我们将确保安装Docker,稍后需要运行本机映像。 Spring Boot Maven 和 Gradle 插件使用Paketo Tiny Builder来构建容器。

4. 使用 Spring Boot 配置和构建项目

将本机构建功能与 Spring Boot 结合使用非常简单。例如,我们通过使用Spring Initializr并添加应用程序代码来创建项目。然后,要使用 GraalVM 的 Native Image 构建器构建本机镜像,我们需要使用 GraalVM 本身提供的 Maven 或 Gradle 插件来扩展我们的构建。

4.1.梅文

Spring Boot Maven 插件的目标是 AOT 处理(即,不是 AOT 编译本身,而是为 AOT 编译器收集元数据,例如,在代码中注册反射的使用)和构建可以与 Docker 一起运行的 OCI 映像。我们可以直接调用这些目标:

mvn spring-boot:process-aot
mvn spring-boot:process-test-aot
mvn spring-boot:build-image

我们不需要这样做,因为 Spring Boot 父 POM 定义了一个将这些目标绑定到构建的 本机配置 文件。我们需要使用这个激活的配置文件进行构建:

mvn clean package -Pnative

如果我们还想执行本机测试,我们可以激活第二个配置文件:

mvn clean package -Pnative,nativeTest

如果我们想构建一个原生镜像,我们必须添加相应的目标 native-maven-plugin 。因此,我们也可以定义一个 本机配置 文件。因为这个插件是由父POM管理的,所以我们可以留下版本号:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>compile-no-fork</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

目前,本机测试执行不支持 Mockito。 因此,我们可以通过将其添加到 POM 来排除 Mocking 测试或直接跳过本机测试:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <configuration>
                    <skipNativeTests>true</skipNativeTests>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

4.2.在没有父 POM 的情况下使用 Spring Boot

如果我们不能继承 Spring Boot Parent POM 而是将其用作*import-*scoped dependency ,我们就必须自己配置插件和配置文件。然后,我们必须将其添加到我们的 POM 中:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>${native-build-tools-plugin.version}</version>
                <extensions>true</extensions>
            </plugin>
        </plugins>
    </pluginManagement>
</build>
<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <image>
                            <builder>paketobuildpacks/builder:tiny</builder>
                            <env>
                                <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                            </env>
                        </image>
                    </configuration>
                    <executions>
                        <execution>
                            <id>process-aot</id>
                            <goals>
                                <goal>process-aot</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <configuration>
                        <classesDirectory>${project.build.outputDirectory}</classesDirectory>
                        <metadataRepository>
                            <enabled>true</enabled>
                        </metadataRepository>
                        <requiredVersion>22.3</requiredVersion>
                    </configuration>
                    <executions>
                        <execution>
                            <id>add-reachability-metadata</id>
                            <goals>
                                <goal>add-reachability-metadata</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
    <profile>
        <id>nativeTest</id>
        <dependencies>
            <dependency>
                <groupId>org.junit.platform</groupId>
                <artifactId>junit-platform-launcher</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>process-test-aot</id>
                            <goals>
                                <goal>process-test-aot</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <configuration>
                        <classesDirectory>${project.build.outputDirectory}</classesDirectory>
                        <metadataRepository>
                            <enabled>true</enabled>
                        </metadataRepository>
                        <requiredVersion>22.3</requiredVersion>
                    </configuration>
                    <executions>
                        <execution>
                            <id>native-test</id>
                            <goals>
                                <goal>test</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>
<properties>
    <native-build-tools-plugin.version>0.9.17</native-build-tools-plugin.version>
</properties>

4.3.摇篮

Spring Boot Gradle 插件提供了 AOT 处理(即不是 AOT 编译本身,而是为 AOT 编译器收集元数据,例如,在代码中注册反射的使用)和构建可以与 Docker 运行的 OCI 映像的任务:

gradle processAot
gradle processTestAot
gradle bootBuildImage

如果我们想构建原生镜像,我们必须添加Gradle插件来构建GraalVM原生镜像

plugins {
    // ...
    id 'org.graalvm.buildtools.native' version '0.9.17'
}

然后,我们可以通过调用来运行测试并构建项目

gradle nativeTest
gradle nativeCompile

目前,本机测试执行不支持 Mockito。 因此,我们可以通过配置 graalvmNative 扩展来排除 Mocking 测试或跳过本机测试,如下所示:

graalvmNative {
    testSupport = false
}

5. 扩展原生镜像构建配置

正如已经提到的,我们必须为 AOT 编译器注册反射、类路径扫描、动态代理等的每个用法。 因为Spring内置的原生支持是一个非常年轻的特性,目前并不是所有的Spring模块都有内置的支持,所以目前需要我们自己添加这个。 这可以通过手动创建构建配置来完成。尽管如此,使用 Spring Boot 提供的接口更容易,这样 Maven 和 Gradle 插件都可以在 AOT 处理期间使用我们的代码来生成构建配置。

指定附加本机配置的一种可能性是 Native Hints 。那么,让我们看看当前缺少的内置支持的两个示例,以及如何将其添加到我们的应用程序中以使其正常工作。

5.1.示例:Jackson 的 PropertyNamingStrategy

在 MVC Web 应用程序中,REST 控制器方法的每个返回值均由 Jackson 序列化,自动将每个属性命名为 JSON 元素。我们可以通过在应用程序属性文件中配置 Jackson 的 PropertyNamingStrategy 来全局影响名称映射:

spring.jacksonproperty-naming-strategy=SNAKE_CASE

SNAKE_CASEPropertyNamingStrategies 类型的静态成员的名称。不幸的是,这个成员是通过反思解决的。所以 AOT 编译器需要知道这一点,否则,我们会收到一条错误消息:

Caused by: java.lang.IllegalArgumentException: Constant named 'SNAKE_CASE' not found
  at org.springframework.util.Assert.notNull(Assert.java:219) ~[na:na]
  at org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
        $Jackson2ObjectMapperBuilderCustomizerConfiguration
        $StandardJackson2ObjectMapperBuilderCustomizer.configurePropertyNamingStrategyField(JacksonAutoConfiguration.java:287) ~[spring-features.exe:na]

为此,我们可以通过如下简单的方式实现并注册 RuntimeHintsRegistrar

@Configuration
@ImportRuntimeHints(JacksonRuntimeHints.PropertyNamingStrategyRegistrar.class)
public class JacksonRuntimeHints {

    static class PropertyNamingStrategyRegistrar implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            try {
                hints
                  .reflection()
                  .registerField(PropertyNamingStrategies.class.getDeclaredField("SNAKE_CASE"));
            } catch (NoSuchFieldException e) {
                // ...
            }
        }
    }

}

注意:自版本 3.0.0-RC2 以来,已经合并了在 Spring Boot 中解决此问题的拉取请求,因此它可以开箱即用地与 Spring Boot 3 配合使用。

5.2.示例:GraphQL 架构文件

如果我们想实现 GraphQL API ,我们需要创建一个模式文件并将其定位在 “classpath:/graphql/*.graphqls” 下,Springs GraphQL 自动配置会自动检测到它。这是通过类路径扫描以及集成的 GraphiQL 测试客户端的欢迎页面完成的。因此,为了在本机可执行文件中正常工作,AOT 编译器需要了解这一点。我们可以用同样的方式注册:

@ImportRuntimeHints(GraphQlRuntimeHints.GraphQlResourcesRegistrar.class)
@Configuration
public class GraphQlRuntimeHints {

    static class GraphQlResourcesRegistrar implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.resources()
              .registerPattern("graphql/**/")
              .registerPattern("graphiql/index.html");
        }
    }

}

Spring GraphQL 团队已经在致力于此,因此我们可能会在未来版本中内置此功能。

6. 编写测试

要测试 RuntimeHintsRegistrar 实现,我们甚至不需要运行 Spring Boot 测试,我们可以创建一个简单的 JUnit 测试,如下所示:

@Test
void shouldRegisterSnakeCasePropertyNamingStrategy() {
    // arrange
    final var hints = new RuntimeHints();
    final var expectSnakeCaseHint = RuntimeHintsPredicates
      .reflection()
      .onField(PropertyNamingStrategies.class, "SNAKE_CASE");
    // act
    new JacksonRuntimeHints.PropertyNamingStrategyRegistrar()
      .registerHints(hints, getClass().getClassLoader());
    // assert
    assertThat(expectSnakeCaseHint).accepts(hints);
}

如果我们想通过集成测试来测试它,我们可以检查 Jackson ObjectMapper 是否具有正确的配置:

@SpringBootTest
class JacksonAutoConfigurationIntegrationTest {

    @Autowired
    ObjectMapper mapper;

    @Test
    void shouldUseSnakeCasePropertyNamingStrategy() {
        assertThat(mapper.getPropertyNamingStrategy())
          .isSameAs(PropertyNamingStrategies.SNAKE_CASE);
    }

}

要使用本机模式对其进行测试,我们必须运行本机测试:

# Maven
mvn clean package -Pnative,nativeTest
# Gradle
gradle nativeTest

如果我们需要为 Spring Boot 测试提供特定于测试的 AOT 支持,我们可以使用 AotTestExecutionListener 接口实现 TestRuntimeHintsRegistrarTestExecutionListener 。我们可以在官方文档中找到详细信息。

7. 春季启动2

Spring 6 和 Spring Boot 3 在本机镜像构建方面迈出了一大步。但对于之前的主要版本,这也是可能的。我们只需要知道还没有内置支持,即有一个补充的Spring Native 计划来处理这个主题。因此,我们必须手动将其包含在我们的项目中并进行配置。对于 AOT 处理,有一个单独的 Maven 和 Gradle 插件,它没有合并到 Spring Boot 插件中。当然,集成库并没有提供像现在这样程度的本机支持(并且将来会提供更多)。

7.1. Spring原生依赖

首先,我们必须添加 Spring Native 的 Maven 依赖项:

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-native</artifactId>
    <version>0.12.1</version>
</dependency>

但是, 对于 Gradle 项目,Spring Native 是由 Spring AOT 插件自动添加的

我们应该注意, 每个 Spring Native 版本仅支持特定的 Spring Boot 版本 - 例如,Spring Native 0.12.1 仅支持 Spring Boot 2.7.1。因此,我们应该确保在 pom.xml 中使用兼容的 Spring Boot Maven 依赖项。

7.2.构建包

要构建 OCI 映像,我们需要显式配置构建包

对于 Maven,我们需要使用Paketo Java buildpacks 的 spring-boot-maven-plugin 和本机映像配置:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <builder>paketobuildpacks/builder:tiny</builder>
                        <env>
                            <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                        </env>
                    </image>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

在这里, 我们将使用各种可用构建器中的 小型 构建器(例如 basefull) 来构建本机映像 。此外,我们通过向 BP_NATIVE_IMAGE 环境变量提供 真实 值来启用构建包。

同样,当使用 Gradle 时,我们可以将 微型 构建器与 BP_NATIVE_IMAGE 环境变量一起添加到 build.gradle 文件中:

bootBuildImage {
    builder = "paketobuildpacks/builder:tiny"
    environment = [
        "BP_NATIVE_IMAGE" : "true"
    ]
}

7.3. Spring AOT 插件

接下来,我们需要添加Spring AOT插件,该插件执行提前转换,有助于改善本机映像的占用空间和兼容性。

因此,让我们将最新的 spring-aot-maven-plugin Maven 依赖项添加到 pom.xml 中:

<plugin>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-aot-maven-plugin</artifactId>
    <version>0.12.1</version>
    <executions>
        <execution>
            <id>generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

同样,对于 Gradle 项目,我们可以在 build.gradle 文件中添加最新的 org.springframework.experimental.aot 依赖项:

plugins {
    id 'org.springframework.experimental.aot' version '0.10.0'
}

此外,正如我们之前提到的,这会自动将 Spring Native 依赖项添加到 Gradle 项目中。

Spring AOT 插件提供了几个选项来确定源生成。例如, removeYamlSupportremoveJmxSupport 等选项分别删除Spring Boot Yaml和Spring Boot JMX支持。

7.4.构建并运行镜像

就是这样!我们已准备好使用 Maven 命令构建 Spring Boot 项目的本机映像:

$ mvn spring-boot:build-image

7.5。原生镜像构建

接下来,我们将添加一个名为 native 的 配置文件,其中包含一些插件的构建支持,例如 native-maven-pluginspring-boot-maven-plugin

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>0.9.17</version>
                    <executions>
                        <execution>
                            <id>build-native</id>
                            <goals>
                                <goal>build</goal>
                            </goals>
                            <phase>package</phase>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <classifier>exec</classifier>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

此配置文件将在打包阶段从构建中调用 本机映像 编译器。

但是,在使用 Gradle 时,我们会将最新的 org.graalvm.buildtools.native 插件添加到 build.gradle 文件中:

plugins {
    id 'org.graalvm.buildtools.native' version '0.9.17'
}

就是这样!我们准备通过在 Maven 命令中提供本机配置文件来构建本机映像:

mvn clean package -Pnative

八、结论

在本教程中,我们探索了使用 Spring Boot 和 GraalVM 的本机构建工具进行本机映像构建。我们了解了 Spring 的内置本机支持。

与往常一样,所有代码实现都可以 在 GitHub 上找到( Spring Boot 2 示例