第七節 ConfigurationClassParser 源碼分析

tips: ConfigurationClassParser 是 Springframework 中的重要類。

本章主要是源碼理解,有難度和深度,也枯燥乏味,可以根據實際情況選擇閱讀。

位置:org.springframework.context.annotation.ConfigurationClassParser

ConfigurationClassParser 它是解密 configuration 的關鍵。理解 ConfigurationClassParser 對理解整個 Spring 框架至關重要。

一、作用是什么

ConfigurationClassParser是一個非常重要的類,它主要用于解析帶有@Configuration注解的類。

@Configuration注解表明該類用作配置類,其中可以定義bean和Spring容器應如何初始化和管理這些bean。

ConfigurationClassParser的作用可以從以下幾個方面詳細闡述:

  1. 解析導入的配置@Import注解允許一個配置類導入另一個配置類。ConfigurationClassParser解析這些@Import注解,確保所有導入的配置也被處理和應用。
  2. 處理屬性注入:通過@PropertySource注解,可以指定一些屬性文件,這些屬性文件中的屬性可以被注入到Spring管理的bean中。ConfigurationClassParser負責解析這些注解,并確保屬性文件被加載且其值可用于注入。
  3. 處理@Conditional注解: Spring框架允許在bean的注冊過程中使用條件邏輯,@Conditional注解及其派生注解(例如@ConditionalOnClass@ConditionalOnProperty等)使得只有在滿足特定條件時,才會進行bean的注冊。ConfigurationClassParser負責解析這些條件注解并應用其邏輯。
  4. processDeferredImportSelectors#processImports 處理擴展配置( Starter 能夠被處理的核心分支)

二、觸發時機

SpringBoot 應用啟動過程中,通過后置處理器去觸發 ConfigurationClassPostProcessor。 然后再調用 ConfigurationClassParser類解析

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {....
}

處理如下:

下面我們將詳細分析源碼流程。

三、ConfigurationClassPostProcessor

下面是 ConfigurationClassPostProcessor 部分核心代碼。

入口方法 processConfigBeanDefinitions(BeanDefinitionRegistry registry) 。開始分析這段代碼。

注意這里有一個 do...while

	do {.....}while (!candidates.isEmpty());

它將逐一識別和解析配置類,然后將配置類中定義的Bean注冊到Spring容器中。這個過程通過不斷循環直到沒有新的配置類候選者出現為止,確保了所有相關的配置都被完整地處理。

// 創建一個配置類解析器,用于解析和處理配置類信息
ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment,this.resourceLoader, this.componentScanBeanNameGenerator, registry);// 初始化一個集合,用于存儲待處理的配置類候選者
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
// 初始化一個集合,用于跟蹤已經解析過的配置類,以避免重復解析
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());// 循環處理,直到沒有新的配置類候選者
do {// 解析當前候選者中的配置類parser.parse(candidates);// 對解析結果進行驗證parser.validate();// 從解析器中獲取已解析的配置類集合Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());// 移除已經處理過的,避免重復處理configClasses.removeAll(alreadyParsed);// 如果讀取器未初始化,創建一個配置類Bean定義讀取器if (this.reader == null) {this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment,this.importBeanNameGenerator, parser.getImportRegistry());}// 加載并注冊配置類中定義的Beanthis.reader.loadBeanDefinitions(configClasses);// 將這批配置類標記為“已解析”alreadyParsed.addAll(configClasses);// 清空候選者集合,為下一輪尋找新候選者做準備candidates.clear();// 檢查是否有新的Bean定義被注冊(可能由@Configuration類引入)if (registry.getBeanDefinitionCount() > candidateNames.length) {// 重新獲取所有Bean定義的名稱String[] newCandidateNames = registry.getBeanDefinitionNames();// 創建一個舊候選名稱的集合,用于辨識新的候選者Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));// 創建一個集合,用于跟蹤已經解析的配置類的類名Set<String> alreadyParsedClasses = new HashSet<>();for (ConfigurationClass configurationClass : alreadyParsed) {alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());}// 遍歷新的Bean定義名稱,尋找新的配置類候選者for (String candidateName : newCandidateNames) {if (!oldCandidateNames.contains(candidateName)) {BeanDefinition bd = registry.getBeanDefinition(candidateName);if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&!alreadyParsedClasses.contains(bd.getBeanClassName())) {candidates.add(new BeanDefinitionHolder(bd, candidateName));}}}// 更新候選名稱列表,以反映新的Bean定義candidateNames = newCandidateNames;}
// 如果還有未處理的候選者,繼續循環
} while (!candidates.isEmpty());

