提前最佳化

本章介紹 Spring 的提前 (AOT) 最佳化。

有關整合測試特定的 AOT 支援,請參閱 測試的提前支援

提前最佳化介紹

Spring 對 AOT 最佳化的支援旨在在構建時檢查 ApplicationContext,並應用通常在執行時發生的決策和發現邏輯。這樣做可以構建更直接的應用程式啟動安排,主要基於類路徑和環境,專注於固定的一組功能。

提早應用此類最佳化意味著以下限制

  • 類路徑在構建時是固定的且完全定義。

  • 應用程式中定義的 bean 在執行時不能改變,這意味著

    • @Profile,特別是針對特定配置檔案的配置,需要在構建時選擇,並在啟用 AOT 時在執行時自動啟用。

    • 影響 bean 是否存在的 Environment 屬性 (@Conditional) 只在構建時考慮。

  • 帶有例項供應商(lambda 或方法引用)的 Bean 定義無法提前轉換。

  • 註冊為單例(通常使用 ConfigurableListableBeanFactoryregisterSingleton)的 Bean 也無法提前轉換。

  • 由於我們不能依賴例項,請確保 Bean 型別儘可能精確。

另請參閱最佳實踐部分。

當存在這些限制時,就可以在構建時執行提前處理並生成額外的資產。經過 Spring AOT 處理的應用程式通常會生成

  • Java 原始碼

  • 位元組碼(通常用於動態代理)

  • RuntimeHints 用於反射、資源載入、序列化和 JDK 代理

目前,AOT 主要用於允許 Spring 應用程式使用 GraalVM 部署為原生映象。我們打算在未來的版本中支援更多基於 JVM 的用例。

AOT 引擎概述

AOT 引擎處理 ApplicationContext 的入口點是 ApplicationContextAotGenerator。它基於代表要最佳化的應用程式的 GenericApplicationContext 和一個 GenerationContext 來處理以下步驟

  • 重新整理 ApplicationContext 以進行 AOT 處理。與傳統的重新整理不同,此版本只建立 bean 定義,而不建立 bean 例項。

  • 呼叫可用的 BeanFactoryInitializationAotProcessor 實現,並將其貢獻應用於 GenerationContext。例如,一個核心實現會遍歷所有候選 bean 定義,並生成必要的程式碼來恢復 BeanFactory 的狀態。

此過程完成後,GenerationContext 將更新生成的程式碼、資源和應用程式執行所需的類。RuntimeHints 例項還可以用於生成相關的 GraalVM 原生映象配置檔案。

ApplicationContextAotGenerator#processAheadOfTime 返回 ApplicationContextInitializer 入口點的類名,該入口點允許在啟用 AOT 最佳化的情況下啟動上下文。

以下各節將更詳細地介紹這些步驟。

AOT 處理的重新整理

所有 GenericApplicationContext 實現都支援 AOT 處理的重新整理。可以使用任意數量的入口點建立應用程式上下文,通常是 @Configuration 註解的類。

我們來看一個基本示例

	@Configuration(proxyBeanMethods=false)
	@ComponentScan
	@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
	public class MyApplication {
	}

使用常規執行時啟動此應用程式涉及多個步驟,包括類路徑掃描、配置類解析、bean 例項化和生命週期回撥處理。AOT 處理的重新整理只應用常規 refresh 的一部分。可以按如下方式觸發 AOT 處理

		RuntimeHints hints = new RuntimeHints();
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(MyApplication.class);
		context.refreshForAotProcessing(hints);
		// ...
		context.close();

在此模式下,BeanFactoryPostProcessor 實現會照常呼叫。這包括配置類解析、import 選擇器、類路徑掃描等。這些步驟確保 BeanRegistry 包含應用程式相關的 bean 定義。如果 bean 定義受條件(例如 @Profile)的保護,則會評估這些條件,不符合條件的 bean 定義在此階段會被丟棄。

如果自定義程式碼需要程式設計式地註冊額外的 bean,請確保自定義註冊程式碼使用 BeanDefinitionRegistry 而不是 BeanFactory,因為只考慮 bean 定義。一個好的模式是實現 ImportBeanDefinitionRegistrar 並透過 @Import 在其中一個配置類上註冊它。

