進階原生映像檔主題

巢狀組態屬性

Spring 預先編譯引擎會自動為組態屬性建立反射提示。然而,並非內部類別的巢狀組態屬性**必須**使用 @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();

	// getters / setters...

	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;

	// getters / setters...

	public int getNumber() {
		return this.number;
	}

	public void setNumber(int number) {
		this.number = number;
	}

}

上面的範例會為 my.properties.namemy.properties.nested.number 產生組態屬性。如果 nested 欄位上沒有 @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;
	}

	// getters / setters...

	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
)
在所有情況下,請使用 public getter 和 setter,否則屬性將無法繫結。

轉換 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 daemon 可用 (請參閱 Get Docker 以取得更多詳細資訊)。如果您在 Linux 上,請 將其設定為允許非 root 使用者

您也需要依照 buildpacks.io 上的安裝指南 安裝 pack

假設 AOT 處理的 Spring Boot 可執行 jar 建置為 myproject-0.0.1-SNAPSHOT.jar 位於 target 目錄中,請執行

$ 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! 這樣的下載管理器。

假設 AOT 處理的 Spring Boot 可執行 jar 建置為 myproject-0.0.1-SNAPSHOT.jar 位於 target 目錄中,請執行

$ 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.packaging.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 搭配使用,請在 可達性中繼資料專案 上提出問題。