配置類須要標註@Configuration殊不知緣由?那此次就不能給你漲薪嘍

專一Java領域分享、成長,拒絕淺嘗輒止。關注公衆號【 BAT的烏托邦】開啓專欄式學習,拒絕淺嘗輒止。本文 https://www.yourbatman.cn 已收錄,裏面一併有Spring技術棧、MyBatis、中間件等小而美的專欄供以學習哦。

前言

各位小夥伴你們好,我是A哥。這是繼上篇文章:真懂Spring的@Configuration配置類?你可能自我感受太良好 的原理/源碼解釋篇。按照本公衆號的定位,原理通常跑不了,雖然很枯燥,但還得作,畢竟作難事必有所得,真的掌握了纔有底氣談漲薪嘛。java

Tips:鑑於常常有些同窗沒法區分某個功能/某項能力屬於Spring Framework的仍是Spring Boot,你能夠參考文章裏的【版本約定】目錄,那裏會說明本文的版本依賴,也就是功能所屬嘍。好比本文內容它就屬於Spring Framework,和Spring Boot木有關係。架構


版本約定

本文內容若沒作特殊說明,均基於如下版本:工具

  • JDK:1.8
  • Spring Framework:5.2.2.RELEASE

正文

Spring的IoC就像個「大熔爐」,什麼都看成Bean放在裏面。然而,雖然它們都放在了一塊兒,可是實際在功能上是有區別的,好比咱們熟悉的BeanPostProcessor就屬於後置處理器功能的Bean,還有本文要討論的@Configuration配置Bean也屬於一種特殊的組件。post

判斷一個Bean是不是Bean的後置處理器很方便,只需看它是否實現了BeanPostProcessor接口便可;那麼如何去肯定一個Bean是不是@Configuration配置Bean呢?如果,如何區分是Full模式仍是Lite模式呢?這便就是本文將要討論的內容。學習


如何判斷一個組件是不是@Configuration配置?

首先須要明確:@Configuration配置前提必須是IoC管理的一個組件(也就是常說的Bean)。Spring使用BeanDefinitionRegistry註冊中心管理着全部的Bean定義信息,那麼對於這些Bean信息哪些屬於@Configuration配置呢,這是須要甄選出來的。this

判斷一個Bean是不是@Configuration配置類這個邏輯統一交由ConfigurationClassUtils這個工具類去完成。spa


ConfigurationClassUtils工具類

見名之意,它是和配置有關的一個工具類,提供幾個靜態工具方法供以使用。它是Spring 3.1新增,對於它的做用,官方給的解釋是:用於標識@Configuration類的實用程序(Utilities)。它主要提供了一個方法:checkConfigurationClassCandidate()用於檢查給定的Bean定義是不是配置類的候選對象(或者在配置/組件類中聲明的嵌套組件類),並作相應的標記debug


checkConfigurationClassCandidate()

它是一個public static工具方法,用於判斷某個Bean定義是不是@Configuration配置。設計

ConfigurationClassUtils:

    public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
        ...
        // 根據Bean定義信息,拿到器對應的註解元數據
        AnnotationMetadata metadata = xxx;
        ...
        
        // 根據註解元數據判斷該Bean定義是不是配置類。如果:那是Full模式仍是Lite模式
        Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
        if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        } else if (config != null || isConfigurationCandidate(metadata)) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        } else {
            return false;
        }
        
        ...
        
        // 到這。它確定是一個完整配置(Full or Lite) 這裏進一步把@Order排序值放上去
        Integer order = getOrder(metadata);
        if (order != null) {
            beanDef.setAttribute(ORDER_ATTRIBUTE, order);
        }

        return true;
    }

步驟總結:3d

  1. 根據Bean定義信息解析成爲一個註解元數據對象AnnotationMetadata metadata

    1. 多是個AnnotatedBeanDefinition,也多是個StandardAnnotationMetadata
  2. 根據註解元數據metadata判斷是不是個@Configuration配置類,有以下三種可能case:

    1. 標註有@Configuration註解而且該註解的proxyBeanMethods = false,那麼mark一下它是Full模式的配置。不然進入下一步判斷
    2. 標註有@Configuration註解或者符合Lite模式的條件(上文有說一共有5種多是Lite模式,源碼處在isConfigurationCandidate(metadata)這個方法裏表述),那麼mark一下它是Lite模式的配置。不然進入下一步判斷
    3. 不是配置類,而且返回結果return false
  3. 能進行到這一步,說明該Bean確定是個配置類了(Full模式或者Lite模式),那就取出其@Order值(如有的話),而後mark進Bean定義裏面去

這個mark動做頗有意義:後面判斷一個配置類是Full模式仍是Lite模式,甚至判斷它是不是個配置類都可經過beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE)這樣完成判斷


方法使用處