因為此模式不實際建立 bean 例項,所以 BeanPostProcessor 實現不會被呼叫,除了與 AOT 處理相關的特定變體。這些是

  • MergedBeanDefinitionPostProcessor 實現後置處理 bean 定義,以提取額外設定,例如 initdestroy 方法。

  • SmartInstantiationAwareBeanPostProcessor 實現在必要時確定更精確的 bean 型別。這確保建立在執行時所需的任何代理。

這部分完成後,BeanFactory 包含應用程式執行所需的 bean 定義。它不觸發 bean 例項化,但允許 AOT 引擎檢查將在執行時建立的 bean。

Bean Factory 初始化 AOT 貢獻

想要參與此步驟的元件可以實現 BeanFactoryInitializationAotProcessor 介面。每個實現都可以基於 bean 工廠的狀態返回一個 AOT 貢獻。

AOT 貢獻是貢獻生成程式碼的元件,這些程式碼複製了特定行為。它還可以貢獻 RuntimeHints,以指示對反射、資源載入、序列化或 JDK 代理的需求。

BeanFactoryInitializationAotProcessor 實現可以在 META-INF/spring/aot.factories 中註冊,鍵等於介面的完全限定名。

BeanFactoryInitializationAotProcessor 介面也可以直接由 bean 實現。在此模式下,該 bean 提供的 AOT 貢獻等同於其在常規執行時提供的功能。因此,此類 bean 會自動從 AOT 最佳化後的上下文中排除。

如果 bean 實現了 BeanFactoryInitializationAotProcessor 介面,則該 bean 及其所有依賴項將在 AOT 處理期間初始化。我們通常建議此介面僅由 BeanFactoryPostProcessor 等基礎設施 bean 實現,這些 bean 的依賴有限,並且已經在 bean 工廠生命週期的早期初始化。如果此類 bean 使用 @Bean 工廠方法註冊,請確保該方法是靜態的,這樣其所在的 @Configuration 類就不必初始化。

Bean 註冊 AOT 貢獻

核心 BeanFactoryInitializationAotProcessor 實現負責收集每個候選 BeanDefinition 的必要貢獻。它透過專用的 BeanRegistrationAotProcessor 來完成。

此介面的使用方式如下

  • BeanPostProcessor bean 實現,以替換其執行時行為。例如 AutowiredAnnotationBeanPostProcessor 實現了此介面,以生成注入帶有 @Autowired 註解的成員的程式碼。

  • 由在 META-INF/spring/aot.factories 中註冊的型別實現,鍵等於介面的完全限定名。通常用於需要針對核心框架的特定功能調整 bean 定義的情況。

如果 bean 實現了 BeanRegistrationAotProcessor 介面,則該 bean 及其所有依賴項將在 AOT 處理期間初始化。我們通常建議此介面僅由 BeanFactoryPostProcessor 等基礎設施 bean 實現,這些 bean 的依賴有限,並且已經在 bean 工廠生命週期的早期初始化。如果此類 bean 使用 @Bean 工廠方法註冊,請確保該方法是靜態的,這樣其所在的 @Configuration 類就不必初始化。

如果沒有 BeanRegistrationAotProcessor 處理特定的已註冊 bean,則由預設實現處理。這是預設行為,因為針對 bean 定義生成的程式碼調整應僅限於特殊情況。

以上一個示例為例,假設 DataSourceConfiguration 如下所示

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

	@Bean
	public SimpleDataSource dataSource() {
		return new SimpleDataSource();
	}

}
@Configuration(proxyBeanMethods = false)
class DataSourceConfiguration {

	@Bean
	fun dataSource() = SimpleDataSource()

}
不支援使用反引號且包含無效 Java 識別符號(非字母開頭、包含空格等)的 Kotlin 類名。

由於此類上沒有任何特定條件,因此 dataSourceConfigurationdataSource 被識別為候選者。AOT 引擎會將上面的配置類轉換為類似以下的程式碼

  • Java

/**
 * Bean definitions for {@link DataSourceConfiguration}
 */
@Generated
public class DataSourceConfiguration__BeanDefinitions {
	/**
	 * Get the bean definition for 'dataSourceConfiguration'
	 */
	public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
		Class<?> beanType = DataSourceConfiguration.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
		return beanDefinition;
	}

	/**
	 * Get the bean instance supplier for 'dataSource'.
	 */
	private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
		return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
				.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
	}

	/**
	 * Get the bean definition for 'dataSource'
	 */
	public static BeanDefinition getDataSourceBeanDefinition() {
		Class<?> beanType = SimpleDataSource.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier());
		return beanDefinition;
	}
}
生成的具體程式碼可能因 bean 定義的具體性質而異。
每個生成的類都使用 org.springframework.aot.generate.Generated 進行註解,以便在需要排除它們時(例如由靜態分析工具)進行識別。

