預先 (AOT) 優化
本章涵蓋 Spring 的預先 (AOT) 優化。
關於特定於整合測試的 AOT 支援,請參閱測試的預先 (AOT) 支援。
預先 (AOT) 優化簡介
Spring 對於 AOT 優化的支援旨在建置時檢查 ApplicationContext
,並應用通常在執行時發生的決策和探索邏輯。這樣做可以建置更直接且專注於一組固定功能(主要基於類路徑和 Environment
)的應用程式啟動配置。
提早應用此類優化意味著以下限制
-
類路徑在建置時是固定的且完全定義的。
-
應用程式中定義的 Bean 無法在執行時變更,這表示
-
@Profile
,特別是 profile 特定的組態,需要在建置時選擇,並且在啟用 AOT 時會在執行時自動啟用。 -
影響 Bean 存在的
Environment
屬性 (@Conditional
) 僅在建置時考量。
-
-
具有實例供應器(lambda 或方法參考)的 Bean 定義無法預先 (ahead-of-time) 轉換。
-
以單例註冊的 Bean(使用
registerSingleton
,通常來自ConfigurableListableBeanFactory
)也無法預先 (ahead-of-time) 轉換。 -
由於我們無法依賴實例,請確保 Bean 類型盡可能精確。
另請參閱最佳實務章節。 |
當這些限制到位時,就有可能在建置時執行預先 (ahead-of-time) 處理並產生額外的資產。經過 Spring AOT 處理的應用程式通常會產生
-
Java 原始碼
-
位元組碼(通常用於動態 Proxy)
-
RuntimeHints
用於反射、資源載入、序列化和 JDK Proxy
目前,AOT 的重點是允許 Spring 應用程式使用 GraalVM 部署為原生映像檔。我們計劃在未來世代支援更多基於 JVM 的使用案例。 |
AOT 引擎總覽
用於處理 ApplicationContext
的 AOT 引擎的進入點是 ApplicationContextAotGenerator
。它根據 GenericApplicationContext
(代表要優化的應用程式)和 GenerationContext
負責以下步驟
-
重新整理用於 AOT 處理的
ApplicationContext
。與傳統重新整理相反,此版本僅建立 Bean 定義,而非 Bean 實例。 -
調用可用的
BeanFactoryInitializationAotProcessor
實作,並將其貢獻應用於GenerationContext
。例如,核心實作會迭代所有候選 Bean 定義,並產生必要的程式碼來還原BeanFactory
的狀態。
一旦此過程完成,GenerationContext
將已更新為應用程式執行所需的產生程式碼、資源和類別。RuntimeHints
實例也可用於產生相關的 GraalVM 原生映像檔組態檔。
ApplicationContextAotGenerator#processAheadOfTime
傳回 ApplicationContextInitializer
進入點的類別名稱,該進入點允許使用 AOT 優化啟動 Context。
以下章節將更詳細地介紹這些步驟。
用於 AOT 處理的重新整理
所有 GenericApplicationContext
實作都支援用於 AOT 處理的重新整理。應用程式 Context 是使用任意數量的進入點建立的,通常以 @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 定義以提取額外的設定,例如init
和destroy
方法。 -
SmartInstantiationAwareBeanPostProcessor
實作在必要時判斷更精確的 Bean 類型。這確保建立執行時所需的任何 Proxy。
一旦此部分完成,BeanFactory
就會包含應用程式執行所需的 Bean 定義。它不會觸發 Bean 實例化,但允許 AOT 引擎檢查執行時將建立的 Bean。
Bean Factory 初始化 AOT 貢獻
想要參與此步驟的組件可以實作 BeanFactoryInitializationAotProcessor
介面。每個實作都可以根據 Bean Factory 的狀態傳回 AOT 貢獻。
AOT 貢獻是一個組件,它貢獻產生的程式碼,該程式碼重現特定的行為。它也可以貢獻 RuntimeHints
以指示對反射、資源載入、序列化或 JDK Proxy 的需求。
可以在 META-INF/spring/aot.factories
中註冊 BeanFactoryInitializationAotProcessor
實作,其索引鍵等於介面的完整限定名稱。
BeanFactoryInitializationAotProcessor
介面也可以由 Bean 直接實作。在此模式下,Bean 提供與常規執行時間提供的功能等效的 AOT 貢獻。因此,此類 Bean 會自動從 AOT 優化 Context 中排除。
如果 Bean 實作 |
Bean 註冊 AOT 貢獻
核心 BeanFactoryInitializationAotProcessor
實作負責收集每個候選 BeanDefinition
的必要貢獻。它使用專用的 BeanRegistrationAotProcessor
來執行此操作。
此介面的使用方式如下
-
由
BeanPostProcessor
Bean 實作,以取代其執行時行為。例如,AutowiredAnnotationBeanPostProcessor
實作此介面以產生程式碼,該程式碼會注入使用@Autowired
註解的成員。 -
由在
META-INF/spring/aot.factories
中註冊的類型實作,其索引鍵等於介面的完整限定名稱。通常在需要針對核心框架的特定功能調整 Bean 定義時使用。
如果 Bean 實作 |
如果沒有 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 類別名稱。 |
由於此類別上沒有任何特定條件,因此 dataSourceConfiguration
和 dataSource
被識別為候選者。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 定義,但以直接方式且盡可能不使用反射。dataSourceConfiguration
有一個 Bean 定義,dataSourceBean
也有一個 Bean 定義。當需要 datasource
實例時,會調用 BeanInstanceSupplier
。此供應器調用 dataSource()
Bean 上的 dataSourceConfiguration
方法。
使用 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。例如,類路徑掃描在原生映像檔中不起作用,因為沒有類路徑的概念。對於這種情況,掃描在建置時發生至關重要。
公開最精確的 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
。 常規後處理都不會考慮 MyImplementation
。例如,如果 MyImplementation
上有一個 Context 應註冊的註解處理器方法,則不會預先偵測到它。
上面的範例應重寫如下
-
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 定義,請考慮使用 RootBeanBefinition
,因為它允許指定處理泛型的 ResolvableType
。
避免多個建構子
容器能夠根據多個候選者選擇要使用的最合適的建構子。但是,這不是最佳實務,如果需要,最好使用 @Autowired
標記慣用的建構子。
如果您正在處理無法修改的程式碼庫,則可以在相關的 Bean 定義上設定 preferredConstructors
屬性,以指示應使用哪個建構子。
避免將複雜的資料結構用於建構子參數和屬性
以程式設計方式製作 RootBeanDefinition
時,您在可以使用的類型方面不受限制。例如,您可能有一個自訂 record
,其中包含 Bean 作為建構子引數的多個屬性。
雖然這在常規執行時間下可以正常運作,但 AOT 不知道如何產生自訂資料結構的程式碼。一個好的經驗法則是記住 Bean 定義是多個模型之上的抽象概念。建議不要使用此類結構,而是分解為簡單類型或參考如此建置的 Bean。
作為最後的手段,您可以實作自己的 org.springframework.aot.generate.ValueCodeGenerator$Delegate
。若要使用它,請使用 Delegate
作為索引鍵,在 META-INF/spring/aot.factories
中註冊其完整限定名稱。
避免使用自訂引數建立 Bean
Spring AOT 偵測到建立 Bean 需要執行的操作,並使用實例供應器將其轉換為產生的程式碼。容器也支援使用 自訂引數 建立 Bean,這會導致 AOT 出現多個問題
-
自訂引數需要動態內省符合的建構子或工廠方法。AOT 無法偵測到這些引數,因此必須手動提供必要的反射提示。
-
繞過實例供應器意味著也將跳過建立之後的所有其他優化。例如,將跳過欄位和方法上的自動裝配,因為它們在實例供應器中處理。
我們建議使用手動工廠模式,其中 Bean 負責建立實例,而不是使用自訂引數建立原型作用域的 Bean。
避免循環相依性
某些使用案例可能會導致一個或多個 Bean 之間出現循環相依性。使用常規執行時間,可能會透過 Setter 方法或欄位上的 @Autowired
連接這些循環相依性。但是,經過 AOT 優化的 Context 將無法啟動,並出現明確的循環相依性。
在 AOT 優化的應用程式中,您應盡可能避免循環相依性。如果無法避免,您可以使用 @Lazy
注入點或 ObjectProvider
來延遲存取或檢索必要的協作 Bean。請參閱此提示以獲取更多資訊。
FactoryBean
應謹慎使用 FactoryBean
,因為它在 Bean 類型解析方面引入了一個中間層,這在概念上可能不是必要的。作為經驗法則,如果 FactoryBean
實例不持有長期狀態,並且在運行時的後續時間點不需要,則應將其替換為常規的 factory 方法,並可能在頂部使用 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 定義,請務必遵循以下步驟
-
使用
RootBeanDefinition
。 -
將
beanClass
設定為FactoryBean
類別,以便 AOT 知道它是一個中間層。 -
將
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,並由 factory 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 上的 @ImportRuntimeHints
或 @Bean
factory 方法註冊此介面的實作。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
就是這樣一個組合註解,它註冊了序列化任意類型的需求。一個典型的用例是在方法體內使用 Web 客戶端時使用 DTO。
以下範例註冊了 Order
以進行序列化。
@Component
class OrderService {
@RegisterReflectionForBinding(Order.class)
public void process(Order order) {
// ...
}
}
這會為 Order
的建構子、欄位、屬性和記錄組件註冊提示。也會為屬性和記錄組件上遞迴使用的類型註冊提示。換句話說,如果 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 追蹤代理程式運行其測試套件(或應用程式本身)來了解應用程式的整體運行時行為。此代理程式將記錄運行時需要 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.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 代理程式,因此請參閱您的建置工具和測試執行外掛程式的文件。可以組態代理程式本身來檢測特定套件(預設情況下,僅檢測 org.springframework
)。您可以在 Spring Framework buildSrc
README 檔案中找到更多詳細資訊。