特別說明,對于 Bean 的加載和實例化不在本范圍了,不進行講解。感興趣可以閱讀相關章節。

上面的這段代碼是 ConfigurationClassPostProcessor 核心。解析來的重頭戲。ConfigurationClassParser

四、ConfigurationClassParser

從這行代碼開始入手parser.parse(candidates);

  1. parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
  2. processDeferredImportSelectors(); // 處理前面推遲的ImportSelector,一些二方包的導入類,將在這個方法中實現。 例如,我們配置在 Starter Spring.factories 中的自動導入類,將在這一環境被加載
parse方法入口
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {// 如果這個配置類應該根據條件注解被跳過,則直接返回不進行處理if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {return;}// 嘗試從已處理的配置類映射中獲取這個配置類ConfigurationClass existingClass = this.configurationClasses.get(configClass);// 如果找到了已存在的配置類if (existingClass != null) {// 如果當前處理的是一個導入的配置類if (configClass.isImported()) {// 如果已存在的配置類也是導入的,則合并導入來源if (existingClass.isImported()) {existingClass.mergeImportedBy(configClass);}// 如果已存在的配置類不是導入的,則忽略當前導入的配置類,保留現有的非導入類return;}else {// 如果找到顯式的bean定義,可能是意在替換一個導入的類。// 移除舊的配置類,采用新的配置類。this.configurationClasses.remove(configClass);this.knownSuperclasses.values().removeIf(configClass::equals);}}// 遞歸處理配置類及其超類層次結構SourceClass sourceClass = asSourceClass(configClass);do {// 處理當前配置類并更新sourceClass為配置類的超類,準備下一輪處理sourceClass = doProcessConfigurationClass(configClass, sourceClass);} while (sourceClass != null); // 如果sourceClass為null,表示超類已經處理完畢// 將處理完的配置類放入配置類映射中,標記為已處理this.configurationClasses.put(configClass, configClass);
}

上面可以理解,解析 MyApplication 類所在工程中的類。最終的解析由 doProcessConfigurationClass 實現

processDeferredImportSelectors

負責處理那些被延遲的特殊接口,使用它來按需動態地導入配置。

這些常常依賴于某些條件才被執行,所以被延遲處理。

// 定義處理延遲的ImportSelector的方法
private void processDeferredImportSelectors() {// 獲取之前收集的所有延遲處理的ImportSelectorList<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;// 將引用置為null,表示開始處理過程,防止重復處理this.deferredImportSelectors = null;// 如果沒有需要處理的延遲ImportSelector,則直接返回if (deferredImports == null) {return;}...... // 遍歷所有的延遲ImportSelectorfor (DeferredImportSelectorHolder deferredImport : deferredImports) {// 獲取與當前ImportSelector相關聯的配置類ConfigurationClass configClass = deferredImport.getConfigurationClass();try {// 調用ImportSelector的selectImports方法,獲取所有的導入類名String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());// 處理這些導入的類,將它們作為配置類進行進一步的處理processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);}}
}

這個方法的主要作用將是找出符合條件的 imports 類。最終還是由processImports() 處理。

到這里,spring.factories 符合條件的一些類將被加載。核心代碼deferredImport.getImportSelector().selectImports(configClass.getMetadata());

到這里我們基本上了解了 Starter 是如何被引入進來的。

真正解析的方法 doProcessConfigurationClass

doProcessConfigurationClass