上面生成的程式碼建立的 bean 定義等同於 @Configuration 類,但採用直接方式,並且儘可能不使用反射。有一個用於 dataSourceConfiguration 的 bean 定義,還有一個用於 dataSourceBean

當需要 datasource 例項時,會呼叫 BeanInstanceSupplier。此 supplier 會呼叫 dataSource() 方法在 dataSourceConfiguration bean 上。

使用 AOT 最佳化執行

AOT 是將 Spring 應用程式轉換為原生可執行檔案的一個強制步驟,因此在此模式下執行時會自動啟用它。可以透過將 spring.aot.enabled 系統屬性設定為 true 來在 JVM 上使用這些最佳化。

當包含 AOT 最佳化時,一些在構建時做出的決策會硬編碼到應用程式設定中。

例如,在構建時啟用的 profile 在執行時也會自動啟用。

最佳實踐

AOT 引擎旨在處理儘可能多的用例,而無需更改應用程式程式碼。但是,請記住,一些最佳化是在構建時基於 bean 的靜態定義進行的。

本節列出了確保應用程式為 AOT 做好準備的最佳實踐。

程式設計式 Bean 註冊

AOT 引擎會處理 @Configuration 模型以及作為處理配置一部分可能被呼叫的任何回撥。如果需要程式設計式註冊額外的 bean,請確保使用 BeanDefinitionRegistry 來註冊 bean 定義。

這通常可以透過 BeanDefinitionRegistryPostProcessor 完成。請注意,如果它自己被註冊為一個 bean,除非您也實現了 BeanFactoryInitializationAotProcessor,否則它會在執行時再次被呼叫。更地道的方式是實現 ImportBeanDefinitionRegistrar 並透過 @Import 在其中一個配置類上註冊它。

這會在配置類解析過程中呼叫您的自定義程式碼。

如果使用不同的回撥程式設計式宣告額外的 bean,它們很可能不會被 AOT 引擎處理,因此不會為它們生成任何提示。根據環境的不同,這些 bean 可能根本不會被註冊。例如,類路徑掃描在原生映象中不起作用,因為沒有類路徑的概念。對於這種情況,掃描在構建時發生至關重要。

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public MyInterface myInterface() {
		return new MyImplementation();
	}

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface(): MyInterface = MyImplementation()

}

暴露最精確的 Bean 型別

雖然您的應用程式可能與 bean 實現的介面互動,但宣告最精確的型別仍然非常重要。AOT 引擎會對 bean 型別執行額外檢查,例如檢測 @Autowired 成員或生命週期回撥方法是否存在。

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public MyImplementation myInterface() {
		return new MyImplementation();
	}

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface() = MyImplementation()

}

對於 @Configuration 類,請確保工廠 @Bean 方法的返回型別儘可能精確。考慮以下示例

在上面的示例中,myInterface bean 的宣告型別是 MyInterface。通常的後處理都不會考慮 MyImplementation

例如,如果 MyImplementation 上有一個帶有註解的處理器方法,上下文應該註冊它,但它不會提前被檢測到。

如果您正在處理一個無法修改的程式碼庫,可以在相關的 bean 定義上設定 preferredConstructors 屬性來指示應使用哪個建構函式。

避免將複雜資料結構用於建構函式引數和屬性

在以程式設計方式建立 RootBeanDefinition 時,您在使用型別方面不受限制。例如,您可能有一個帶有多個屬性的自定義 record,您的 bean 將其作為建構函式引數。

雖然這在常規執行時環境中工作良好,但 AOT 不知道如何生成您的自定義資料結構的程式碼。一個好的經驗法則是記住 bean 定義是構建在多種模型之上的抽象。建議不要使用此類結構,而是分解為簡單型別或引用以這種方式構建的 bean。

作為最後手段,您可以實現自己的 org.springframework.aot.generate.ValueCodeGenerator$Delegate。要使用它,請在 META-INF/spring/aot.factories 中以 Delegate 為鍵註冊其完全限定名。

