spring源碼------@Conditional注解的解析Condition接口巾乳,以及springboot中的擴展

1.SpringBoot中的共同點

?在springBoot中有很多這種標簽@ConditionalOnXXX標簽讓springBoot的代碼更加標簽化配置更加靈活息罗。這些標簽都有共同點轻局,這里例舉兩個標簽的源碼

1.1``@ConditionalOnXXX`
......
@Conditional({OnClassCondition.class})
public @interface ConditionalOnClass {
    ......
}
......
@Conditional({OnBeanCondition.class})
public @interface ConditionalOnBean {
    ......
}

?發(fā)現(xiàn)這些標簽的共同點是上面都貼有@Conditional標簽梧却,然后在進入到這個標簽里面的值的類看看

class OnClassCondition extends FilteringSpringBootCondition {
    ......
}
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {
    ......
}

?這里發(fā)現(xiàn)又有一個共同點這兩個類都是FilteringSpringBootCondition的子類迹蛤,但是這個類是springBoot擴展實現(xiàn)的我們要找的是源頭在spring中類民珍,網上找可以發(fā)現(xiàn)這個類最后間接實現(xiàn)了spring的Condition。因此我們后面要找的就是Condition類盗飒。

2. Conditional標簽的解析

2.1Condition介紹

?在Condition類中只有一個方法matches方法嚷量。這類的作用是,在bean的定義即將被注冊之前逆趣,會檢查條件是否匹配蝶溶,然后根據匹配的結果決定是否注冊bean。

/**
 * A single {@code condition} that must be {@linkplain #matches matched} in order
 * for a component to be registered.
 *
 * <p>Conditions are checked immediately before the bean-definition is due to be
 * registered and are free to veto registration based on any criteria that can
 * be determined at that point.
 *
 * <p>Conditions must follow the same restrictions as {@link BeanFactoryPostProcessor}
 * and take care to never interact with bean instances. For more fine-grained control
 * of conditions that interact with {@code @Configuration} beans consider the
 * {@link ConfigurationCondition} interface.
 *
 * @author Phillip Webb
 * @since 4.0
 * @see ConfigurationCondition
 * @see Conditional
 * @see ConditionContext
 */
@FunctionalInterface
public interface Condition {

    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

?檢查條件是否匹配的方法就是matches宣渗,在源碼的類描述中提到了一點抖所。如果實現(xiàn)的是這個類,那么必須注意落包,在實現(xiàn)方法也就是matches中不能與bean的實例交互部蛇。之所以要注意這一點是因為這個方法的調用時間在bean的實例化之前的,此時如果跟實例交互就會提前實例化bean咐蝇,可能會引起錯誤涯鲁。如果想要對貼有@Configuration標簽的bean更細粒度的控制可以通過實現(xiàn)ConfigurationCondition來完成。

2.2ConditionEvaluator類處理match方法

?通過查看Conditionmatches在哪里被調用有序。發(fā)現(xiàn)整個spring中只有在ConditionEvaluator中調用了這個方法抹腿。這個類的作用是評估一個貼了Conditional注解的類是否需要跳過。通過類上面的注解來判斷旭寿。進入到類方法

    //metadata是AnnotationMetadataReadingVisitor類型的警绩,在5.2版本被SimpleAnnotationMetadataReadingVisitor代替
    public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
        //檢查注解中是否包含@Conditional類型的注解
        if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
            return false;
        }
        //如果沒有指定了當前bean是解析還是注冊
        if (phase == null) {
            //bean的注解信息封裝對象是AnnotationMetadata類型并且,類上有@Component盅称,@ComponentScan肩祥,@Import后室,@ImportResource,則表示為解析類型
            if (metadata instanceof AnnotationMetadata &&
                    ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
                return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
            }
            return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
        }

        List<Condition> conditions = new ArrayList<>();
        //從bean的注解信息封裝對象中獲取所有的Conditional類型或者Conditional的派生注解
        for (String[] conditionClasses : getConditionClasses(metadata)) {
            for (String conditionClass : conditionClasses) {
                //實例化Conditional中的條件判斷類(Condition的子類)
                Condition condition = getCondition(conditionClass, this.context.getClassLoader());
                //添加到條件集合中
                conditions.add(condition);
            }
        }
        //根據Condition的優(yōu)先級進行排序
        AnnotationAwareOrderComparator.sort(conditions);

        for (Condition condition : conditions) {
            ConfigurationPhase requiredPhase = null;
            //如果是ConfigurationCondition類型的Condition
            if (condition instanceof ConfigurationCondition) {
                //獲取需要對bean進行的操作混狠,是解析還是注冊
                requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
            }
            //(如果requiredPhase==null或者指定的操作類型是目前階段的操作類型)并且不符合設置的條件則跳過
            if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
                return true;
            }
        }

        return false;
    }

?上面這個方法作用就是判斷當前bean處于解析還是注冊岸霹,如果處于解析階段則跳過,如果處于注冊階段則不跳過将饺。其中Conditionmatches方法就起到了判斷的是否符合的作用贡避,進而覺得是否跳過當前bean。

2.3 ConfigurationClassPostProcessorprocessConfigBeanDefinitions

?還是通過查找ConditionEvaluator類的match方法調用鏈的方式予弧,發(fā)現(xiàn)最后都是在ConfigurationClassPostProcessorprocessConfigBeanDefinitions中進行調用的刮吧。一共有兩個調用的位置,這里用調用的位置的代碼進行展示

    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
        //獲取registry中定義的所有的bean的name
        String[] candidateNames = registry.getBeanDefinitionNames();
        ......
        do {
            //第一個會調用shouldSkip的位置掖蛤,這里是解析能夠直接獲取的候選配置bean杀捻。可能是Component坠七,ComponentScan水醋,Import旗笔,ImportResource或者有Bean注解的bean
            parser.parse(candidates);
            parser.validate();
            //獲取上面封裝已經解析過的配置bean的ConfigurationClass集合
            Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
            //移除前面已經處理過的
            configClasses.removeAll(alreadyParsed);
            //第二個會調用shouldSkip的位置彪置,這里是加載configurationClasse中內部可能存在配置bean,比如方法上加了@Bean或者@Configuration標簽的bean
            this.reader.loadBeanDefinitions(configClasses);
            alreadyParsed.addAll(configClasses);
            }
        ......
    }

?這里的parse方法解析BeanDefinitionRegistry中能直接獲取到的候選bean蝇恶,并解析保存到ConfigurationClassParser類的保存解析過的配置類的集合configurationClasses中拳魁。
?loadBeanDefinitions則是對上面解析的集合configurationClasses中的bean內部的進一步的處理,處理類內部定義的bean撮弧。

2.4ConfigurationClassParserparse方法
public void parse(Set<BeanDefinitionHolder> configCandidates) {
    ......
        try {
                if (bd instanceof AnnotatedBeanDefinition) {
                    parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
                }
             }
     ......
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
        processConfigurationClass(new ConfigurationClass(metadata, beanName));
    }

?在ConfigurationClassParserparse方法中有三個分支潘懊,分別是對不同類型的BeanDefinition進行解析,這里進入AnnotatedBeanDefinition類型的贿衍。
?進入到parse方法后在進入里面調用的processConfigurationClass方法授舟,這里只需要分析開頭就知道了

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
        //檢查當前解析的配置bean是否包含Conditional注解,如果不包含則不需要跳過
        // 如果包含了則進行match方法得到匹配結果贸辈,如果是符合的并且設置的配置解析策略是解析階段不需要調過
        if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
            return;
        }
    ......
    //后面的步驟就是解析配置bean释树,然后進行注冊的操作
}

?可以看到這里就是對是否跳過bean解析的位置。在這里Conditional標簽的作用就完了擎淤。主要就是覺得當前的配置bean是否符合我們規(guī)定的規(guī)則奢啥,不符合就不會注冊。

2.5ConfigurationClassBeanDefinitionReaderloadBeanDefinitions方法

?這里對上面已經解析過的bean類集合的內部進行處理的步驟嘴拢。是一個循環(huán)迭代處理的過程

    public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
        //對import標簽處理的類
        TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
        //循環(huán)處理
        for (ConfigurationClass configClass : configurationModel) {
            
            loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
        }
    }

TrackedConditionEvaluator主要是處理@import標簽同時也you的桩盲。舉個例子:如果A類通過@import類引入了另外的一個B類,如果A類需要跳過解析席吴,那么B類也肯定需要調過解析赌结。如果A類需要進行解析捞蛋,那么B類也需要進行解析。后面會分析內部的方法柬姚,現(xiàn)在進入到loadBeanDefinitionsForConfigurationClass方法襟交。

    private void loadBeanDefinitionsForConfigurationClass(
            ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
        //檢查當前的bean是否是通過@import注解引入的,如果是的則循環(huán)解析到原始的貼有@import標簽的bean伤靠,檢查是否有@conditionl標簽并檢查是否需要跳過
        if (trackedConditionEvaluator.shouldSkip(configClass)) {
            //獲取當前配置bean的beanName
            String beanName = configClass.getBeanName();
            //如果當前beanName在BeanDefinitionRegistry中需要注冊的bean的列表中則移除捣域,因為這個bean需要被跳過
            if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
                this.registry.removeBeanDefinition(beanName);
            }
            //移除在ImportRegistry中imports列表中的該beanName,因為這個bean需要被跳過
            this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
            return;
        }
        //如果當前配置bean是通過@import注解進行注入的則進行注冊
        if (configClass.isImported()) {
            registerBeanDefinitionForImportedConfigurationClass(configClass);
        }
        //獲取當前配置類的BeanMethod,就是在方法上面貼了@Bean注解的方法
        for (BeanMethod beanMethod : configClass.getBeanMethods()) {
            //進行加載注入
            loadBeanDefinitionsForBeanMethod(beanMethod);
        }
        //加載configClass中的配置的resource
        loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
        //加載configClass中的配置的ImportBeanDefinitionRegistrar
        loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
    }

?這里理解還是比較簡單的宴合,就是判斷當前的配置類是否需要跳過焕梅,因為當前的配置類也可能是通過@import標簽進行引入的,所以有必要進行對最原始的進行引入的類進行分析卦洽,決定當前的類是否需要跳過贞言。如果需要調過,則一處對應的需要進行注冊bean列表中的改bean阀蒂,如果不需要則進行注冊處理该窗。這里關鍵在于當前的bean是否需要調過,就在TrackedConditionEvaluator類的shouldSkip方法中蚤霞,而這個類也只有這一個方法酗失。這里比較難以理解,我也是看來半天才理解的昧绣」骐龋可以多看幾遍,debug更好夜畴。

    private class TrackedConditionEvaluator {

        private final Map<ConfigurationClass, Boolean> skipped = new HashMap<>();

        public boolean shouldSkip(ConfigurationClass configClass) {
            //檢查當前的配置類是否需要調過拖刃,因為這里是前面的直接獲取到的配置類,如果當前類需要跳過那么贪绘,內部的也必定需要跳過
            //如果是null則說明這個類不是需要跳過的兑牡,但是也不代表是不需要跳過的,因為如果是被引入的則決定于最外面的一層bean是否需要跳過
            Boolean skip = this.skipped.get(configClass);
            if (skip == null) {
                //當前的配置bean是不是通過@import注解引入的均函,不是的則不需要跳過,因為只有是在bean內部定義的bean才需要判斷外面的一層bean是否需要跳過
                if (configClass.isImported()) {
                    boolean allSkipped = true;
                    //獲取通過@import標簽引入這個配置類的bean
                    for (ConfigurationClass importedBy : configClass.getImportedBy()) {
                        //檢查引入配置類的bean的是否需要跳過(這里一直會檢查到最終的引入類垄琐,來決定是否需要全部跳過)
                        if (!shouldSkip(importedBy)) {
                            allSkipped = false;
                            break;
                        }
                    }
                    //如果所有的bean(1.當前的配置bean边酒,2.引入當前配置bean) 的bean 都是需要跳過的,則這個配置bean需要跳過
                    if (allSkipped) {
                        // The config classes that imported this one were all skipped, therefore we are skipped...
                        skip = true;
                    }
                }
                //能夠到這一步的是最層的bean狸窘,例如A引入了B墩朦,B引入了C,那么A就是最外層的bean翻擒,檢查A對應的@condition決定是否需要跳過氓涣,
                if (skip == null) {
                    //這里就是對ConditionEvaluator方法的調用
                    skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
                }
                //將對應的配置bean記錄起來是否需要跳過
                this.skipped.put(configClass, skip);
            }
            return skip;
        }
    }

?其實最后目的還是很簡單的就是最終到最上級的bean牛哺,來決定當前bean是否需要進行注冊。前面舉得例子就是這個意思劳吠。

3. spring中的總結

?對于Conditional標簽的解析上面就是全部的了引润。作用就是來決定貼了這個注解的bean,通過指定的Condition實現(xiàn)類實現(xiàn)matches方法來決定是否需要進行解析痒玩,需要進行注冊淳附。代碼在調用鏈
AbstractApplicationContext

public void refresh() throws BeansException, IllegalStateException {
    .......
    invokeBeanFactoryPostProcessors(beanFactory);
    ......
}

    protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
        PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
        ......
}

?進入到PostProcessorRegistrationDelegate類,在這個類的invokeBeanFactoryPostProcessors方法中會多次調用到ConfigurationClassPostProcessorpostProcessBeanDefinitionRegistrypostProcessBeanFactory方法蠢古。在這兩個方法中都會調用上面講解的processConfigBeanDefinitions方法進而處理Condition標簽奴曙。

4.springboot中的擴展

?在spring中實現(xiàn)Condition接口的類很少,在springboot中才廣泛的運用到了草讶。而springboot也在spring的基礎上做了一個基礎的擴展實現(xiàn)洽糟,然后再在這個基礎的擴展實現(xiàn)上進一步擴展的。這個擴展的實現(xiàn)類就是SpringBootCondition類《檎剑現(xiàn)在進入到這個類

public abstract class SpringBootCondition implements Condition {
    @Override
    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //從封裝condition注解信息的類中獲取指定的Condition類的實現(xiàn)類
        String classOrMethodName = getClassOrMethodName(metadata);
        try {
            //確定匹配結果以及日志輸出對象
            ConditionOutcome outcome = getMatchOutcome(context, metadata);
            //打印匹配的情況坤溃,如果是不匹配會打印不匹配的原因
            logOutcome(classOrMethodName, outcome);
            //將匹配結果進行存儲
            recordEvaluation(context, classOrMethodName, outcome);
            //返回匹配的結果
            return outcome.isMatch();
        }
        catch (NoClassDefFoundError ex) {
            throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
                    + ex.getMessage() + " not found. Make sure your own configuration does not rely on "
                    + "that class. This can also happen if you are "
                    + "@ComponentScanning a springframework package (e.g. if you "
                    + "put a @ComponentScan in the default package by mistake)", ex);
        }
        catch (RuntimeException ex) {
            throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
        }
    }

    public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);

}

?可以看到這里的擴展的就是對匹配的結果進行封裝,然后打印以及存儲嘱丢。其中getMatchOutcome是由各個SpringBootCondition的實現(xiàn)類去實現(xiàn)的薪介。作用就是判斷各自按照各自的使用條件來判斷是否符合來返回匹配的結果。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末屿讽,一起剝皮案震驚了整個濱河市昭灵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伐谈,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件试疙,死亡現(xiàn)場離奇詭異诵棵,居然都是意外死亡,警方通過查閱死者的電腦和手機祝旷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門履澳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人怀跛,你說我怎么就攤上這事距贷。” “怎么了吻谋?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵忠蝗,是天一觀的道長。 經常有香客問我漓拾,道長阁最,這世上最難降的妖魔是什么戒祠? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮速种,結果婚禮上姜盈,老公的妹妹穿的比我還像新娘。我一直安慰自己配阵,他們只是感情好馏颂,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著棋傍,像睡著了一般饱亮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舍沙,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天近上,我揣著相機與錄音,去河邊找鬼拂铡。 笑死壹无,一個胖子當著我的面吹牛,可吹牛的內容都是我干的感帅。 我是一名探鬼主播斗锭,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼失球!你這毒婦竟也來了岖是?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤实苞,失蹤者是張志新(化名)和其女友劉穎豺撑,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體黔牵,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡聪轿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了猾浦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陆错。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖金赦,靈堂內的尸體忽然破棺而出音瓷,到底是詐尸還是另有隱情,我是刑警寧澤夹抗,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布绳慎,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏偷线。R本人自食惡果不足惜磨确,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望声邦。 院中可真熱鬧乏奥,春花似錦、人聲如沸亥曹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽媳瞪。三九已至骗炉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蛇受,已是汗流浹背句葵。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兢仰,地道東北人乍丈。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像把将,于是被迫代替她去往敵國和親轻专。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容