提前最佳化
本章介紹 Spring 的提前 (AOT) 最佳化。
有關整合測試特定的 AOT 支援,請參閱 測試的提前支援。
提前最佳化介紹
Spring 對 AOT 最佳化的支援旨在在構建時檢查 ApplicationContext
,並應用通常在執行時發生的決策和發現邏輯。這樣做可以構建更直接的應用程式啟動安排,主要基於類路徑和環境,專注於固定的一組功能。
提早應用此類最佳化意味著以下限制
-
類路徑在構建時是固定的且完全定義。
-
應用程式中定義的 bean 在執行時不能改變,這意味著
-
@Profile
,特別是針對特定配置檔案的配置,需要在構建時選擇,並在啟用 AOT 時在執行時自動啟用。 -
影響 bean 是否存在的
Environment
屬性 (@Conditional
) 只在構建時考慮。
-
-
帶有例項供應商(lambda 或方法引用)的 Bean 定義無法提前轉換。
-
註冊為單例(通常使用
ConfigurableListableBeanFactory
的registerSingleton
)的 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 定義,以提取額外設定,例如init
和destroy
方法。 -
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 實現了 |
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 進行註解,以便在需要排除它們時(例如由靜態分析工具)進行識別。 |
上面生成的程式碼建立的 bean 定義等同於 @Configuration
類,但採用直接方式,並且儘可能不使用反射。有一個用於 dataSourceConfiguration
的 bean 定義,還有一個用於 dataSourceBean
。
當需要 datasource 例項時,會呼叫 BeanInstanceSupplier
。此 supplier 會呼叫 dataSource()
方法在 dataSourceConfiguration
bean 上。
當包含 AOT 最佳化時,一些在構建時做出的決策會硬編碼到應用程式設定中。
例如,在構建時啟用的 profile 在執行時也會自動啟用。
AOT 引擎旨在處理儘可能多的用例,而無需更改應用程式程式碼。但是,請記住,一些最佳化是在構建時基於 bean 的靜態定義進行的。
本節列出了確保應用程式為 AOT 做好準備的最佳實踐。
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 實現的介面互動,但宣告最精確的型別仍然非常重要。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 出現一些問題。
-
自定義引數需要對匹配的建構函式或工廠方法進行動態自省。AOT 無法檢測到這些引數,因此必須手動提供必要的反射提示。
-
繞過例項 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 定義,請確保遵循以下步驟
-
使用
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 並由工廠 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 檔案中找到更多詳細資訊。