springboot2.0.3源碼篇 - 自動配置的實現,發現也不是那麼複雜

来源:https://www.cnblogs.com/youzhibing/archive/2019/04/05/10559275.html
-Advertisement-
Play Games

前言 開心一刻 女兒: “媽媽,你這麼漂亮,當年怎麼嫁給了爸爸呢?” 媽媽: “當年你爸不是窮嘛!‘ 女兒: “窮你還嫁給他!” 媽媽: “那時候剛剛畢業參加工作,領導對我說,他是我的扶貧對象,我年輕理解錯了,就嫁給他了!” 女兒...... @Import註解應用 應用開發中,當我們的功能模塊比較 ...


前言

  開心一刻   

    女兒: “媽媽,你這麼漂亮,當年怎麼嫁給了爸爸呢?”
    媽媽: “當年你爸不是窮嘛!‘
    女兒: “窮你還嫁給他!”
    媽媽: “那時候剛剛畢業參加工作,領導對我說,他是我的扶貧對象,我年輕理解錯了,就嫁給他了!”
    女兒......

@Import註解應用

  應用開發中,當我們的功能模塊比較多時,往往會按模塊或類別對Spring的bean配置文件進行管理,使配置文件模塊化,更容易維護;spring3.0之前,對Spring XML bean文件進行拆分, 例如

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
 
    <import resource="config/user.xml"/>
    <import resource="config/role.xml"/>
    <import resource="config/permission.xml"/>
 
</beans>

  spring3.0及之後,引入了@Import註解,提供與Spring XML中的<import />元素等效的功能;spring4.2之前,@Import只支持導入配置類(@Configuration修飾的類、ImportSelector實現類和ImportBeanDefinitionRegistrar實現類),而spring4.2及之後不僅支持導入配置類,同時也支持導入常規的java類(如普通的User類)

  示例地址:spring-boot-autoconfig,四種都有配置,不用down下來運行,看一眼具體如何配置即可

  運行測試用例,結果如下

  可以看到,Dog、Cat、Role、User、Permission的實例都已經註冊到了spring容器,也就是說上述講的@Import的4種方式都是能夠將實例註冊到spring容器的

@Import註解原理

  @Import何以有如此強大的功能,背後肯定有某個團隊在運作,而這個團隊是誰了,就是spring;spring容器肯定在某個階段有對@Import進行了處理,至於spring是在什麼時候對@Import進行了怎樣的處理,我們來跟一跟源碼;ConfigurationClassPostProcessor實現了BeanDefinitionRegistryPostProcessor,那麼它會在spring啟動的refresh階段被應用,我們從refresh的invokeBeanFactoryPostProcessors方法開始

  ConfigurationClassPostProcessor

    註意此時spring容器中的bean定義與bean實例,數量非常少,大家可以留心觀察下

    一路跟下來,我們來到processConfigBeanDefinitions方法,該方法會創建一個ConfigurationClassParser對象,該對象會分析所有@Configuration註解的配置類,產生一組ConfigurationClass對象,然後從這組ConfigurationClass對象中載入bean定義

  ConfigurationClassParser

    主要是parse方法

public void parse(Set<BeanDefinitionHolder> configCandidates) {    
    this.deferredImportSelectors = new LinkedList<>();

    // 通常情況下configCandidates中就一個BeanDefinitionHolder,關聯的是我們的啟動類
    // 示例中是:com.lee.autoconfig.AutoConfigApplication
    for (BeanDefinitionHolder holder : configCandidates) {
        BeanDefinition bd = holder.getBeanDefinition();
        try {
            // 被@Configuration註解修飾的類會被解析為AnnotatedGenericBeanDefinition,AnnotatedGenericBeanDefinition實現類AnnotatedBeanDefinition介面
            if (bd instanceof AnnotatedBeanDefinition) {
                parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
            }
            else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
                parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
            }
            else {
                parse(bd.getBeanClassName(), holder.getBeanName());
            }
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                    "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
        }
    }

    // 處理延遲的ImportSelector,這裡本文的重點:自動配置的入口
    processDeferredImportSelectors();
}
View Code

    從啟動類(示例中是com.lee.autoconfig.AutoConfigApplication)開始,遞歸解析配置類以及配置類的父級配置類;邊跟邊註意beanFactory中beanDefinitionMap的變化,ConfigurationClassParser對象有beanFactory的引用,屬性名叫registry;我們可以仔細看下doProcessConfigurationClass方法

/**
 * 通過從源類中讀取註解、成員和方法來構建一個完整的配置類:ConfigurationClass
 * 註意返回值,是父級類或null(null包含兩種情況,沒找到父級類或之前已經處理完成)
 */
