在 Spring 中,@Configuration
、@ComponentScan
、@Bean
、@Import
等注解的掃描、解析和 BeanDefinition
注冊是一個分層處理的過程。下面我們以 @Configuration
類為例,結合代碼流程詳細說明其從掃描到注冊的完整邏輯。
1. 整體流程概覽
以下是核心步驟的流程圖:
1. 掃描候選配置類 → 2. 解析注解元數據 → 3. 注冊 BeanDefinition
具體分為以下階段:
- 掃描階段:通過
BeanDefinitionRegistry
獲取所有候選配置類。 - 解析階段:使用
ConfigurationClassParser
解析注解(如@ComponentScan
、@Bean
、@Import
)。 - 注冊階段:通過
ConfigurationClassBeanDefinitionReader
將解析結果注冊為BeanDefinition
。
2. 詳細步驟解析
2.1 掃描階段:識別候選配置類
觸發入口:
ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()
邏輯:
- 從
BeanDefinitionRegistry
獲取所有已注冊的BeanDefinition
名稱:String[] beanNames = registry.getBeanDefinitionNames();
- 遍歷這些名稱,檢查對應的
BeanDefinition
是否是候選配置類:- 條件:類上有
@Configuration
、@Component
、@ComponentScan
、@Import
、@ImportResource
,或類中有@Bean
方法。 - 判斷邏輯:
if (isFullConfigurationCandidate(beanDef) || isLiteConfigurationCandidate(beanDef)) {configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); }
isFullConfigurationCandidate(beanDef)
:檢查是否有@Configuration
注解。isLiteConfigurationCandidate(beanDef)
:檢查是否有其他相關注解(如@Component
、@Bean
方法)。
- 條件:類上有
關鍵點:
- 掃描的輸入是已注冊的
BeanDefinition
(可能來自 XML、Java Config 或自動掃描)。 - 此時尚未解析注解內容,僅識別出需要進一步處理的候選類。
2.2 解析階段:處理注解元數據
核心類:ConfigurationClassParser
入口方法:parse()
邏輯:遞歸解析每個候選配置類的注解。
(1) 解析 @ComponentScan
- 作用:掃描指定包路徑下的
@Component
類(如@Service
、@Repository
)。 - 流程:
- 獲取
@ComponentScan
注解的basePackages
或basePackageClasses
。 - 使用
ClassPathBeanDefinitionScanner
掃描類路徑:scanner.scan(basePackages);
- 掃描到的類會被注冊為新的
BeanDefinition
(類型為ScannedGenericBeanDefinition
)。
- 獲取
- 關鍵點:
- 掃描時使用
ASM
或反射讀取類注解,避免提前加載類到 JVM。 - 新注冊的
BeanDefinition
可能也會被后續解析(如果它們也是配置類)。
- 掃描時使用
(2) 解析 @Bean
方法
- 作用:將配置類中的
@Bean
方法轉換為BeanDefinition
。 - 流程:
- 遍歷配置類中的所有方法,篩選帶
@Bean
注解的方法。 - 為每個
@Bean
方法生成一個BeanDefinition
:- 類型:
ConfigurationClassBeanDefinition
。 - 工廠方法:設置為
@Bean
方法(通過factoryMethodName
和factoryBeanName
指定)。 - 依賴:解析
@Bean
方法的參數(按類型或@Qualifier
注入)。
- 類型:
- 遍歷配置類中的所有方法,篩選帶
- 示例:
@Configuration public class AppConfig {@Beanpublic DataSource dataSource() {return new HikariDataSource();} }
- 生成的
BeanDefinition
會記錄:factoryBeanName=appConfig
,factoryMethodName=dataSource
。
- 生成的
(3) 解析 @Import
- 作用:動態導入其他配置類或
BeanDefinition
。 - 三種處理方式:
- 普通類:直接注冊為
BeanDefinition
。@Import(OtherConfig.class)
ImportSelector
:通過編程方式選擇要導入的類。@Import(MyImportSelector.class)
MyImportSelector
實現selectImports()
方法,返回要導入的類名數組。
ImportBeanDefinitionRegistrar
:直接注冊BeanDefinition
。@Import(MyRegistrar.class)
MyRegistrar
實現registerBeanDefinitions()
方法,手動操作BeanDefinitionRegistry
。
- 普通類:直接注冊為
(4) 處理父類與接口
- 遞歸檢查配置類的父類和接口,確保不遺漏任何
@Bean
方法或元注解。
2.3 注冊階段:加載 BeanDefinition
核心類:ConfigurationClassBeanDefinitionReader
入口方法:loadBeanDefinitions()
邏輯:將解析結果(ConfigurationClass
對象)轉換為 BeanDefinition
并注冊到容器。
(1) 注冊 @Import
的類
- 普通類:通過
registry.registerBeanDefinition()
直接注冊。 ImportBeanDefinitionRegistrar
:調用其registerBeanDefinitions()
方法。
(2) 注冊 @Bean
方法
- 為每個
@Bean
方法生成BeanDefinition
并注冊:for (BeanMethod beanMethod : configClass.getBeanMethods()) {loadBeanDefinitionsForBeanMethod(beanMethod); }
(3) 處理嵌套配置類
- 如果配置類內部有
@Configuration
靜態嵌套類,遞歸處理。
3. 關鍵設計點
(1) 延遲加載與遞歸處理
- 延遲加載:
@ComponentScan
掃描到的類可能也是配置類,需要遞歸解析。 - 循環依賴處理:Spring 通過提前暴露
BeanDefinition
解決配置類之間的循環引用。
(2) 元數據存儲
ConfigurationClass
對象存儲解析后的中間結果(如@Bean
方法、@Import
類等)。BeanDefinition
的attribute
字段存儲配置類的元信息(如@Lazy
、@Primary
)。
(3) 性能優化
- ASM 字節碼分析:在掃描階段避免加載類到 JVM。
- 緩存:解析結果緩存到
ConfigurationClass
中,避免重復處理。
4. 示例全流程
場景
@Configuration
@ComponentScan("com.example.service")
@Import(OtherConfig.class)
public class AppConfig {@Beanpublic DataSource dataSource() {return new HikariDataSource();}
}
步驟
- 掃描階段:
- 發現
AppConfig
是候選配置類(有@Configuration
)。
- 發現
- 解析階段:
- 解析
@ComponentScan
:掃描com.example.service
包,注冊@Service
類。 - 解析
@Import(OtherConfig.class)
:遞歸處理OtherConfig
。 - 解析
@Bean dataSource()
:生成工廠方法BeanDefinition
。
- 解析
- 注冊階段:
- 注冊
OtherConfig
及其@Bean
方法。 - 注冊
dataSource
的BeanDefinition
。
- 注冊
5. 總結
Spring 對配置類注解的處理是一個分層遞歸的過程:
- 掃描:通過
BeanDefinitionRegistry
篩選候選類。 - 解析:
ConfigurationClassParser
解析注解并生成中間結果(ConfigurationClass
)。 - 注冊:
ConfigurationClassBeanDefinitionReader
將解析結果轉換為BeanDefinition
。
這種設計將注解元數據解析與 BeanDefinition
注冊分離,確保了靈活性和擴展性(如支持動態 ImportSelector
)。同時,遞歸處理和緩存機制解決了復雜依賴和性能問題。