Spring 之 Condition 條件注解 實踐和源碼學習

Condition 是在spring4.0 增加的條件注解,通過這個可以功能可以實現(xiàn)選擇性的注入Bean操作莺匠,接下來先學習下Condition是如何使用的浇衬,然后分析spring源碼了解其中的實現(xiàn)原理懒构。

更多可看==>Spring&SpringBoot 實踐和源碼學習

Demo

注意:以下三個代碼塊分屬不同的文件,便于說明具體問題

@Bean("contectService")
@Conditional(LoadConditional.class)
// 條件控制耘擂,如果對應的match操作返回true胆剧,則會注入該bean
// 否則會跳過處理該bean
public ContextService contextService() {
    return new ContextService();
}

public class LoadConditional implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 查看在Bootstrap的ENV的值是否等于test
        // 后面會介紹這個context上下文,其中包含了整個的bean工廠內(nèi)容
        return Bootstrap.ENV.equals("test");
    }
}

@SpringBootApplication
@ComponentScan("com.demo.boot")
@EnableSwagger2
public class Bootstrap {

    public static String ENV = "test";

    public static void main(String[] args) {
        SpringApplication.run(Bootstrap.class);
    }

}

這樣就簡單的通過@Conditional實現(xiàn)選擇性的注入Bean操作醉冤,實際開發(fā)中秩霍,主要是為了區(qū)分測試環(huán)境和線上環(huán)境,其實在spring已經(jīng)添加了一部分常用的條件注解蚁阳,如下表格

Conditions 描述
@ConditionalOnBean 在存在某個bean的時候
@ConditionalOnMissingBean 不存在某個bean的時候
@ConditionalOnClass 當前classpath可以找到某個類型的類時
@ConditionalOnMissingClass 當前classpath不可以找到某個類型的類時
@ConditionalOnResource 當前classpath是否存在某個資源文件
@ConditionalOnProperty 當前jvm是否包含某個系統(tǒng)屬性為某個值
@ConditionalOnWebApplication 當前spring context是否是web應用程序

源碼學習

在之前的一篇筆記SpringBoot 之 EnableAutoConfiguration 實踐和源碼學習 中寫道@Configuration實現(xiàn)bean的注入铃绒,那么條件注解肯定就類似于一道閥門在實現(xiàn)bean的注入前通過條件篩選去完成選擇性的bean注入,接著上面那篇學習筆記螺捐,來到ConfigurationClassPostProcessor 類颠悬,關(guān)于這個類不再過多介紹

隨著Spring的BPP的invoke來到processConfigBeanDefinitions 方法

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    //  ......忽略前面一大推代碼
    // 遍歷當前裝載到Spring 容器的所有類,獲取其中包含了@Configuration的bean到candidates中
    // 依次遍歷解析Configuration的類
    do {
        parser.parse(candidates);
        parser.validate();
        // 解析完成归粉,獲取所有的類椿疗,暫時還不包含@Bean的信息

        Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(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());
        }
        // 配置類讀取信息,進行條件篩選
        // 也就是我們本篇學習筆記的Condition注解原理所在地
        
        this.reader.loadBeanDefinitions(configClasses);
        alreadyParsed.addAll(configClasses);
             // ....
}

來到ConfigurationClassBeanDefinitionReader 類中糠悼,肯定會進行類的遍歷操作届榄,獲取其中的@Bean,這個方法非常的關(guān)鍵倔喂,包含了很多功能

ConfigurationClassBeanDefinitionReader 類

public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
    TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
    for (ConfigurationClass configClass : configurationModel) {
        loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
    }
    // 傳遞的configClass就是添加了@Configuration 的包裝類   // trackedConditionEvaluator 記錄著類是否會攔截的處理類
}
image
image

再來到其實現(xiàn)的細節(jié)方法中

