SpringBoot 之 EnableAutoConfiguration 實踐和源碼學(xué)習(xí)

EnableAutoConfiguration 是SpringBoot的Enable系列中一個比較基礎(chǔ)的的功能模塊毫蚓,現(xiàn)在我們就來學(xué)習(xí)如何使用,以及分析源碼學(xué)習(xí)其工作原理

EnableAutoConfiguration 從名字也可以很容易看出來其功能是能夠自動裝配配置,在SpringBoot中如果需要為其他人提供SDK等接口使用,使用方本身必須實例化接口類才可調(diào)用蛹找,如果每一個使用方都單獨(dú)去實例化該接口蜕便,必然導(dǎo)致使用成本的增加劫恒,EnableAutoConfiguration就能很好的解決這個問題,使用方通過這個就可以直接使用轿腺,避免額外操作两嘴。

1、EnableAutoConfiguration 源碼學(xué)習(xí)

先提個問題族壳,如果現(xiàn)在只能使用Spring Framework憔辫,該如何實現(xiàn)類似的功能呢?
或許能想到的只有BPP仿荆,BeanPostProcessor或者BeanFactoryPostProcessor贰您,只是他們處理的范疇不一樣,BeanPostProcessor更多的是處理單個bean拢操,而BeanFactoryPostProcessor是處理context上下文的

Spring包含了多種類型的BPP锦亦,在spring的生命周期的多個位置提供了對外的鉤子便于擴(kuò)展更多功能,關(guān)于BPP可以看看BPP的內(nèi)容
在官方文檔中令境,對BeanFactoryPostProcessor方法的簡述也說的非常清楚杠园,Modify the application context's internal bean factory after its standard initialization. All bean definitions will have been loaded, but no beans will have been instantiated yet. This allows for overriding or adding properties even to eager-initializing beans.

事實上,SpringBoot也確實是這樣干的舔庶,ConfigurationClassPostProcessor 就是實例化了一個BeanDefinitionRegistryPostProcessor抛蚁,從而擁有了修改context上下文的bean信息以及注冊bean的功能

如下圖由Spring的BPP處理器調(diào)用到ConfigurationClassPostProcessor,然后來到了AutoConfigurationImportSelector 類中

image

1.1惕橙、ConfigurationClassPostProcessor 處理

先了解下ConfigurationClassPostProcessor 這個BPP是如何被注入到spring容器的

springboot啟動學(xué)習(xí)筆記中 已經(jīng)介紹了spring的context上下文創(chuàng)建是由context = createApplicationContext(); 實現(xiàn)的瞧甩,深入該代碼直到AnnotationConfigUtils 類可以發(fā)現(xiàn)其操作是如果沒發(fā)現(xiàn)org.springframework.context.annotation.internalCommonAnnotationProcessor這個bean的name,那就添加一個ConfigurationClassPostProcessor bean弥鹦,具體如下圖

image

這樣我們就清楚了ConfigurationClassPostProcessor 這個類是如何裝載進(jìn)spring容器中的肚逸,接下來就是ConfigurationClassPostProcessor這個類具體的調(diào)用操作了