避免使用自定義引數建立 Bean

Spring AOT 檢測建立 bean 需要做什麼,並使用例項 supplier 將其轉換為生成的程式碼。容器也支援使用自定義引數建立 bean,這會導致 AOT 出現一些問題。

  1. 自定義引數需要對匹配的建構函式或工廠方法進行動態自省。AOT 無法檢測到這些引數,因此必須手動提供必要的反射提示。

  2. 繞過例項 supplier 意味著建立後的所有其他最佳化也會被跳過。例如,欄位和方法上的自動裝配將被跳過,因為它們是在例項 supplier 中處理的。

與其使用自定義引數建立 prototype 範圍的 bean,我們建議採用手動工廠模式,其中一個 bean 負責建立例項。

避免迴圈依賴

某些用例可能導致一個或多個 bean 之間出現迴圈依賴。在常規執行時環境中,可能可以透過在 setter 方法或欄位上使用 @Autowired 來連線這些迴圈依賴。然而,AOT 最佳化的上下文在存在顯式迴圈依賴時將無法啟動。

因此,在 AOT 最佳化的應用程式中,應努力避免迴圈依賴。如果無法避免,可以使用 @Lazy 注入點或 ObjectProvider 來延遲訪問或檢索必要的協作 bean。有關更多資訊,請參閱此提示

FactoryBean

應謹慎使用 FactoryBean,因為它在 bean 型別解析方面引入了一箇中間層,這可能在概念上不是必需的。根據經驗,如果 FactoryBean 例項不持有長期狀態且在執行時後期不需要,則應將其替換為常規工廠方法,頂部可以加上 FactoryBean 介面卡層(用於宣告式配置目的)。

如果您的 FactoryBean 實現沒有解析物件型別(即 T),則需要格外小心。考慮以下示例

  • Java

  • Kotlin

public class ClientFactoryBean<T extends AbstractClient> implements FactoryBean<T> {
	// ...
}
class ClientFactoryBean<T : AbstractClient> : FactoryBean<T> {
	// ...
}

具體客戶端宣告應為客戶端提供已解析的泛型,如以下示例所示

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

	@Bean
	public ClientFactoryBean<MyClient> myClient() {
		return new ClientFactoryBean<>(...);
	}

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myClient() = ClientFactoryBean<MyClient>(...)

}

如果以程式設計方式註冊 FactoryBean bean 定義,請確保遵循以下步驟

  1. 使用 RootBeanDefinition

  2. beanClass 設定為 FactoryBean 類,以便 AOT 知道它是一箇中間層。

  3. ResolvableType 設定為已解析的泛型,這確保暴露最精確的型別。

以下示例展示了一個基本定義

  • Java

  • Kotlin

RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);
val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java)
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java));
// ...
registry.registerBeanDefinition("myClient", beanDefinition)

JPA

必須事先知道 JPA 持久化單元,以便應用某些最佳化。考慮以下基本示例

  • Java

  • Kotlin

@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource) {
	LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
	factoryBean.setDataSource(dataSource);
	factoryBean.setPackagesToScan("com.example.app");
	return factoryBean;
}
@Bean
fun customDBEntityManagerFactory(dataSource: DataSource): LocalContainerEntityManagerFactoryBean {
	val factoryBean = LocalContainerEntityManagerFactoryBean()
	factoryBean.dataSource = dataSource
	factoryBean.setPackagesToScan("com.example.app")
	return factoryBean
}

為確保提前進行掃描,必須宣告一個 PersistenceManagedTypes bean 並由工廠 bean 定義使用,如以下示例所示

  • Java

  • Kotlin

@Bean
PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
	return new PersistenceManagedTypesScanner(resourceLoader)
			.scan("com.example.app");
}

@Bean
LocalContainerEntityManagerFactoryBean customDBEntityManagerFactory(DataSource dataSource, PersistenceManagedTypes managedTypes) {
	LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
	factoryBean.setDataSource(dataSource);
	factoryBean.setManagedTypes(managedTypes);
	return factoryBean;
}
@Bean
fun persistenceManagedTypes(resourceLoader: ResourceLoader): PersistenceManagedTypes {
	return PersistenceManagedTypesScanner(resourceLoader)
			.scan("com.example.app")
}