private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass,
        TrackedConditionEvaluator trackedConditionEvaluator) {

    if (trackedConditionEvaluator.shouldSkip(configClass)) {
           // 如果該configClass被過濾掉铝条,也就是不應該實例化
           // 先直接進入到該方法中看看實現(xiàn)細節(jié)
        String beanName = configClass.getBeanName();
        if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
            this.registry.removeBeanDefinition(beanName);
            // 從context上下文中移除該config的bean信息
        }
        this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
        return;
        // 直接返回
    }
     // 。席噩。班缰。。悼枢。 待續(xù)
}
private class TrackedConditionEvaluator {
    private final Map<ConfigurationClass, Boolean> skipped = new HashMap<ConfigurationClass, Boolean>();
    // 保存著所有config類的篩選狀態(tài)
    public boolean shouldSkip(ConfigurationClass configClass) {
        Boolean skip = this.skipped.get(configClass);
        if (skip == null) {
              // 新處理的類信息
            if (configClass.isImported()) {
                  // 如果該配置類包含@Import導入的類
                boolean allSkipped = true;
                for (ConfigurationClass importedBy : configClass.getImportedBy()) {
                      // 以此遍歷迭代執(zhí)行
                    if (!shouldSkip(importedBy)) {
                          // 迭代每一個被Import導入的類埠忘,如果返回false,也就是不應該被忽略的類
                          // 則直接break,是否會導致 后面的需要跳過的類被忽略莹妒?
                        allSkipped = false;
                        break;
                    }
                }
                if (allSkipped) {
                    // 所有通過@Import的類全部跳過處理
                    skip = true;
                }
            }
            if (skip == null) {
                skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
                // 獲取元信息(也就是類似注解信息)判斷是否可以跳過名船。重點關(guān)注!
            }
            this.skipped.put(configClass, skip);
        }
        return skip;
    }
}

ConditionEvaluator 類

public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) {
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
          // 如果沒有元信息旨怠,或者注解類中不包含Conditional類渠驼,就直接返回false,表示不能跳過
          // 注解類Conditional的用途就在這里了<濉C陨取!爽哎!
        return false;
    }

    if (phase == null) {
          // 默認傳遞的phase是ConfigurationPhase.REGISTER_BEAN
        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<Condition>();
    for (String[] conditionClasses : getConditionClasses(metadata)) {
          // 獲取包含了@Conditional 的value值
          // 類比demo中就是@Conditional(LoadConditional.class)的LoadConditional類全稱
        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            // 完成LoadConditional類的實例化(必須是不帶參數(shù)的構(gòu)造器)
            conditions.add(condition);
        }
    }

    AnnotationAwareOrderComparator.sort(conditions);
    // 按照order進行排序

    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        if (requiredPhase == null || requiredPhase == phase) {
            if (!condition.matches(this.context, metadata)) {
                  // 這個matchee 方法就是自定義實現(xiàn)的方法蜓席,具體如上demo中的LoadConditional的重載方法
                  // 這是個循環(huán)的方法,意味著可以有多個條件注解倦青,
                  // 而且一旦出現(xiàn)了一個被過濾了則直接認定為需要被跳過
                return true;
            }
        }
    }
    return false;
}

該方法就是獲取configuration類的注解信息瓮床,然后調(diào)用相關(guān)條件過濾的matches方法獲取匹配結(jié)果

image

這個圖也正好說明了傳遞到matches方法的context包含的內(nèi)容,例如Spring的Bean工廠产镐,以及上下文環(huán)境等信息(一般這個上下文環(huán)境使用的比較多)

到這里整個過程感覺比較清晰隘庄,但是這是從類的角度觸發(fā),處理的也是@Configuration類癣亚,而沒有@Bean的處理過程

那么繼續(xù)來到loadBeanDefinitionsForConfigurationClass 類中

private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass,
        TrackedConditionEvaluator trackedConditionEvaluator) {

    if (trackedConditionEvaluator.shouldSkip(configClass)) {
           // 上面已經(jīng)說了
        String beanName = configClass.getBeanName();
        if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
            this.registry.removeBeanDefinition(beanName);
        }
        this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
        return;
    }

    if (configClass.isImported()) {
           // 包含了@Import的情況
        registerBeanDefinitionForImportedConfigurationClass(configClass);
    }
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
          // 這個地方是處理@Bean的地方丑掺,也就是我們需要關(guān)注的地方,先忽略下
        loadBeanDefinitionsForBeanMethod(beanMethod);
    }
    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
    // 處理使用了@ImportResource 導入的xml述雾、groovy文件等處理入口
    // 肯定是按照讀取xml的原理一樣街州,聲明一個reader然后解析resource的內(nèi)容,然后填充到register中
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}

回過頭關(guān)注處理@Bean的細節(jié)

