預先最佳化

本章涵蓋 Spring 的提前(AOT)最佳化。

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

提前最佳化簡介

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

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

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

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

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

    • 影響 Bean 存在的 Environment 屬性(@Conditional)僅在構建時考慮。

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

  • 註冊為單例的 Bean(使用 registerSingleton,通常來自 ConfigurableListableBeanFactory)也無法提前轉換。

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

另請參閱 最佳實踐 部分。

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

  • Java 原始碼

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

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

目前,AOT 專注於允許 Spring 應用程式使用 GraalVM 部署為本機映像。我們打算在未來支援更多基於 JVM 的用例。

AOT 引擎概述

AOT 引擎處理 ApplicationContext 的入口點是 ApplicationContextAotGenerator。它根據表示要最佳化的應用程式的 GenericApplicationContextGenerationContext 執行以下步驟

  • 重新整理 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 實現 照常呼叫。這包括配置類解析、匯入選擇器、類路徑掃描等。這些步驟確保 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 工廠初始化 AOT 貢獻

希望參與此步驟的元件可以實現 BeanFactoryInitializationAotProcessor 介面。每個實現都可以根據 Bean 工廠的狀態返回一個 AOT 貢獻。

AOT 貢獻是一個元件,它貢獻生成的程式碼以重現特定行為。它還可以貢獻 RuntimeHints 以指示需要反射、資源載入、序列化或 JDK 代理。

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

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

如果一個 Bean 實現了 BeanFactoryInitializationAotProcessor 介面,那麼該 Bean 及其所有依賴項將在 AOT 處理期間初始化。我們通常建議此介面僅由基礎設施 Bean 實現,例如 BeanFactoryPostProcessor,這些 Bean 的依賴項有限,並且已在 Bean 工廠生命週期的早期初始化。如果此類 Bean 是使用 @Bean 工廠方法註冊的,請確保該方法是 static,以便其封閉的 @Configuration 類不必初始化。

Bean 註冊 AOT 貢獻

核心 BeanFactoryInitializationAotProcessor 實現負責收集每個候選 BeanDefinition 所需的貢獻。它透過專用的 BeanRegistrationAotProcessor 來完成此操作。

此介面的使用方式如下

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

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

如果一個 Bean 實現了 BeanRegistrationAotProcessor 介面,那麼該 Bean 及其所有依賴項將在 AOT 處理期間初始化。我們通常建議此介面僅由基礎設施 Bean 實現,例如 BeanFactoryPostProcessor,這些 Bean 的依賴項有限,並且已在 Bean 工廠生命週期的早期初始化。如果此類 Bean 是使用 @Bean 工廠方法註冊的,請確保該方法是 static,以便其封閉的 @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 註解,以便在需要排除它們時進行識別,例如透過靜態分析工具。

上面生成的程式碼建立了等效於 @Configuration 類的 Bean 定義,但以直接的方式,並且如果可能的話根本不使用反射。dataSourceConfigurationdataSourceBean 各有一個 Bean 定義。當需要 datasource 例項時,會呼叫 BeanInstanceSupplier。此供應商會在 dataSourceConfiguration Bean 上呼叫 dataSource() 方法。

使用 AOT 最佳化執行

AOT 是將 Spring 應用程式轉換為本機可執行檔案的強制步驟,因此在本機映像中執行時會自動啟用。但是,也可以透過將 spring.aot.enabled 系統屬性設定為 true 來在 JVM 上使用 AOT 最佳化。

當包含 AOT 最佳化時,在構建時做出的一些決策會在應用程式設定中硬編碼。例如,在構建時啟用的配置檔案在執行時也會自動啟用。

最佳實踐

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

本節列出了確保您的應用程式已準備好進行 AOT 的最佳實踐。

程式化 Bean 註冊

AOT 引擎負責 @Configuration 模型以及在處理配置時可能呼叫的任何回撥。如果您需要以程式設計方式註冊其他 Bean,請務必使用 BeanDefinitionRegistry 來註冊 Bean 定義。

這通常可以透過 BeanDefinitionRegistryPostProcessor 完成。請注意,如果它本身註冊為一個 Bean,那麼在執行時它將再次被呼叫,除非您也確保實現 BeanFactoryInitializationAotProcessor。更慣用的方法是實現 ImportBeanDefinitionRegistrar,並使用 @Import 在您的一個配置類上註冊它。這會在配置類解析期間呼叫您的自定義程式碼。

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

公開最精確的 Bean 型別

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

對於 @Configuration 類,請確保 @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()

}

在上面的示例中,myInterface bean 的宣告型別是 MyInterface。在 AOT 處理期間,通常的後處理都不會考慮 MyImplementation。例如,如果 MyImplementation 上有一個帶註解的處理方法,並且上下文應該註冊它,那麼在 AOT 處理期間將不會檢測到它。