ConfigurationClassPostProcessor 類

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    List<BeanDefinitionHolder> configCandidates = new ArrayList<BeanDefinitionHolder>();
    String[] candidateNames = registry.getBeanDefinitionNames();

    for (String beanName : candidateNames) {
           // 遍歷spring容器所有的beanName信息,此時還未裝載業(yè)務(wù)bean彬坏,
           // 只有spring&springboot本身框架層次需要的一些特定bean存在(特別注意包含主啟動類)
           // 這點(diǎn)在之前的關(guān)于springboot的啟動學(xué)習(xí)筆記中已經(jīng)介紹了
        BeanDefinition beanDef = registry.getBeanDefinition(beanName);
        if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
                ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
                // 確認(rèn)該beandefinition是否存在org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass 鍵值對信息
                // 如果存在則假定是已經(jīng)經(jīng)過配置類處理過了
            if (logger.isDebugEnabled()) {
                logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
            }
        }
        else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
               // 否則檢查一下當(dāng)前的beandefinition是否符合config 配置類
              // 具體實現(xiàn)原理就是獲取到bean的注解信息吼虎,然后查看是否存在 @Configuration 注解類或者 @Bean 注解
              // 如果有,則返回true
              // 當(dāng)然在這里只會有主啟動類才包含了@Configuration 的信息
            configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
        }
    }

    // Return immediately if no @Configuration classes were found
    if (configCandidates.isEmpty()) {
        return;
    }
    // 一般情況下苍鲜,到這里只會有主啟動類一個configCandidates信息存在

    // Sort by previously determined @Order value, if applicable
    Collections.sort(configCandidates, new Comparator<BeanDefinitionHolder>() {
        @Override
        public int compare(BeanDefinitionHolder bd1, BeanDefinitionHolder bd2) {
            int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
            int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
            return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
        }
    });

    // Detect any custom bean name generation strategy supplied through the enclosing application context
    SingletonBeanRegistry sbr = null;
    if (registry instanceof SingletonBeanRegistry) {
        sbr = (SingletonBeanRegistry) registry;
        if (!this.localBeanNameGeneratorSet && sbr.containsSingleton(CONFIGURATION_BEAN_NAME_GENERATOR)) {
             // 如果當(dāng)前類不包含beanName 生成器 同時 context包含了單一的beanName生成器
             // 設(shè)置當(dāng)前bean的生成器信息
            BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
            this.componentScanBeanNameGenerator = generator;
            this.importBeanNameGenerator = generator;
        }
    }

    // 生成配置解析類parses思灰,開始解析每一個包含了@Configuration 的類
    ConfigurationClassParser parser = new ConfigurationClassParser(
            this.metadataReaderFactory, this.problemReporter, this.environment,
            this.resourceLoader, this.componentScanBeanNameGenerator, registry);

    Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
    Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
    do {
        parser.parse(candidates);
        // 關(guān)鍵的地方來了,這里就會去解析真正包含了@Configuration 的類
        parser.validate();

        Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
        // 所有通過@Configuration 裝載進(jìn)來的類集合
        configClasses.removeAll(alreadyParsed);
        // 已經(jīng)裝載的就移除掉

        // 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());
        }
        // reader是配置類裝載類beandefinition
        this.reader.loadBeanDefinitions(configClasses);
        // 裝載到Spring容器中混滔,包含了那些使用@Bean的類信息
        alreadyParsed.addAll(configClasses);
            ......
            // 到這里就可以認(rèn)為@Configuration 的導(dǎo)入基本完成了
}

真正進(jìn)入到@Configuration 解析的入口處代碼

ConfigurationClassParser 類

image

在經(jīng)過processDeferredImportSelectors方法調(diào)用的之前洒疚,已經(jīng)經(jīng)過了parse處理歹颓,明確了@Configuration 包含的ImportSelectors 信息
如果需要自定義該注解則一定也要實現(xiàn)利用@Import注解導(dǎo)入的ImportSelector 實現(xiàn)類

private void processDeferredImportSelectors() {
    List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
    this.deferredImportSelectors = null;
    Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR);
    // 對獲取的DeferredImportSelectorHolder 排序后進(jìn)行遍歷操作

    for (DeferredImportSelectorHolder deferredImport : deferredImports) {
        ConfigurationClass configClass = deferredImport.getConfigurationClass();
        // configClass 就是主啟動類
        try {
            String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
            // 所有的需要導(dǎo)入的import類的selectImports方法執(zhí)行,
            // 這個里面就是EnableAutoConfigurationImportSelector 
            // 具體的EnableAutoConfigurationImportSelector里面的selectImports后面說
            processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
            // 把獲取的配置類信息進(jìn)一步迭代處理油湖,因為存在在類中包含了配置類的情況
            // 不過需要注意巍扛,這時候并未往spring容器中注入
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                    "Failed to process import candidates for configuration class [" +
                    configClass.getMetadata().getClassName() + "]", ex);
        }
    }
}

1.2、EnableAutoConfigurationImportSelector 的 selectImports 執(zhí)行