@Bean
fun customDBEntityManagerFactory(dataSource: DataSource, managedTypes: PersistenceManagedTypes): LocalContainerEntityManagerFactoryBean {
	val factoryBean = LocalContainerEntityManagerFactoryBean()
	factoryBean.dataSource = dataSource
	factoryBean.setManagedTypes(managedTypes)
	return factoryBean
}

執行時提示

與常規 JVM 執行時相比,將應用程式作為本地映象執行需要額外資訊。例如,GraalVM 需要事先知道元件是否使用了反射。類似地,除非明確指定,否則類路徑資源不會包含在本地映象中。因此,如果應用程式需要載入資源,必須從相應的 GraalVM 本地映象配置檔案中引用它。

RuntimeHints API 收集執行時對反射、資源載入、序列化和 JDK 代理的需求。以下示例確保在本地映象中,config/app.properties 可以在執行時從類路徑載入。

  • Java

  • Kotlin

runtimeHints.resources().registerPattern("config/app.properties");
runtimeHints.resources().registerPattern("config/app.properties")

在 AOT 處理期間會自動處理許多契約。例如,會檢查 @Controller 方法的返回型別,如果 Spring 檢測到該型別應被序列化(通常為 JSON),則會新增相關的反射提示。

對於核心容器無法推斷的情況,您可以以程式設計方式註冊此類提示。還提供了一些方便的註解用於常見用例。

@ImportRuntimeHints

RuntimeHintsRegistrar 實現允許您獲得 AOT 引擎管理的 RuntimeHints 例項的回撥。可以透過在任何 Spring bean 或 @Bean 工廠方法上使用 @ImportRuntimeHints 來註冊此介面的實現。RuntimeHintsRegistrar 實在構建時被檢測和呼叫。

import java.util.Locale;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

@Component
@ImportRuntimeHints(SpellCheckService.SpellCheckServiceRuntimeHints.class)
public class SpellCheckService {

	public void loadDictionary(Locale locale) {
		ClassPathResource resource = new ClassPathResource("dicts/" + locale.getLanguage() + ".txt");
		//...
	}

	static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {

		@Override
		public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
			hints.resources().registerPattern("dicts/*");
		}
	}

}

如果可能的話,@ImportRuntimeHints 應儘可能靠近需要提示的元件使用。這樣,如果該元件沒有貢獻到 BeanFactory 中,這些提示也不會被貢獻。

也可以透過在 META-INF/spring/aot.factories 中新增一個條目來靜態註冊實現,該條目的鍵等於 RuntimeHintsRegistrar 介面的完全限定名。

@Reflective

@Reflective 提供了一種慣用的方式來標記帶註解元素上對反射的需求。例如,@EventListener@Reflective 元註解,因為底層實現使用反射呼叫帶註解的方法。

開箱即用,只有 Spring bean 會被考慮,但您可以使用 @ReflectiveScan 選擇啟用掃描。在下面的示例中,將考慮 com.example.app 包及其子包的所有型別

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ReflectiveScan;

@Configuration
@ReflectiveScan("com.example.app")
public class MyConfiguration {
}

掃描在 AOT 處理期間進行,目標包中的型別不需要具有類級註解即可被考慮。這將執行“深度掃描”,並檢查型別、欄位、建構函式、方法和封閉元素上是否存在 @Reflective(直接或作為元註解)。

預設情況下,@Reflective 為帶註解的元素註冊一個呼叫提示。可以透過 @Reflective 註解指定自定義的 ReflectiveProcessor 實現來調整此行為。

庫作者可以出於自身目的重用此註解。此類自定義的示例在下一節中介紹。

@RegisterReflection

@RegisterReflection@Reflective 的一個特殊化,提供了一種宣告性方式來為任意型別註冊反射。

作為 @Reflective 的特殊化,如果您使用 @ReflectiveScan,它也會被檢測到。

在以下示例中,可以透過反射呼叫 AccountService 上的公共建構函式和公共方法。

@Configuration
@RegisterReflection(classes = AccountService.class, memberCategories =
		{ MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS })
class MyConfiguration {
}

@RegisterReflection 可以應用於類級別的任何目標型別,但也可以直接應用於方法,以便更好地指示實際需要提示的位置。

@RegisterReflection 可以用作元註解以提供更具體的需求。 @RegisterReflectionForBinding 就是這樣一種組合註解,它註冊了對任意型別進行序列化的需求。一個典型用例是使用容器無法推斷的 DTO,例如在方法體中使用 web 客戶端。

