知識庫 / Spring / Spring Boot RSS 訂閱

原生鏡像與 Spring Boot 和 GraalVM

Spring Boot
HongKong
6
12:30 PM · Dec 06 ,2025

1. 概述

本文將介紹原生圖像以及如何從 Spring Boot 應用程序和 GraalVM 的原生圖像構建器中創建原生圖像。 我們參考 Spring Boot 3,但將在文章末尾解決與 Spring Boot 2 的差異。

2. 本地圖像

本地圖像是一種構建 Java 代碼為獨立可執行文件的技術。該可執行文件包含應用程序類、其依賴項中的類、運行時庫類以及來自 JDK 的靜態鏈接的本機代碼。JVM 已打包到本機圖像中,因此目標系統上不需要任何 Java 運行時環境,但構建工件是平台相關的。因此,我們需要為每個支持的目標系統構建一個工件,這在使用諸如 Docker 之類的容器技術時會更容易,其中我們可以使用 Docker 構建一個作為目標系統運行容器,然後將其部署到任何 Docker 運行時。

2.1. GraalVM 和原生鏡像構建器

GraalVM (General Recursive Applicative and Algorithmic Language Virtual Machine) 是一款為 Java 和其他 JVM 語言(包括 JavaScript、Ruby、Python 等)編寫的高性能 JDK 發行版。它提供了一個原生鏡像構建器——一種將 Java 應用程序和虛擬機打包成獨立可執行文件的工具。它由 Spring Boot 官方支持,通過 Maven 和 Gradle 插件(Spring Boot 3.0.0 版本,存在一些例外情況,例如目前 Mockito 不支持原生測試)。

2.2 特殊功能

當構建原生圖像時,我們會遇到兩種典型功能。

即時編譯 (AOT Compilation) 是將高級 Java 代碼編譯為原生可執行代碼的過程。通常,這由 JVM 的即時編譯器 (JIT) 在運行時完成,這允許在應用程序執行過程中進行觀察和優化。與即時編譯相比,這種優勢將喪失。

通常,在進行即時編譯之前,可能存在一個可選步驟,稱為 AOT 處理,即收集代碼元數據並將其提供給 AOT 編譯器。由於 AOT 處理可以針對特定框架進行,而 AOT 編譯器則更具通用性,因此將這些 2 步劃分為合理的分步過程。以下圖片提供了一個概述:

Java 平台另一個特色是,只需將 JAR 文件添加到類路徑,即可在目標系統上進行擴展。由於啓動時對反射和註解掃描,我們就可以在應用程序中獲得擴展行為。

不幸的是,這會減慢啓動時間,並且對雲原生應用程序尤其沒有好處,因為服務器運行時和 Java 基礎類也打包到 JAR 中。因此,我們放棄了此功能,然後可以使用 封閉世界優化 構建應用程序。

這兩個功能都減少了在運行時需要執行的工作量。

2.3. 優勢

原生圖像提供了多種優勢,例如即時啓動和降低的內存消耗。它們可以打包成輕量級容器鏡像,從而實現更快速、更高效的部署,並且具有降低的攻擊面。

2.4. 侷限性

由於封閉世界優化,在編寫應用程序代碼和使用框架時,我們需要注意一些限制,具體如下:

  • 類初始化器可以在構建時執行,以實現更快的啓動和更好的峯值性能。但是,我們必須意識到這可能會打破代碼中的一些假設,例如,當加載一個需要在構建時可用的文件時。
  • 反射和動態代理在運行時成本較高,因此在封閉世界假設下,在構建時會進行優化。在類初始化器中,我們可以不受限制地使用它們(僅在構建時執行)。任何其他使用都必須告知 AOT 編譯器,Native Image 構建器會嘗試通過靜態代碼分析來實現。如果失敗,則必須提供此信息,例如通過 配置文件
  • 同樣適用於基於反射的所有技術,如 JNI 和序列化。
  • 此外,Native Image 構建器還提供了一個比 JNI 更簡單、開銷更低的本機接口。
  • 對於原生鏡像構建,運行時不再有字節碼可用,因此使用針對 JVMTI 的調試和監控工具不可行。我們必須使用本機調試器和監控工具。

關於 Spring Boot,我們必須意識到,諸如 profiles、條件 Bean 和 .enable 屬性之類的功能在運行時已不再完全受支持不再完全支持如果使用 profiles,則必須在構建時指定它們。

3. 基本設置

在開始構建原生圖像之前,我們需要安裝必要的工具。

3.1. GraalVM 和原生鏡像

首先,按照 安裝説明,安裝 GraalVM 的當前版本以及 native-image 構建器。 (Spring Boot 需要版本 22.3) 確保安裝目錄可以通過 GRAALVM_HOME 環境變量訪問,並且將 “<GRAALVM_HOME>/bin” 添加到 PATH 變量中。

3.2. 本地編譯器

在構建過程中,Native Image 構建器會調用特定平台的本地編譯器。因此,我們需要使用本地編譯器,並遵循 “先決條件” 指南,以供我們的平台使用。這將使構建過程具有平台依賴性。我們必須意識到,僅在特定平台的命令行中才能運行構建。例如,使用 Git Bash 在 Windows 上運行構建將不起作用。相反,需要使用 Windows 命令行。

3.3. Docker

為了確保順利進行,請先安裝 Docker,它在後續運行原生鏡像時是必需的。 Spring Boot Maven 和 Gradle 插件使用 Paketo Tiny Builder 構建容器。

