高階原生映象主題

巢狀配置屬性

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 屬性將無法繫結。您也可以註解 getter 方法。

使用建構函式繫結時,您必須用 @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;
	}

}

使用記錄 (records) 時,您必須用 @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

只要 Spring Boot 可執行 Jar 包含 AOT 生成的資產,就可以將其轉換為原生映象。這樣做有很多原因,其中包括

  • 您可以保留常規的 JVM 管道,並在 CI/CD 平臺上將 JVM 應用程式轉換為原生映象。

  • 由於 native-image 不支援交叉編譯,您可以保留一個與作業系統無關的部署 Artifact,稍後再將其轉換為不同的作業系統架構。

您可以使用雲原生 Buildpacks 或 GraalVM 附帶的 native-image 工具將 Spring Boot 可執行 Jar 轉換為原生映象。

您的可執行 Jar 必須包含 AOT 生成的資產,例如生成的類和 JSON 提示檔案。

使用 Buildpacks

Spring Boot 應用程式通常透過 Maven (mvn spring-boot:build-image) 或 Gradle (gradle bootBuildImage) 整合來使用雲原生 Buildpacks。但是,您也可以使用 pack 將經過 AOT 處理的 Spring Boot 可執行 Jar 轉換為原生容器映象。

首先,請確保 Docker daemon 已可用(更多詳情請參見獲取 Docker)。如果您使用的是 Linux,請配置它以允許非 root 使用者

您還需要按照 buildpacks.io 上的安裝指南 安裝 pack

假設一個經過 AOT 處理並構建為 myproject-0.0.1-SNAPSHOT.jar 的 Spring Boot 可執行 Jar 位於 target 目錄中,執行

$ pack build --builder paketobuildpacks/builder-jammy-java-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 處理並構建為 myproject-0.0.1-SNAPSHOT.jar 的 Spring Boot 可執行 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 的方法,該 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 JSON 提示檔案中靜態提供自定義提示。這些檔案應放在 src/main/resources/ 目錄下的 META-INF/native-image/*/*/ 目錄中。AOT 處理期間生成的提示會寫入名為 META-INF/native-image/{groupId}/{artifactId}/ 的目錄。將您的靜態提示檔案放在與此位置不衝突的目錄中,例如 META-INF/native-image/{groupId}/{artifactId}-additional-hints/

已知限制

GraalVM 原生映象是一項不斷發展的技術,並非所有庫都提供支援。GraalVM 社群透過為尚未提供自身支援的專案提供可達性元資料來提供幫助。Spring 本身不包含第三方庫的提示,而是依賴於可達性元資料專案。

如果您在為 Spring Boot 應用程式生成原生映象時遇到問題,請檢視 Spring Boot wiki 上的Spring Boot with GraalVM 頁面。您還可以向 GitHub 上的 spring-aot-smoke-tests 專案貢獻問題,該專案用於確認常見的應用程式型別按預期工作。

如果您發現某個庫無法與 GraalVM 一起使用,請在可達性元資料專案中提出問題。