以下示例註冊 Order 用於序列化。

@Component
class OrderService {

	@RegisterReflectionForBinding(Order.class)
	public void process(Order order) {
		// ...
	}

}

這會為 Order 的建構函式、欄位、屬性和 record 元件註冊提示。還會為屬性和 record 元件上過渡使用的型別註冊提示。換句話說,如果 Order 暴露其他型別,也會為這些型別註冊提示。

測試執行時提示

Spring Core 還提供了 RuntimeHintsPredicates,這是一個用於檢查現有提示是否匹配特定用例的實用工具。可以在您自己的測試中使用它來驗證 RuntimeHintsRegistrar 包含預期的結果。我們可以為 SpellCheckService 編寫測試,並確保能夠在執行時載入字典。

	@Test
	void shouldRegisterResourceHints() {
		RuntimeHints hints = new RuntimeHints();
		new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
		assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
				.accepts(hints);
	}

使用 RuntimeHintsPredicates,我們可以檢查反射、資源、序列化或代理生成提示。這種方法適用於單元測試,但意味著元件的執行時行為是已知曉的。

透過使用 GraalVM tracing agent 執行應用程式的測試套件(或應用程式本身),可以瞭解更多關於應用程式的全域性執行時行為。該 agent 將記錄執行時所有需要 GraalVM 提示的相關呼叫,並將其寫入 JSON 配置檔案。

為了進行更有針對性的發現和測試,Spring Framework 提供了核心 AOT 測試實用工具的專用模組,"org.springframework:spring-core-test"。此模組包含 RuntimeHints Agent,它是一個 Java agent,用於記錄所有與執行時提示相關的方法呼叫,並幫助您斷言給定的 RuntimeHints 例項覆蓋了所有記錄的呼叫。讓我們考慮一個基礎設施片段,我們想測試在 AOT 處理階段貢獻的提示。

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.util.ClassUtils;

public class SampleReflection {

	private final Log logger = LogFactory.getLog(SampleReflection.class);

	public void performReflection() {
		try {
			Class<?> springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null);
			Method getVersion = ClassUtils.getMethod(springVersion, "getVersion");
			String version = (String) getVersion.invoke(null);
			logger.info("Spring version: " + version);
		}
		catch (Exception exc) {
			logger.error("reflection failed", exc);
		}
	}

}

然後我們可以編寫一個單元測試(無需本地編譯)來檢查我們貢獻的提示

import java.util.List;

import org.junit.jupiter.api.Test;

import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
import org.springframework.core.SpringVersion;

import static org.assertj.core.api.Assertions.assertThat;

// @EnabledIfRuntimeHintsAgent signals that the annotated test class or test
// method is only enabled if the RuntimeHintsAgent is loaded on the current JVM.
// It also tags tests with the "RuntimeHints" JUnit tag.
@EnabledIfRuntimeHintsAgent
class SampleReflectionRuntimeHintsTests {

	@Test
	void shouldRegisterReflectionHints() {
		RuntimeHints runtimeHints = new RuntimeHints();
		// Call a RuntimeHintsRegistrar that contributes hints like:
		runtimeHints.reflection().registerType(SpringVersion.class, typeHint ->
				typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));

		// Invoke the relevant piece of code we want to test within a recording lambda
		RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
			SampleReflection sample = new SampleReflection();
			sample.performReflection();
		});
		// assert that the recorded invocations are covered by the contributed hints
		assertThat(invocations).match(runtimeHints);
	}

}

如果您忘記貢獻提示,測試將失敗並提供有關呼叫的一些詳細資訊

org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection
INFO: Spring version: 6.2.0

Missing <"ReflectionHints"> for invocation <java.lang.Class#forName>
with arguments ["org.springframework.core.SpringVersion",
    false,
    jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7].
Stacktrace:
<"org.springframework.util.ClassUtils#forName, Line 284
io.spring.runtimehintstesting.SampleReflection#performReflection, Line 19
io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25

有多種方法可以在您的構建中配置此 Java agent,因此請參閱您的構建工具和測試執行外掛的文件。agent 本身可以配置為檢測特定包(預設情況下,僅檢測 org.springframework)。您將在 Spring Framework buildSrc README 檔案中找到更多詳細資訊。