進階原生映像檔主題

巢狀組態屬性

Spring 預先 (AOT) 引擎會自動為組態屬性建立反射提示。然而,非內部類別的巢狀組態屬性**必須**使用 `@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.name` 和 `my.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
)
請在所有情況下使用公開的 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 可用(詳情請參閱 取得 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 系統進行調整。
您的 jar 檔中可能沒有包含 @META-INF/native-image/argfile。只有在需要覆寫可達性中繼資料時才會包含它。
native-image-cp 旗標不接受萬用字元。您需要確保列出所有 jar 檔(上述指令使用 findtr 來執行此操作)。

使用追蹤代理程式

GraalVM native image 追蹤代理程式 允許您攔截 JVM 上的反射、資源或代理使用情況,以便產生相關提示。Spring 應該會自動產生大部分的提示,但可以使用追蹤代理程式快速識別遺漏的項目。

使用代理程式產生 native image 提示時,有幾種方法

  • 直接啟動應用程式並執行它。

  • 執行應用程式測試以執行應用程式。

當 Spring 無法辨識程式庫或模式時,第一個選項有助於識別遺漏的提示。

第二個選項對於可重複的設定來說更具吸引力,但預設情況下,產生的提示將包含測試基礎架構所需的所有內容。當應用程式實際執行時,其中一些將是不必要的。為了應對這個問題,代理程式支援一個存取過濾檔案,該檔案將導致從產生的輸出中排除某些資料。

直接啟動應用程式

使用以下指令啟動應用程式,並附加 native image 追蹤代理程式

$ 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 停止應用程式。

應用程式關閉時,native image 追蹤代理程式會將提示檔案寫入指定的設定輸出目錄。您可以手動檢查這些檔案,或將它們用作 native image 建置程序的輸入。要將它們用作輸入,請將它們複製到 src/main/resources/META-INF/native-image/ 目錄中。下次建置 native image 時,GraalVM 將會考慮這些檔案。

native image 追蹤代理程式還有更多進階選項可以設定,例如透過呼叫者類別過濾記錄的提示等。如需進一步閱讀,請參閱 官方文件

自訂提示 (Custom Hints)

如果您需要為反射、資源、序列化、代理使用等提供自己的提示,您可以使用 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 社群正在透過為尚未發佈自身原生映像支援的專案提供 可達性中繼資料 來提供協助。Spring 本身不包含第三方程式庫的提示,而是依賴於可達性中繼資料專案。

如果您在為 Spring Boot 應用程式產生原生映像時遇到問題,請查看 Spring Boot wiki 的 Spring Boot 與 GraalVM 頁面。您也可以在 GitHub 上為 spring-aot-smoke-tests 專案貢獻問題,該專案用於確認常見的應用程式類型是否按預期工作。

如果您發現無法與 GraalVM 搭配使用的程式庫,請在 可達性中繼資料專案 上提出問題。