private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
    ConfigurationClass configClass = beanMethod.getConfigurationClass();
    // 獲取方法的@Configuration信息
    MethodMetadata metadata = beanMethod.getMetadata();
    String methodName = metadata.getMethodName();

    if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
          // 這個就是上面說沒有處理method的另一個處理入口
          // 返回true就意味著可以被跳過處理
        configClass.skippedBeanMethods.add(methodName);
        return;
    }
    if (configClass.skippedBeanMethods.contains(methodName)) {
        return;
    }

    // Consider name and any aliases
    // name值是一個String[] 的樣式玻孟,主要是處理別名的情況
    AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
    List<String> names = new ArrayList<String>(Arrays.asList(bean.getStringArray("name")));
    String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

    // Register aliases even when overridden
    for (String alias : names) {
        this.registry.registerAlias(beanName, alias);
    }

    // Has this effectively been overridden before (e.g. via XML)?
    // springboot本身的重名會被IDEA類似工具發(fā)現(xiàn)唆缴,但是無法判斷xml是否也存在
    // 主要是去重操作
    if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
        return;
    }

    ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata);
    beanDef.setResource(configClass.getResource());
    beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));

    if (metadata.isStatic()) {
        // static @Bean method
        beanDef.setBeanClassName(configClass.getMetadata().getClassName());
        beanDef.setFactoryMethodName(methodName);
    }
    else {
        // instance @Bean method
        beanDef.setFactoryBeanName(configClass.getBeanName());
        beanDef.setUniqueFactoryMethodName(methodName);
    }
    beanDef.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR);
    beanDef.setAttribute(RequiredAnnotationBeanPostProcessor.SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);

    AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);

    Autowire autowire = bean.getEnum("autowire");
    if (autowire.isAutowire()) {
        beanDef.setAutowireMode(autowire.value());
    }

    String initMethodName = bean.getString("initMethod");
    if (StringUtils.hasText(initMethodName)) {
        beanDef.setInitMethodName(initMethodName);
    }

    String destroyMethodName = bean.getString("destroyMethod");
    if (destroyMethodName != null) {
        beanDef.setDestroyMethodName(destroyMethodName);
    }
    
    // 設(shè)置init、destroy方法等
    // scope范圍等數(shù)據(jù)

    // Consider scoping
    ScopedProxyMode proxyMode = ScopedProxyMode.NO;
    AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
    if (attributes != null) {
        beanDef.setScope(attributes.getString("value"));
        proxyMode = attributes.getEnum("proxyMode");
        if (proxyMode == ScopedProxyMode.DEFAULT) {
            proxyMode = ScopedProxyMode.NO;
        }
    }

    // Replace the original bean definition with the target one, if necessary
    BeanDefinition beanDefToRegister = beanDef;
    if (proxyMode != ScopedProxyMode.NO) {
        BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
                new BeanDefinitionHolder(beanDef, beanName), this.registry,
                proxyMode == ScopedProxyMode.TARGET_CLASS);
        beanDefToRegister = new ConfigurationClassBeanDefinition(
                (RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata);
    }

    if (logger.isDebugEnabled()) {
        logger.debug(String.format("Registering bean definition for @Bean method %s.%s()",
                configClass.getMetadata().getClassName(), beanName));
    }

    this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}

到這里整個的源碼學習就完全結(jié)束了黍翎,整個的過程也還是按照spring原本的套路去實現(xiàn)的面徽,從一個Spring BPP作為入口處理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市匣掸,隨后出現(xiàn)的幾起案子趟紊,更是在濱河造成了極大的恐慌,老刑警劉巖碰酝,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霎匈,死亡現(xiàn)場離奇詭異,居然都是意外死亡送爸,警方通過查閱死者的電腦和手機铛嘱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門暖释,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弄痹,你說我怎么就攤上這事饭入∏镀鳎” “怎么了肛真?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爽航。 經(jīng)常有香客問我蚓让,道長,這世上最難降的妖魔是什么讥珍? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任历极,我火速辦了婚禮,結(jié)果婚禮上衷佃,老公的妹妹穿的比我還像新娘趟卸。我一直安慰自己,他們只是感情好氏义,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布锄列。 她就那樣靜靜地躺著,像睡著了一般惯悠。 火紅的嫁衣襯著肌膚如雪邻邮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天克婶,我揣著相機與錄音筒严,去河邊找鬼。 笑死情萤,一個胖子當著我的面吹牛鸭蛙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播筋岛,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼娶视,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泉蝌?” 一聲冷哼從身側(cè)響起歇万,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎勋陪,沒想到半個月后贪磺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡诅愚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年寒锚,在試婚紗的時候發(fā)現(xiàn)自己被綠了劫映。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡刹前,死狀恐怖泳赋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喇喉,我是刑警寧澤祖今,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站拣技,受9級特大地震影響千诬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜膏斤,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一徐绑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧莫辨,春花似錦傲茄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至敞映,卻和暖如春较曼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背振愿。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工捷犹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冕末。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓萍歉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親档桃。 傳聞我的和親對象是個殘疾皇子枪孩,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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