4. 使用 Spring Boot 配置和構建項目

使用 Spring Boot 的原生構建功能非常簡單。我們首先創建項目,例如,通過使用 Spring Initializr 並添加應用程序代碼。然後,為了使用 GraalVM 的 Native Image 構建器構建原生鏡像,我們需要使用 GraalVM 提供的 Maven 或 Gradle 插件來擴展構建過程。

4.1. Maven

Spring Boot Maven 插件 提供用於 AOT(即非自身編譯,而是收集 AOT 編譯器元數據,例如註冊代碼中反射使用情況)處理以及構建可供 Docker 運行的 OCI 鏡像的目標。我們可以直接調用這些目標:

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

我們不需要這樣做,因為 Spring Boot 父POM 定義了一個 native 模式,它將這些目標綁定到構建上。我們需要激活此模式進行構建:

mvn clean package -Pnative

如果我們也想執行原生測試,我們可以激活第二個配置文件:

mvn clean package -Pnative,nativeTest

如果我們要構建原生鏡像,則必須添加相應的 native-maven-plugin。因此,我們也可以定義一個 native 配置文件。由於此插件由父 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 不支持原生測試執行。因此,我們可以排除 Mock 測試或通過將以下內容添加到我們的 POM 中,直接跳過原生測試:

<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. 在不使用 Spring Boot Parent POM 的情況下使用

如果無法從 Spring Boot Parent POM 繼承,但將其作為 <em import-scoped dependency</em> 使用,則必須手動配置插件和 profile。然後,需要將其添加到我們的 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. Gradle

Spring Boot Gradle 插件提供用於 AOT 處理(即,不直接進行 AOT 編譯,而是收集 AOT 編譯器元數據,例如,註冊代碼中反射的用法)的任務,以及用於構建可供 Docker 運行的 OCI 鏡像的任務:

gradle processAot
gradle processTestAot
gradle bootBuildImage

為了構建原生鏡像,我們需要添加 GraalVM Native Image 構建的 Gradle 插件

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

然後,我們可以運行測試並通過調用來構建項目。

gradle nativeTest
gradle nativeCompile

目前,Mockito 不支持原生測試執行。因此,我們可以排除 Mock 測試或通過配置 graalvmNative 擴展來跳過原生測試,方法如下:

graalvmNative {
    testSupport = false
}

5. 擴展原生鏡像構建配置

正如前面提到的,我們需要為 AOT 編譯器註冊所有反射、類路徑掃描、動態代理等的使用。由於 Spring 內置的原生支持是一個非常年輕的功能,並非所有 Spring 模塊當前都具有內置支持,因此我們目前需要自己添加它。 這可以通過手動創建構建配置來完成。 然而,使用 Spring Boot 提供的接口更容易,這樣 Maven 和 Gradle 插件可以在 AOT 處理期間使用我們的代碼來生成構建配置。

指定額外的原生配置的一種可能性是 原生提示。 讓我們來看兩個當前缺少內置支持的示例以及如何將其添加到應用程序中以使其正常工作。

5.1. 示例:Jackson 的 PropertyNamingStrategy

在 MVC Web 應用程序中,每個 REST 控制器方法的返回值都會被 Jackson 序列化,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 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. 編寫測試

為了測試 <em >RuntimeHintsRegistrar</em> 的實現,我們甚至不需要運行 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 的支持,我們可以實現一個 TestRuntimeHintsRegistrar 或使用 AotTestExecutionListener 接口,並使用一個 TestExecutionListener。有關更多詳細信息,請參閲 官方文檔

7. Spring Boot 2

Spring 6 和 Spring Boot 3 在原生鏡像構建方面邁出了重要一步。但即使在之前的重大版本中,這也是可行的。我們只需要知道,目前還沒有內置支持,即存在一個補充的 Spring Native 倡議,該倡議處理了這一主題。因此,我們需要手動包含和配置它在我們的項目中。對於 AOT(提前編譯)處理,存在一個單獨的 Maven 和 Gradle 插件,它並未合併到 Spring Boot 插件中。當然,集成庫也並未提供與現在(以及未來)相同的原生支持。

7.1. Spring Native 依賴

首先,我們需要添加 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. Buildpacks

為了構建 OCI 鏡像,我們需要明確配置一個 Buildpack。

使用 Maven 時,我們需要使用 spring-boot-maven-plugin,並使用 Paketo Java Buildpacks 進行原生鏡像配置,如 Paketo Java Buildpacks 所述。

<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>

在這裏,我們將使用眾多可用的構建器中的 tiny 構建器,例如 basefull 來構建原生鏡像

同時,我們通過將 true 值提供給 BP_NATIVE_IMAGE 環境變量來啓用構建包。

同樣,在使用 Gradle 時,我們可以將 tiny 構建器以及 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 依賴項添加到我們的 <em>pom.xml</em> 中:

<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-plugin spring-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>

此配置將調用 native-image 編譯器在打包階段進行構建。

但是,在使用 Gradle 時,我們將向 org.graalvm.buildtools.native 插件添加至 build.gradle 文件:

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

好的,以下是翻譯後的內容:

這就完成了!我們準備好通過在 Maven 的 package 命令中提供原生配置文件來構建我們的原生鏡像。

mvn clean package -Pnative

8. 結論

在本教程中,我們探討了使用 Spring Boot 和 GraalVM 原生構建工具構建 Native Image 的方法。我們瞭解了 Spring 對原生支持的內置支持。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.