它遞歸地處理嵌套類、處理@PropertySource注解、處理@ComponentScan注解、處理@Import注解、處理@ImportResource注解、處理@Bean方法、處理接口上的默認方法,最后處理父類

  1. 處理成員類:方法首先遞歸地處理配置類中定義的任何成員類(嵌套類)。
  2. 處理@PropertySource注解:然后遍歷配置類上的所有@PropertySource注解,這些注解用來指明屬性文件的位置。如果當前的環境實現了ConfigurableEnvironment接口,則處理注解指定的屬性源
  3. 處理@ComponentScan注解:接下來,處理配置類上的所有@ComponentScan注解,這些注解指示Spring掃描特定包下的組件(即帶有@Component@Service等注解的類),并注冊為Spring容器中的Bean。如果有條件注解指示在此階段跳過處理,則不執行掃描。
  4. 處理@Import注解:處理配置類上的@Import注解,這些注解用來導入其他配置類或配置選擇器,允許模塊化地組織配置。
  5. 處理@ImportResource注解:處理配置類上的@ImportResource注解,這些注解用于導入XML配置文件。
  6. 處理@Bean方法:收集配置類中所有帶有@Bean注解的方法的元數據,并將它們添加到配置類對象中。這些方法定義了應該由Spring容器管理的Bean。
  7. 處理接口上的默認方法:如果配置類實現了接口,并在這些接口上定義了默認方法,這些方法也會被處理。
  8. 處理父類:最后,如果配置類有超類,那么這個方法會檢查超類是否也是一個配置類,不是Java內置類,并且還沒有被處理過。如果滿足條件,則遞歸地處理這個超類。

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)throws IOException {// 首先,遞歸處理任何成員類(嵌套類)processMemberClasses(configClass, sourceClass);// 處理所有的@PropertySource注解for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), PropertySources.class,org.springframework.context.annotation.PropertySource.class)) {// 如果環境實現了ConfigurableEnvironment接口if (this.environment instanceof ConfigurableEnvironment) {// 處理@PropertySource注解processPropertySource(propertySource);} else {// 如果環境沒有實現ConfigurableEnvironment接口logger.warn("忽略了[" + sourceClass.getMetadata().getClassName() +"]上的@PropertySource注解。原因:環境必須實現ConfigurableEnvironment接口");}}// 處理所有的@ComponentScan注解Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);// 如果存在@ComponentScan注解,并且當前階段不應該跳過if (!componentScans.isEmpty() &&!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {// 循環處理每個@ComponentScan注解for (AnnotationAttributes componentScan : componentScans) {// 配置類上存在@ComponentScan注解 -> 立即執行掃描Set<BeanDefinitionHolder> scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());// 檢查掃描結果中的定義集合,如果有進一步的配置類,遞歸解析for (BeanDefinitionHolder holder : scannedBeanDefinitions) {if (ConfigurationClassUtils.checkConfigurationClassCandidate(holder.getBeanDefinition(), this.metadataReaderFactory)) {parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());}}}}// 處理所有的@Import注解processImports(configClass, sourceClass, getImports(sourceClass), true);// 處理所有的@ImportResource注解AnnotationAttributes importResource =AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);// 如果@ImportResource注解存在if (importResource != null) {// 獲取資源位置String[] resources = importResource.getStringArray("locations");// 獲取資源的閱讀器類Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");// 循環處理每個資源for (String resource : resources) {// 解析資源位置中的占位符String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);// 把解析后的資源添加到配置類configClass.addImportedResource(resolvedResource, readerClass);}}// 處理單獨的@Bean方法Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);// 循環處理每個@Bean方法for (MethodMetadata methodMetadata : beanMethods) {// 添加@Bean方法到配置類configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));}// 處理接口上的默認方法processInterfaces(configClass, sourceClass);// 處理父類,如果存在的話if (sourceClass.getMetadata().hasSuperClass()) {// 獲取父類名稱String superclass = sourceClass.getMetadata().getSuperClassName();// 如果父類存在,并且父類不是java.*開頭,并且尚未處理過if (superclass != null && !superclass.startsWith("java") &&!this.knownSuperclasses.containsKey(superclass)) {// 記錄已知的父類this.knownSuperclasses.put(superclass, configClass);// 找到父類,返回其注解元數據并遞歸處理return sourceClass.getSuperClass();}}// 沒有父類 -> 處理完成return null;
}

到這里,大體流程我們已經清楚。不再繼續針對注解的解析進行講解,感興趣可以自行下載源碼閱讀理解。

五、本章小結

本章是對整 ConfigurationClassParser 進行講解,它是 Spring framework 中的最核心類。