因此,上述示例應重寫如下

  • Java

  • Kotlin

@Configuration(proxyBeanMethods = false)
public class UserConfiguration {

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

}
@Configuration(proxyBeanMethods = false)
class UserConfiguration {

	@Bean
	fun myInterface() = MyImplementation()

}

如果您以程式設計方式註冊 Bean 定義,請考慮使用 RootBeanDefinition,因為它允許指定處理泛型的 ResolvableType

避免多個建構函式

容器能夠根據多個候選建構函式選擇最合適的建構函式。但是,依賴此並非最佳實踐,如果需要,最好使用 @Autowired 標記首選建構函式。

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

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

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

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

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

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

Spring AOT 檢測建立 Bean 所需的操作,並將其轉換為使用例項供應商的生成程式碼。容器還支援使用 自定義引數 建立 Bean,這可能導致 AOT 出現幾個問題

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

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

我們建議採用手動工廠模式,其中 Bean 負責例項的建立,而不是使用自定義引數建立原型作用域的 Bean。

避免迴圈依賴

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

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

FactoryBean

應謹慎使用 FactoryBean,因為它在 Bean 型別解析方面引入了一箇中間層,這在概念上可能不是必需的。根據經驗,如果 FactoryBean 例項不持有長期狀態,並且在執行時不需要在以後使用,則應將其替換為常規的 @Bean 工廠方法,可能在其之上新增一個 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
}

為了確保實體掃描提前發生,必須宣告並由工廠 Bean 定義使用 PersistenceManagedTypes 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,也會檢測到 @RegisterReflection

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

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

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

@RegisterReflection 可以用作元註解以支援更具體的需求。@RegisterReflectionForBinding 是一個複合註解,它使用 @RegisterReflection 進行元註解,並註冊任意型別序列化的需求。一個典型的用例是容器無法推斷的 DTO 的使用,例如在方法體中使用 Web 客戶端。

以下示例註冊 Order 以進行序列化。

@Component
class OrderService {

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

}

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

基於約定的轉換的執行時提示

儘管核心容器提供了對許多常見型別的自動轉換的內建支援(請參閱 Spring 型別轉換),但某些轉換是透過依賴反射的基於約定的演算法支援的。

具體來說,如果沒有為特定源 → 目標型別對在 ConversionService 中註冊顯式 Converter,則內部 ObjectToObjectConverter 將嘗試使用約定將源物件轉換為目標型別,方法是委託給源物件上的方法或目標型別上的靜態工廠方法或建構函式。由於此基於約定的演算法可以在執行時應用於任意型別,因此核心容器無法推斷支援此類反射所需的執行時提示。

如果您遇到本機映像中由於缺少執行時提示而導致的基於約定的轉換問題,您可以以程式設計方式註冊必要的提示。例如,如果您的應用程式需要從 java.time.Instant 轉換為 java.sql.Timestamp 並依賴 ObjectToObjectConverter 使用反射呼叫 java.sql.Timestamp.from(Instant),您可以實現自定義 RuntimeHintsRegitrar 以支援本機映像中的此用例,如以下示例所示。

  • Java

public class TimestampConversionRuntimeHints implements RuntimeHintsRegistrar {

	public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
		ReflectionHints reflectionHints = hints.reflection();

		reflectionHints.registerTypeIfPresent(classLoader, "java.sql.Timestamp", hint -> hint
				.withMethod("from", List.of(TypeReference.of(Instant.class)), ExecutableMode.INVOKE)
				.onReachableType(TypeReference.of("java.sql.Timestamp")));
	}
}

然後可以透過 @ImportRuntimeHints 宣告性地註冊 TimestampConversionRuntimeHints,或者透過 META-INF/spring/aot.factories 配置檔案靜態註冊。

上面的 TimestampConversionRuntimeHints 類是框架中包含的 ObjectToObjectConverterRuntimeHints 類的簡化版本,並且預設註冊。

因此,框架已經處理了這種特定的 InstantTimestamp 用例。

測試執行時提示

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 跟蹤代理 執行應用程式的測試套件(或應用程式本身)來了解應用程式的全域性執行時行為。此代理將記錄所有需要 GraalVM 提示的相關呼叫,並將其作為 JSON 配置檔案寫入。

為了更具針對性的發現和測試,Spring Framework 附帶了一個專用模組,其中包含核心 AOT 測試實用程式,即 "org.springframework:spring-core-test"。此模組包含 RuntimeHints Agent,這是一個 Java 代理,它記錄所有與執行時提示相關的方法呼叫,並幫助您斷言給定的 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.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 = org.springframework.aot.test.agent.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 代理,因此請參閱您的構建工具和測試執行外掛的文件。代理本身可以配置為檢測特定包(預設情況下,只檢測 org.springframework)。您將在 Spring Framework buildSrc README 檔案中找到更多詳細資訊。

© . This site is unofficial and not affiliated with VMware.