@Nullable
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)) {
        if (this.environment instanceof ConfigurableEnvironment) {
            processPropertySource(propertySource);
        }
        else {
            logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                    "]. Reason: Environment must implement ConfigurableEnvironment");
        }
    }

    // 處理配置類上所有的@ComponentScan註解,包括@ComponentScans和ComponentScan
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    if (!componentScans.isEmpty() &&
            !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
        for (AnnotationAttributes componentScan : componentScans) {
            // 立即掃描@ComponentScan修飾的配置類,
            // 通常是從啟動類所在的包(示例中是com.lee.autoconfig)開始掃描,掃描配置類(被@Configuration修飾的類)
            Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
            // 進一步檢查通過配置類掃描得到的bean定義集,併在需要時遞歸解析
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                if (bdCand == null) {
                    bdCand = holder.getBeanDefinition();
                }
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                    parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        }
    }

    // 處理配置類上所有的@Import註解
    // 包括@Import支持的4種類型:ImportSelector、ImportBeanDefinitionRegistrar、@Configuration和普通java類
    // 普通java類會被按@Configuration方式處理
    processImports(configClass, sourceClass, getImports(sourceClass), true);

    // 處理配置類上所有的@ImportResource註解,xml方式的bean就是其中之一
    AnnotationAttributes importResource =
            AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
    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);
    for (MethodMetadata methodMetadata : beanMethods) {
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }

    // 處理預設的方法或介面
    processInterfaces(configClass, sourceClass);

    // 處理父級類,如果有的話
    if (sourceClass.getMetadata().hasSuperClass()) {
        String superclass = sourceClass.getMetadata().getSuperClassName();
        if (superclass != null && !superclass.startsWith("java") &&
                !this.knownSuperclasses.containsKey(superclass)) {
            this.knownSuperclasses.put(superclass, configClass);
            // Superclass found, return its annotation metadata and recurse
            return sourceClass.getSuperClass();
        }
    }

    // No superclass -> processing is complete
    return null;
}
View Code

    上述代碼中寫了相關註釋,有興趣的同學可以更進一步的去跟,這裡我只跟下processImports方法,因為這個與自動配置息息相關

    起始的ConfigurationClass包括:1、工程中所有我們自定義的被@Configuration修飾的類,示例中就只有AnimalConfig;2、應用的啟動類,示例中是:AutoConfigApplication。

    我們自定義的ConfigurationClass一般不會包含多級父級ConfigurationClass,例如AnimalConfig,就沒有父級ConfigurationClass,解析就比較簡單,我們無需關註,但AutoConfigApplication就不一樣了,他往往會被多個註解修飾,而這些註解會牽扯出多個ConfigurationClass,需要遞歸處理所有的ConfigurationClass;上圖中,我們跟到了一個比較重要的類:AutoConfigurationImportSelector,實例化之後封裝成了DeferredImportSelectorHolder對象,存放到了ConfigurationClassParser的deferredImportSelectors屬性中

自動配置源碼解析

  有人可能有這樣的疑問:哪來的AutoConfigurationImportSelector,它有什麼用? 客觀莫急,我們慢慢往下看

  我們的應用啟動類被@SpringBootApplication,它是個組合註解,詳情如下

  相信大家都看到@Import(AutoConfigurationImportSelector.class)了,ConfigurationClassParser就是從此解析到的AutoConfigurationImportSelector,至於AutoConfigurationImportSelector有什麼用,馬上揭曉;我們回到ConfigurationClassParser的parse方法,裡面還有個很重要的方法:processDeferredImportSelectors,值得我們詳細跟下

  processDeferredImportSelectors

    說的簡單點,從類路徑下的所有spring.facoties文件中讀取全部的自動配置類(spring.factories文件中org.springframework.boot.autoconfigure.EnableAutoConfiguration的值),然後篩選出滿足條件的配置類,封裝成ConfigurationClass,存放到ConfigurationClassParser的configurationClasses屬性中

    說的詳細點,分兩個方法進行說明

      selectImports方法

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 從類路徑下的spring.factories文件中讀取所有配置類(org.springframework.boot.autoconfigure.EnableAutoConfigurationd的值)
    // 得到所有配置類的全路徑類名的集合 - 數組
    // 此時得到的是類名,至於該類存不存在,還需要在下麵步驟中進行檢驗
    List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);
    // 去重重覆的
    configurations = removeDuplicates(configurations);
    // 獲取需要排除的配置類,@SpringBootApplication exclude和excludeName的值
    // 以及配置文件中spring.autoconfigure.exclude的值
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    // 驗證排除的配置類是否存在 - 類路徑下是否存在該類
    checkExcludedClasses(configurations, exclusions);
    // 剔除需要排除的配置類
    configurations.removeAll(exclusions);
    // 進行過濾 - 通過配置類的條件註解(@ConditionalOnClass、@ConditionalOnBean等)來判斷配置類是否符合條件
    configurations = filter(configurations, autoConfigurationMetadata);
    // 觸發自動配置事件 - ConditionEvaluationReportAutoConfigurationImportListener
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // 返回@Import方式 所有滿足條件的配置類
    return StringUtils.toStringArray(configurations);
}
View Code

        從類路徑下的所有spring.facoties文件中讀取org.springframework.boot.autoconfigure.EnableAutoConfiguration的所有值,此時獲取的是全路徑類名的數組,然後進行篩選過濾,1、先去重處理,因為多個spring.factories中可能存在重覆的;2、然後剔除我們配置的需要排除的類,包括@SpringBootApplication註解的exclude、excludeName,以及配置文件中的spring.autoconfigure.exclude;3、條件過濾,過濾出滿足自己條件註解的配置類。最終獲取所有滿足條件的自動配置類,示例中有24個。

        條件註解更詳細的信息請查看:spring-boot-2.0.3源碼篇 - @Configuration、Condition與@Conditional,讀取spring.facoties文件的詳細信息請查看:spring-boot-2.0.3啟動源碼篇一 - SpringApplication構造方法

      processImports方法

        這個方法在解析ConfigurationClassParser的parse方法的時候已經用到過了,只是沒有做說明,它其實就是用來處理配置類上的@Import註解的;上述selectImports方法解析出來的配置類,每個配置類都會經過processImports方法處理,遞歸處理@Import註解,就與遞歸處理我們的啟動類的@Import註解一樣,從而獲取所有的自動配置類;springboot的自動配置就是這樣實現的。

  此時還只是獲取了滿足條件的自動配置類,配置類中的bean定義載入還沒有進行,我們回到ConfigurationClassPostProcessor的processConfigBeanDefinitions方法,其中有如下代碼