知曉了checkConfigurationClassCandidate()可以判斷一個Bean(定義)是不是一個配置類,那麼它在何時會被使用呢?經過查找能夠發現它被以下兩處使用到:

  • 使用處:ConfigurationClassPostProcessor.processConfigBeanDefinitions()處理配置Bean定義階段。
ConfigurationClassPostProcessor:

    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        
        // 拿出當前全部的Bean定義信息,一個個的檢查是不是配置類    
        String[] candidateNames = registry.getBeanDefinitionNames();
        for (String beanName : candidateNames) {
            BeanDefinition beanDef = registry.getBeanDefinition(beanName);
            if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
                logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
            }
            // 若是該Bean定義不是配置類,那就繼續判斷一次它是不是配置類,如果就加入結果集合裏
            else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
                configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
            }
        }
        ...
    }

ConfigurationClassPostProcessor是個BeanDefinitionRegistryPostProcessor,會在BeanFactory 準備好後執行生命週期方法。所以天然而然的,checkConfigurationClassCandidate()會在此階段調用,用於區分出哪些是配置Bean。

值得注意的是ConfigurationClassPostProcessor的執行時期是很是早期的(BeanFactory準備好後就執行嘛),這個時候容器內的Bean定義不多。這個時候只有主配置類才被註冊了進來,那些想經過@ComponentScan掃進來的配置類都還沒到「時間」,這個時間節點很重要,請注意區分。爲了方便你理解,我分別把Spring和Spring Boot在此階段的Bean定義信息截圖展現以下:


以上是Spring環境,對應代碼爲:

new AnnotationConfigApplicationContext(AppConfig.class);


以上是Spring Boot環境,對應代碼爲:

@SpringBootApplication
public class Boot2Demo1Application {
    public static void main(String[] args) {
        SpringApplication.run(Boot2Demo1Application.class, args);
    }
}
相比之下,Spring Boot裏多了 internalCachingMetadataReaderFactory這個Bean定義。緣由是SB定義了一個 CachingMetadataReaderFactoryPostProcessor把它放進去的,因爲此Processor也是個 BeanDefinitionRegistryPostProcessor而且order值爲 Ordered.HIGHEST_PRECEDENCE,因此它會優先於 ConfigurationClassPostProcessor執行把它註冊進去~
  • 使用處:ConfigurationClassParser.doProcessConfigurationClass() 解析 @Configuration配置類階段。所處的大階段同上使用處,仍舊是ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry()階段
ConfigurationClassParser:

    @Nullable
    protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
        ... // 先解析nested內部類(內部類會存在@Bean方法嘛~)
        ... // 解析@PropertySource資源,加入到environment環境
        ... // 解析@ComponentScan註解,把組件掃描進來
        scannedBeanDefinitions = ComponentScanAnnotationParser.parse(componentScan, ...);
            // 把掃描到的Bean定義信息依舊須要一個個的判斷,是不是配置類    
            // 如果配置類,就繼續看成一個@Configuration配置類來解析parse() 遞歸嘛
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                ...
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                    parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        ... // 解析@Import註解
        ... // 解析@ImportResource註解
        ... // 解析當前配置裏配置的@Bean方法
        ... // 解析接口默認方法(由於配置類可能實現接口,而後接口默認方法可能標註有@Bean )
        ... // 處理父類(遞歸,直到父類爲java.打頭的爲止)
    }

這個方法是Spring對配置類解析的最核心步驟,經過它順帶也可以解答你的疑惑了吧:爲什麼你僅需在類上標註一個@Configuration註解便可讓它成爲一個配置類?由於被Scan掃描進去了嘛~

經過以上兩個使用處的分析和對比,對於@Configuration配置類的理解,你至少應該掌握了以下訊息:

  1. @Configuration配置類確定是個組件,存在於IoC容器裏
  2. @Configuration配置類是有主次之分的,主配置類是驅動整個程序的入口,能夠是一個,也能夠是多個(若存在多個,支持使用@Order排序)
  3. 咱們平時通常只書寫次配置類(並且通常寫多個),它通常是藉助主配置類的@ComponentScan能力完成加載進而解析的(固然也多是@Import、又或是被其它次配置類驅動的)
  4. 配置類能夠存在嵌套(如內部類),繼承,實現接口等特性

聊完了最爲重要的checkConfigurationClassCandidate()方法,固然還有必要看看ConfigurationClassUtils的另外一個工具方法isConfigurationCandidate()


isConfigurationCandidate()

它是一個public static工具方法,經過給定的註解元數據信息來判斷它是不是一個Configuration

