使用 Spring Boot 和 GraalVM 的原生镜像

🧑 博主简介:CSDN博客专家历代文学网 (PC端可以访问:历代文学,移动端可微信小程序搜索"历代文学 ")总架构师,15年工作经验,精通Java编程高并发设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作 请加本人wx(注明来自csdn ):foreast_sea


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 和其他几种语言。它提供了一个 Native Image 构建器 -- 一种从 Java 应用程序构建原生代码并将其与 VM 一起打包成独立可执行文件的工具。它由 Spring Boot MavenGradle Plugin 官方支持,但有一些例外(最糟糕的是 Mockito 目前不支持原生测试)。

2.2. 特殊功能

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

预先 (AOT) 编译是将高级 Java 代码编译为本机可执行代码的过程。通常,这是由 JVM 的 Just-in-time 编译器 (JIT) 在运行时完成的,它允许在执行应用程序时进行观察和优化。在 AOT 编译的情况下,此优势将丢失。

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

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

遗憾的是,这会减慢启动时间,并且不会带来任何好处,尤其是对于云原生应用程序,其中甚至服务器运行时和 Java 基类也被打包到 JAR 中。因此,我们省去了这个功能,然后可以使用 Closed World Optimization 构建应用程序。

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

2.3. 优势

本机映像具有各种优势,例如即时启动和减少内存消耗。它们可以打包到轻量级容器映像中,以便更快、更高效地部署,并且它们减少了攻击面。

2.4. 限制

由于 Closed World Optimization,在编写应用程序代码和使用框架时,我们必须注意一些限制。不久:

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

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

3. 基本设置

在构建原生镜像之前,我们必须安装这些工具。

3.1. GraalVM 和原生镜像

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

3.2. 原生编译器

在构建过程中,Native Image 构建器会调用特定于平台的原生编译器。因此,我们需要这个原生编译器,按照我们平台的 "Prerequisite" 说明进行操作。这将使构建平台相关。我们必须知道,只能在特定于平台的命令行中运行构建。例如,使用 Git Bash 在 Windows 上运行构建将不起作用。我们需要改用 Windows 命令行。

3.3. Docker

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

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

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

4.1. Maven 浏览器

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

hljs-copy-wrapper 复制代码
mvn spring-boot:process-aot
mvn spring-boot:process-test-aot
mvn spring-boot:build-image

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

hljs-copy-wrapper 复制代码
mvn clean package -Pnative

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

hljs-copy-wrapper 复制代码
mvn clean package -Pnative,nativeTest

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

hljs-copy-wrapper 复制代码
<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。**因此,我们可以排除 Mocking 测试,或者通过将以下内容添加到我们的 POM 中来跳过本机测试:

hljs-copy-wrapper 复制代码
<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 继承,而是将其用作导入范围的依赖项,则必须自己配置插件和配置文件。然后,我们必须将以下内容添加到我们的 POM 中:

hljs-copy-wrapper 复制代码
<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 映像:

hljs-copy-wrapper 复制代码
gradle processAot
gradle processTestAot
gradle bootBuildImage

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

hljs-copy-wrapper 复制代码
plugins {
    // ...
    id 'org.graalvm.buildtools.native' version '0.9.17'
}

然后,我们可以通过调用

hljs-copy-wrapper 复制代码
gradle nativeTest
gradle nativeCompile

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

hljs-copy-wrapper 复制代码
graalvmNative {
    testSupport = false
}

5. 扩展本机映像生成配置

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

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

5.1. 示例:Jackson 的 PropertyNamingStrategy

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

hljs-copy-wrapper 复制代码
spring.jacksonproperty-naming-strategy=SNAKE_CASE

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

hljs-copy-wrapper 复制代码
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

hljs-copy-wrapper 复制代码
@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 自动配置会自动检测到它。这是通过 Classpath scanning 以及集成的 GraphiQL 测试客户端的欢迎页面完成的。因此,要在本机可执行文件中正常工作,AOT 编译器需要了解这一点。我们可以用同样的方式注册它:

hljs-copy-wrapper 复制代码
@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 测试,如下所示:

hljs-copy-wrapper 复制代码
@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 是否具有正确的配置:

hljs-copy-wrapper 复制代码
@SpringBootTest
class JacksonAutoConfigurationIntegrationTest {

    @Autowired
    ObjectMapper mapper;

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

}

要使用 native 模式对其进行测试,我们必须运行一个 native test:

hljs-copy-wrapper 复制代码
# Maven
mvn clean package -Pnative,nativeTest
# Gradle
gradle nativeTest

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

7. Spring Boot 2

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

7.1. Spring 原生依赖

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

hljs-copy-wrapper 复制代码
<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

hljs-copy-wrapper 复制代码
<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 环境变量提供 true 值来启用 buildpack。

同样,在使用 Gradle 时,我们可以将 tiny 构建器以及 BP_NATIVE_IMAGE 环境变量添加到 build.gradle 文件中:

hljs-copy-wrapper 复制代码
bootBuildImage {
    builder = "paketobuildpacks/builder:tiny"
    environment = [
        "BP_NATIVE_IMAGE" : "true"
    ]
}

7.3. Spring AOT 插件

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

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

hljs-copy-wrapper 复制代码
<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 依赖项:

hljs-copy-wrapper 复制代码
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 项目的本机映像:

hljs-copy-wrapper 复制代码
$ mvn spring-boot:build-image

7.5. 原生镜像构建

接下来,我们将添加一个名为 native 的配置文件,该配置文件支持一些插件,例如 native-maven-plugin 和 spring-boot-maven-plugin

hljs-copy-wrapper 复制代码
<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>

此配置文件将在打包阶段从构建中调用 native-image 编译器。

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

hljs-copy-wrapper 复制代码
plugins {
    id 'org.graalvm.buildtools.native' version '0.9.17'
}

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

hljs-copy-wrapper 复制代码
mvn clean package -Pnative

8. 总结

在本教程中,我们探索了使用 Spring Boot 和 GraalVM 的原生构建工具构建原生镜像。我们了解了 Spring 的内置原生支持。所有代码实现都可以在 GitHub 上找到Spring Boot 2 示例)。

相关推荐
mghio3 小时前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室8 小时前
java日常开发笔记和开发问题记录
java
咖啡教室8 小时前
java练习项目记录笔记
java
鱼樱前端9 小时前
maven的基础安装和使用--mac/window版本
java·后端
RainbowSea10 小时前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq
RainbowSea10 小时前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·消息队列·rabbitmq
李少兄11 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝11 小时前
【设计模式】原型模式
java·设计模式·原型模式
可乐加.糖12 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信