// 各種方式的配置類的解析,包括springboot的自動配置 - @Import、AutoConfigurationImportSelector
parser.parse(candidates);
parser.validate();

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);

// Read the model and create bean definitions based on its content
if (this.reader == null) {
    this.reader = new ConfigurationClassBeanDefinitionReader(
            registry, this.sourceExtractor, this.resourceLoader, this.environment,
            this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);    // 將配置類中的bean定義載入到beanFactory

  至此,springboot的自動配置源碼解析就完成了,有興趣的可以更近一步的深入

總結

  1、各個方法之間的調用時序圖如下,結合這個時序圖看上面的內容,更好看懂

  2、springboot自動配置底層依賴的是SpringFactoriesLoader和AutoConfigurationImportSelector;@EnableAutoConfiguration註解就像一個八爪魚,抓取所有滿足條件的配置類,然後讀取其中的bean定義到spring容器,@EnableAutoConfiguration得以生效的關鍵組件關係圖如下


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一般情況下,在我們做訪問許可權管理的時候,會把用戶的正確登錄後的基本信息保存在Session中,以後用戶每次請求頁面或介面數據的時候,拿到 Session中存儲的用戶基本信息,查看比較他有沒有登錄和能否訪問當前頁面。 Session的原理,也就是在伺服器端生成一個SessionID對應了存儲的用戶數據 ...
  • 定義 提供了一個統一的介面,用來訪問子系統中一群介面 適用場景 詳解 外觀模式,主要理解外觀。通俗一點可以認為這個模式是將子系統封裝到一起,提供給應用的層面就提供一個方法。不直接由應用層直接訪問子系統。 下麵我們看看ibatis的源碼來具體理解外觀模式。 上述代碼其實是完成一個創建MetaObjec ...
  • 簡單工廠模式概述 簡單工廠模式的結構與實現 結構: 實現 1 abstract class Product 2 { 3 public void MethName() 4 { 5 //公共方法的實現 6 } 7 public abstract void MethodDiff(); 8 //聲明抽象業務 ...
  • 定義: 指原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象。不需要知道任何創建的細節,不調用構造函數適用場景: 詳解: 接下來我們分下麵幾部分講解: 1.原型模式的核心 其實很簡單,就是實現Cloneable介面,然後重寫clone()方法。上面我們已經說過 ,當你在上面的適用場景中的時 ...
  • 模式的誕生與定義 -Context(模式可適用的前提條件)-Theme或Problem(在特定條件下要解決的目標問題)-Solution(對目標問題求解過程中各種物理關係的記述) 設計模式的分類 Gof設計模式 創建型模式(關註對象的創建過程,對類的實例化過程進行抽象,描述如何將對象的創建和使用分離 ...
  • 概念重覆請求是指一個請求因為某些原因被多次提交,場景簡述如下:1)用戶快速多次點擊按鈕2)Nginx失敗重試機制3)服務框架失敗重試機制4)MQ消息重覆消費5)第三方支付支付成功後,因為異常原因導致的多次非同步回調; 冪等性是指同樣的請求參數,多次請求返回的結果相同。一般是因為重覆請求導致的重覆操作等 ...
  • 經過前兩個模式的學習,是不是對設計模式有了進一步的認識了呢,現在,我們繼續沖鴨。 本章可以稱為“給愛用繼承的人一個全新的設計眼界”。這裡我們即將再度探討典型的繼承濫用問題,我們將學到如何使用對象組合的方式,做到在運行時裝飾類。為什麼呢?一旦熟悉了裝飾的技巧,你將能夠在不修改任何底層代碼的情況下,給對 ...
  • 多線程 什麼是線程? - 能獨立運行的基本單位——線程(Threads)。 - 線程是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。 - 一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。 - 就好比生產的工廠,一個車 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...