到這里,Starter 的整個過程已經分析完成,但是針對條件裝配,我們將在下一章進行講解。

?已同步發布到公眾號:面湯放鹽?第七節 ConfigurationClassParser 源碼分析 (qq.com)

掘金賬號:第七節 ConfigurationClassParser 源碼分析 - 掘金 (juejin.cn)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/14694.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/14694.shtml
英文地址,請注明出處:http://en.pswp.cn/web/14694.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[LLM-Agents]淺析Agent工具使用框架:MM-ReAct

上文LLM-Agents]詳解Agent中工具使用Workflow提到MM-ReAct框架&#xff0c;通過結合ChatGPT 與視覺專家模型來解決復雜的視覺理解任務的框架。通過設計文本提示&#xff08;prompt design&#xff09;&#xff0c;使得語言模型能夠接受、關聯和處理多模態信息&#xff0c;如圖像…

winform在一個類中調用窗體的控件和方法的兩個方式

第一: 在類中創建窗體對象的方式&#xff0c;通過對象調用控件或方法 eg: Form1 form1 new Form1(); form1.Button; //調用控件 form1.Method(); //調用方法 要注意&#xff0c;對應控件的Modifiers屬性要設置成public . 第二: 在窗體Form類下定義靜態變量(例如:form1)&…

Multi-Attention Transformer for Naturalistic Driving Action Recognition

標題&#xff1a;用于自然駕駛行為識別的多注意力Transformer 源文鏈接&#xff1a;https://openaccess.thecvf.com/content/CVPR2023W/AICity/papers/Dong_Multi-Attention_Transformer_for_Naturalistic_Driving_Action_Recognition_CVPRW_2023_paper.pdfhttps://openaccess…

linux創建私有docker倉庫以及推拉

創建私有倉庫&#xff1a; 1.下載 registry鏡像。 2.執行 registry 鏡像&#xff08;#為注釋內容&#xff0c;\為換行&#xff09;&#xff1a; docker run -d \# --restartalways每次都是開機自動啟動--restartalways \# --name registry 表示容器名--name registry \# 表示…

java讀取shp文件,獲取點位