ConfigurationClassUtils:

    static {
        candidateIndicators.add(Component.class.getName());
        candidateIndicators.add(ComponentScan.class.getName());
        candidateIndicators.add(Import.class.getName());
        candidateIndicators.add(ImportResource.class.getName());
    }

    public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
        // 不考慮接口 or 註解 說明:註解的話也是一種「特殊」的接口哦
        if (metadata.isInterface()) {
            return false;
        }
        // 只要該類上標註有以上4個註解任意一個,都算配置類
        for (String indicator : candidateIndicators) {
            if (metadata.isAnnotated(indicator)) {
                return true;
            }
        }
        // 若一個註解都沒標註,那就看有木有@Bean方法 如有那也算配置類
        return metadata.hasAnnotatedMethods(Bean.class.getName());
    }

步驟總結:

  1. 如果接口類型(含註解類型),直接不予考慮,返回false。不然繼續判斷
  2. 若此類上標註有@Component、@ComponentScan、@Import、@ImportResource任意一個註解,就判斷成功返回true。不然繼續判斷
  3. 到此步,就說明此類上沒有標註任何註解。若存在@Bean方法,返回true,不然返回false。

須要特別特別特別注意的是:此方法它的並不考慮@Configuration註解,是「輕量級」判斷,這是它和checkConfigurationClassCandidate()方法的最主要區別。固然,後者依賴於前者,依賴它來根據註解元數據判斷是不是Lite模式的配置。


Spring 5.2.0版本變化說明

由於本文的講解和代碼均是基於Spring 5.2.2.RELEASE的,而並非全部小夥伴都會用到這麼新的版本。關於此部分的實現,以Spring 5.2.0版本爲分界線實現上有些許差別,因此在此處作出說明。


proxyBeanMethods屬性的做用

proxyBeanMethods屬性是Spring 5.2.0版本爲@Configuration註解新增長的一個屬性:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    @AliasFor(annotation = Component.class)
    String value() default "";
    // @since 5.2
    boolean proxyBeanMethods() default true;
}

它的做用是:是否容許代理@Bean方法。說白了:決定此配置使用Full模式仍是Lite模式。爲了保持向下兼容,proxyBeanMethods的默認值是true,使用Full模式配置。

Spring 5.2提出了這個屬性項,是指望你在已經瞭解了它的做用以後,顯示的把它置爲false的,由於在雲原生將要到來的今天,啓動速度方面Spring一直在作着努力,也但願你能配合嘛。這不Spring Boot就「配合」得很好,它在2.2.0版本(依賴於Spring 5.2.0)起就把它的全部的自動配置類的此屬性改成了false,即@Configuration(proxyBeanMethods = false)


Full模式/Lite模式實現上的差別

因爲Spring 5.2.0新增了proxyBeanMethods屬性來控制模式,所以實現上也有些許詫異,請各位注意甄別:

Spring 5.2.0+版本判斷實現:

ConfigurationClassUtils:

        Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
        if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        } else if (config != null || isConfigurationCandidate(metadata)) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        } else {
            return false;
        }

Spring 5.2.0-版本判斷實現:

ConfigurationClassUtils:

        if (isFullConfigurationCandidate(metadata)) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        } else if (isLiteConfigurationCandidate(metadata)) {
            beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        } else {
            return false;
        }

思考題?

  1. 既然isConfigurationCandidate()判斷方法是爲checkConfigurationClassCandidate()服務,那Spring爲什麼也把它設計爲public static呢?
  2. ConfigurationClassUtils裏還存在對@Order順序的解析方法,不是說Spring的Bean是無序的嗎?這又如何理解呢?

總結

本文做爲上篇文章的續篇,解釋了@Configuration配置的Full模式和Lite模式的判斷原理,同時順帶的也介紹了什麼叫主配置配和次配置類,這個概念(雖然官方並不這麼叫)對你理解Spring Framework是很是有幫助的。若是你使用是基於Spring 5.2.0+的版本,在瞭解了這兩篇文章內容的基礎上,建議你的配置類均採用Lite模式去作,即顯示設置proxyBeanMethods = false

另外關於此部份內容,有些更爲感興趣的小夥伴問到:爲何Full模式下經過方法調用指向的仍舊是原來的Bean,保證了只會執行一次呢?開啓的是Full模式這只是表象緣由,想要回答此問題須要涉及到CGLIB加強實現的深水區內容,爲了知足這些好奇(好學)的娃子,計劃會在下篇文章繼續再拿一篇專程講解(預計篇幅不短,萬字以上),你可訂閱個人公衆號持續保持關注。


關注A哥

  • 原創不易,碼字更不易。關注A哥的公衆號【BAT的烏托邦】,開啓有深度的專欄式學習,拒絕淺嘗輒止
  • 專欄式學習,咱們是認真的(關注公衆號回覆「知識星球」領券後再輕裝入駐)
  • 加A哥好友(fsx641385712),備註「Java入羣」邀你進入【Java高工、架構師】系列純純純技術羣

BAT的烏托邦公衆號二維碼

相關文章
相關標籤/搜索