來到AutoConfigurationImportSelector類

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
          // 如果注解原信息未激活乏德,則不可用撤奸,直接返回空
        return NO_IMPORTS;
    }
    try {
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        List<String> configurations = getCandidateConfigurations(annotationMetadata,
                attributes);
        // 利用SpringFactoriesLoader 獲取系統(tǒng)中所有的META-INF/spring.factories 的 EnableAutoConfiguration 的鍵值對信息,其中就包含了上面我們自定義的類信息
        configurations = removeDuplicates(configurations);
        // 存在多處地方可能注冊了相同的類信息喊括,去重處理
        configurations = sort(configurations, autoConfigurationMetadata);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        // 匹配出包含exclude胧瓜、excludeName 的列表信息,后續(xù)移除該config
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        // 總之經(jīng)過各種操作郑什,最后產(chǎn)出了可用的配置類列表
        fireAutoConfigurationImportEvents(configurations, exclusions);
        // 配置導(dǎo)入的事件觸發(fā)
        return configurations.toArray(new String[configurations.size()]);
        // 返回最后的配置類列表
    }
    catch (IOException ex) {
        throw new IllegalStateException(ex);
    }
}

1.3府喳、總結(jié)

到這里對EnableAutoConfiguration的源碼學(xué)習(xí)就算是結(jié)束了,利用Spring對外的BeanFactoryPostProcessor的鉤子ConfigurationClassPostProcessor去解析出@Import引入的類蘑拯,然后解析出所有被@Configuration的對象钝满,然后注冊到spring容器中,這樣就完成了整個的自動裝載過程

2申窘、DEMO

image

如上圖弯蚜,可以發(fā)現(xiàn)在資源根目錄下存放了一個"META-INF/spring.factories"的文件,里面的內(nèi)容也很簡單

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.demo.boot.api.CustomAutoConfiguration

CustomAutoConfiguration 類代碼

@Configuration
public class CustomAutoConfiguration {

    @Bean
    public ApiStudent apiStudent() {
        ApiStudent apiStudent = new ApiStudent();
        // ApiStudent 只有一個name字段的基礎(chǔ)POJO
        apiStudent.setName("auto-config");
        return apiStudent;
    }

}

非常簡單的一個注冊bean到spring的樣例

現(xiàn)在我們在另一個服務(wù)中引入該服務(wù)剃法,然后直接通過@resource 注解就可以引用到ApiStudent這個bean了

@Resource
private ApiStudent apiStudent;

@GetMapping("/custom-autoconfig")
@ResponseBody
public String autoConfig() {
    return apiStudent.getName();
}
image

可以看出網(wǎng)頁上輸出的內(nèi)容就是起初在CustomAutoConfiguration中為apiStudent這個bean設(shè)置的屬性值

到此demo就結(jié)束了熟吏,非常的基礎(chǔ)的一個樣例,在實際應(yīng)用中也是如此玄窝,也使用的非常頻繁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末悍引,一起剝皮案震驚了整個濱河市恩脂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趣斤,老刑警劉巖俩块,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異浓领,居然都是意外死亡玉凯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進(jìn)店門联贩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漫仆,“玉大人,你說我怎么就攤上這事泪幌∶ぱ幔” “怎么了署照?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吗浩。 經(jīng)常有香客問我建芙,道長,這世上最難降的妖魔是什么懂扼? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任禁荸,我火速辦了婚禮,結(jié)果婚禮上阀湿,老公的妹妹穿的比我還像新娘赶熟。我一直安慰自己,他們只是感情好炕倘,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布钧大。 她就那樣靜靜地躺著,像睡著了一般罩旋。 火紅的嫁衣襯著肌膚如雪啊央。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天涨醋,我揣著相機(jī)與錄音瓜饥,去河邊找鬼。 笑死浴骂,一個胖子當(dāng)著我的面吹牛乓土,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播溯警,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼趣苏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梯轻?” 一聲冷哼從身側(cè)響起食磕,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喳挑,沒想到半個月后彬伦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伊诵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年单绑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片曹宴。...
    茶點(diǎn)故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡搂橙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笛坦,到底是詐尸還是另有隱情份氧,我是刑警寧澤唯袄,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站蜗帜,受9級特大地震影響恋拷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜厅缺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一蔬顾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧湘捎,春花似錦诀豁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至活翩,卻和暖如春烹骨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背材泄。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工沮焕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拉宗。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓峦树,卻偏偏與公主長得像,于是被迫代替她去往敵國和親旦事。 傳聞我的和親對象是個殘疾皇子魁巩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容