Testvoid contextLoads() {System.out.println(System.currentTimeMillis());//1716516228057 1716516228798String zipFilePath "C:\\code\\risk\\risk_management_backend\\edatope-app\\src\\main\\resources\\新中心范圍SHP導入模板.zip";String destDir &quo…

【Muduo】TcpServer類

TcpServer統領之前所有的類&#xff0c;是用戶直接使用的類。它通過ThreadPool管理所有的loopthread&#xff0c;保存所有的TcpConnection&#xff0c;保存用戶提供的各種回調函數并向TcpConnection的Channel中注冊回調。它負責監聽指定的端口&#xff0c;并接受來自客戶端的連…

ZeRO-3、模型并行、流水線并行適用情況

ZeRO-3 適用場景&#xff1a;參數量大但計算量相對均衡的情況。 主要特點&#xff1a; 參數分片&#xff1a;將模型參數、優化器狀態和梯度在多個 GPU 上進行分片。顯存優化&#xff1a;顯著減少每個 GPU 上的顯存占用&#xff0c;使得可以在較小的 GPU 上訓練更大的模型。 …

思科模擬器--06.單臂路由升級版--多端路由互連實驗--24.5.20

實驗圖紙如下: 第0步: 先放置六臺個人電腦,一臺交換機和一臺2911路由器(千兆路由器(G0開頭的)) 接著,用直通線將 PC0的F0,PC1的F0分別和交換機的F0/0, F0/1連接 交換機的F0/3和路由器的G0/0連接 PC2的F0,PC3的F0分別和交換機的F0/4, F0/5連接 交換機的F0/6和路由器的G0/1…

電腦連接愛快iKuai軟路由之后,網卡沒有正常獲取到IP,無法訪問愛快路由管理頁?

前言 上一次咱們說到在愛快控制臺上設置/辨認lan口&#xff0c;設置完成之后&#xff0c;其他的一些設置就需要在愛快iKuai軟路由的管理頁面上設置。 有些小伙伴會發現&#xff0c;當電腦連接上愛快軟路由的lan口之后&#xff0c;電腦并沒有正常獲取到ip&#xff0c;導致無法訪…

JavaScript表達式和運算符

表達式 表達式一般由常量、變量、運算符、子表達式構成。最簡單的表達式可以是一個簡單的值。常量或變量。例&#xff1a;var a10 運算符 運算符一般用符號來表示&#xff0c;也有些使用關鍵字表示。運算符由3中類型 1.一元運算符&#xff1a;一個運算符能夠結合一個操作數&…

【Arthas】阿里的線上jvm監控診斷工具的基本使用

關于對運行中的項目做java監測的需求下&#xff0c;Arthas則是一個很好的解決方案。 我們可以用來 1.監控cpu 現成、內存、堆棧 2.排查cpu飚高 造成原因 3.接口沒反應 是否死鎖 4.接口慢優化 5.代碼未按預期執行 是分支不對 還是沒提交&#xff1f; 6.線上低級錯誤 能不能不重啟…

STL--set和multiset集合

set和multiset會根據特定的排序準則&#xff0c;自動將元素排序。兩者不同之處在于multiset 允許元素重復而 set 不允許。如下圖: 使用set或multiset&#xff0c;必須先包含頭文件: #include <set>上述兩個類型都被定義為命名空間std內的class template: namespace std…

亞馬遜自養號測評:深入解析與搭建要求

在亞馬遜這電商平臺上&#xff0c;商品的評價對于賣家來說至關重要。為了提升商品的曝光率、排名、權重和銷量&#xff0c;賣家們紛紛采用各種推廣方式&#xff0c;其中&#xff0c;亞馬遜自養號測評成為了越來越多賣家選擇的一種有效方式。 亞馬遜自養號測評&#xff0c;顧名…

Android Retrofit 封裝模版

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 一、加上網絡訪問的權限二、引入依賴三、由API生成JavaBean四、封裝Retrofit五、調用 一、加上網絡訪問的權限 <uses-permission android:name"android.p…

分布式事務——9種解決方案的原理與分類

目錄 一、概要1. 分布式事務的概念2. 分布式事務解決方案分類 二、常見的分布式事務解決方案1. 基礎的 2PC&#xff08;二階段提交&#xff09;1.1 核心思想1.2 簡介1.3 主要特點1.3.1 優點1.3.2 缺點 2. 基礎的 3PC&#xff08;三階段提交&#xff09;2.1 核心思想2.2 簡介2.3…

C語言/數據結構——每日一題(有效的括號)

一.前言 如果想要使用C語言來解決這道題——有效的括號&#xff1a;https://leetcode.cn/problems/valid-parentheses/description/我們必須要借用上一篇我們所講的內容——棧的實現&#xff1a;https://blog.csdn.net/yiqingaa/article/details/138923750?spm1001.2014.3001.…

go routing 之 gorilla/mux

1. 背景 繼續學習 go 2. 關于 routing 的學習 上一篇 go 用的庫是&#xff1a;net/http &#xff0c;這次我們使用官方的庫 github.com/gorilla/mux 來實現 routing。 3. demo示例 package mainimport ("fmt""net/http""github.com/gorilla/mux&…

react實現把pc網站快捷添加到桌面快捷方式

文章目錄 1. 需求2. 實現效果3. 核心邏輯4. 完整react代碼 1. 需求 這種需求其實在國外一些游戲網站和推廣網站中經常會用到&#xff0c;目的是為了讓客戶 快捷方便的保存網站到桌面 &#xff0c;網站主動盡量避免下次找不到網站地址了&#xff0c;當然精確的客戶自己也可以使…

Python 字符串中運算符號可運行

使用eval() re {\n "path": "/sms/sendMsg",\n "data": {\n "mobile": "12345678901",\n "signCode": "短信簽名",\n "templateCode": "SMS_yyyy…

Oracle遞歸查詢筆記

目錄 一、創建表結構和插入數據 二、查詢所有子節點 三、查詢所有父節點 四、查詢指定節點的根節點 五、查詢指定節點的遞歸路徑 六、遞歸子類 七、遞歸父類 一、創建表結構和插入數據 CREATE TABLE "REGION" ( "ID" VARCHAR2(36) DEFAULT SYS_GUI…