spring-boot-2.0.3不一樣系列之番外篇 - @Configuration、Condition與@Conditional

来源:https://www.cnblogs.com/youzhibing/archive/2019/02/22/9692091.html
-Advertisement-
Play Games

前言 開心一刻 一名劫匪慌忙中竄上了一輛車的後座,上車後發現主駕和副駕的一男一女疑惑地回頭看著他,他立即拔出槍威脅到:“趕快開車,甩掉後面的警車,否則老子一槍崩了你!”,於是副駕上的男人轉過臉對那女的說:“大姐,別慌,聽我口令把剛纔的動作再練習一遍,掛一檔,輕鬆離合,輕踩油門,走...走,哎 走.. ...


前言

  開心一刻

   一名劫匪慌忙中竄上了一輛車的後座,上車後發現主駕和副駕的一男一女疑惑地回頭看著他,他立即拔出槍威脅到:“趕快開車,甩掉後面的警車,否則老子一槍崩了你!”,於是副駕上的男人轉過臉對那女的說:“大姐,別慌,聽我口令把剛纔的動作再練習一遍,掛一檔,輕鬆離合,輕踩油門,走...走,哎 走...哎,哎,對,走走... 最後,三人都躺到了醫院,劫匪的手上還戴上了銬子...

劫匪的內心

  路漫漫其修遠兮,吾將上下而求索!

  github:https://github.com/youzhibing

  碼雲(gitee):https://gitee.com/youzhibing

前情回顧

  估摸著大家已經忘記了createApplicationContext的內容,本文不做過多的回顧,只是提醒大家:在AnnotationConfigServletWebServerApplicationContext的實例化過程中,實例化了AnnotatedBeanDefinitionReader,另外也將ConfigurationClassPostProcessor定義註冊到了beanFactory中,如下圖所示

  看著AnnotatedBeanDefinitionReader、ConfigurationClassPostProcessor是不是隱約感覺到了什麼?

概念介紹與應用

  @Configuration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {

    /**
     * Explicitly specify the name of the Spring bean definition associated
     * with this Configuration class. If left unspecified (the common case),
     * a bean name will be automatically generated.
     * <p>The custom name applies only if the Configuration class is picked up via
     * component scanning or supplied directly to a {@link AnnotationConfigApplicationContext}.
     * If the Configuration class is registered as a traditional XML bean definition,
     * the name/id of the bean element will take precedence.
     * @return the suggested component name, if any (or empty String otherwise)
     * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator
     */
    @AliasFor(annotation = Component.class)
    String value() default "";

}
View Code

    @Configuration能夠修飾Class、interface和enum,用的最多的還是標註在類上,相當於把該類作為spring的xml配置文件中的<beans>,用於配置spring容器;@Configuration往往會結合@Bean來使用,@Bean等價於spring的xml配置文件中的<bean>,用於註冊bean對象。@Configuration和@Bean組成了基於java類的配置,是spring的推薦配置方式。最簡單的使用如下

@Configuration
public class MyConfiguration {

    @Bean
    public Cat mycat() {
        return new Cat();
    }
}

    如上代碼就會在spring容器中註冊一個名叫mycat的Cat類型的Bean

  Condition

@FunctionalInterface
public interface Condition {

    /**
     * Determine if the condition matches.
     * @param context the condition context
     * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
     * or {@link org.springframework.core.type.MethodMetadata method} being checked
     * @return {@code true} if the condition matches and the component can be registered,
     * or {@code false} to veto the annotated component's registration
     */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}
View Code

    Spring的條件化配置,當我們向spring註冊bean時,可以對這個bean添加一定的自定義條件,當滿足這個條件時註冊這個bean,否則不註冊。springboot中部分實現子類如下

    springboot更多實現請查看org.springframework.boot.autoconfigure.condition包。Condition一般配合@Conditional使用,更多信息往下看

  @Conditional

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

    /**
     * All {@link Condition}s that must {@linkplain Condition#matches match}
     * in order for the component to be registered.
     */
    Class<? extends Condition>[] value();

}
View Code

    Spring的條件註解,其value是一個Class<? extends Condition>[],只有數組中的全部Condition全部匹配成功時,被@Conditional修飾的組件才會被註冊到Spring容器中。@Conditional只是一個標誌,標示需要進行條件判斷,而具體的判斷規則則由具體的Condition來實現。

    在SpringBoot源碼中很容易看到被@Conditional註解的組合註解,例如:@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnClass、@ConditionalOnMissingClass等,具體如下

    springboot還提供了AutoConfigureAfter、AutoConfigureBefore、AutoConfigureOrder,看名字基本知道其作用,具體細節需要大家自己去跟了。

  完整應用案例

    介面都能訪問通,數據返回也都正確,非常完美

    完整工程代碼:spring-boot-condition

    當我們把MyConfiguration中的myCat方法註釋掉(ConditionWeb中的cat相關也註釋掉),再啟動應用的時候,應用報錯啟動不起來,提示如下信息:

Description:

Field dog in com.lee.condition.web.ConditionWeb required a bean of type 'com.lee.condition.model.Dog' that could not be found.
    - Bean method 'myDog' in 'MyConfiguration' not loaded because @ConditionalOnBean (types: com.lee.condition.model.Cat; SearchStrategy: all) did not find any beans of type com.lee.condition.model.Cat


Action:

Consider revisiting the conditions above or defining a bean of type 'com.lee.condition.model.Dog' in your configuration.
View Code

    ConditionWeb中需要Dog類型的bean,而Dog實例化又依賴Cat實例,而我們沒有實例化Cat,所以應用啟動報錯,提示如上信息 

源碼探究

  我們要探究什麼了?不探究太細,就探究@Configuration修飾的配置類是何時解析的,@Conditional是何時生效、如何生效的

  @Configuration修飾的配置類是何時解析的

    ConfigurationClassPostProcessor是一個BeanFactoryPostProcessor(可以查看ConfigurationClassPostProcessor的類繼承結構圖),那麼我們從AbstractApplicationContext的refresh方法調用的invokeBeanFactoryPostProcessors(beanFactory)方法開始

    來到了processConfigurationClass方法,其詳細代碼如下

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    // ConfigurationClass是否應該被skip
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
        return;
    }

    ConfigurationClass existingClass = this.configurationClasses.get(configClass);
    if (existingClass != null) {
        if (configClass.isImported()) {
            if (existingClass.isImported()) {
                existingClass.mergeImportedBy(configClass);
            }
            // Otherwise ignore new imported config class; existing non-imported class overrides it.
            return;
        }
        else {
            // Explicit bean definition found, probably replacing an import.
            // Let's remove the old one and go with the new one.
            this.configurationClasses.remove(configClass);
            this.knownSuperclasses.values().removeIf(configClass::equals);
        }
    }

    // Recursively process the configuration class and its superclass hierarchy. 遞歸處理configuration class和它的父級類
    // 也就說會遞歸處理我們的應用入口類:ConditionApplication.class,以及ConditionApplication.class的父級類
    SourceClass sourceClass = asSourceClass(configClass);
    do {
        sourceClass = doProcessConfigurationClass(configClass, sourceClass);
    }
    while (sourceClass != null);

    // 將滿足條件的ConfigurationClass都放入configurationClasses集合中
    // 後續會載入configurationClasses集合中所有的ConfigurationClass中配置的bean定義
    this.configurationClasses.put(configClass, configClass);
}
View Code

    其中shouldSkip方法如下

/**
 * Determine if an item should be skipped based on {@code @Conditional} annotations.
 * @param metadata the meta data
 * @param phase the phase of the call
 * @return if the item should be skipped
 */
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    // 如果這個類沒有註解修飾,或者沒有被@Conditional註解(包括Conditional系列)所修飾,不會skip
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
        return false;
    }

    // 如果參數中沒有設置條件註解的生效階段
    if (phase == null) {
        if (metadata instanceof AnnotationMetadata &&
                ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
            return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
        }
        return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
    }

    // 要解析的配置類的條件集合,即@Conditional的value
    List<Condition> conditions = new ArrayList<>();
    for (String[] conditionClasses : getConditionClasses(metadata)) {
        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, this.context.getClassLoader());
            conditions.add(condition);
        }
    }

    // 對條件進行排序
    AnnotationAwareOrderComparator.sort(conditions);

    // 遍歷條件,逐個匹配
    for (Condition condition : conditions) {
        ConfigurationPhase requiredPhase = null;
        if (condition instanceof ConfigurationCondition) {
            requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
        }
        // 條件註解的生效階段滿足,一旦有條件匹配不成功,則返回true,skip此類
        if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
            return true;
        }
    }

    return false;
}
View Code

    我們再回過頭去看processConfigBeanDefinitions方法

/**
 * Build and validate a configuration model based on the registry of
 * {@link Configuration} classes.
 *  驗證@Configuration修飾的類,滿足條件則構建成configuration model
 */
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
    String[] candidateNames = registry.getBeanDefinitionNames();

    for (String beanName : candidateNames) {
        BeanDefinition beanDef = registry.getBeanDefinition(beanName);
        if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
                ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
            }
        }
        else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
            configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
        }
    }

    // Return immediately if no @Configuration classes were found
    if (configCandidates.isEmpty()) {
        return;
    }

    // Sort by previously determined @Order value, if applicable
    configCandidates.sort((bd1, bd2) -> {
        int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
        int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
        return Integer.compare(i1, i2);
    });

    // Detect any custom bean name generation strategy supplied through the enclosing application context
    // 檢測自定義的bean生成策略
    SingletonBeanRegistry sbr = null;
    if (registry instanceof SingletonBeanRegistry) {
        sbr = (SingletonBeanRegistry) registry;
        if (!this.localBeanNameGeneratorSet) {
            BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
            if (generator != null) {
                this.componentScanBeanNameGenerator = generator;
                this.importBeanNameGenerator = generator;
            }
        }
    }

    if (this.environment == null) {
        this.environment = new StandardEnvironment();
    }

    // Parse each @Configuration class
    // 解析每一個被@Configuration修飾的class
    ConfigurationClassParser parser = new ConfigurationClassParser(
            this.metadataReaderFactory, this.problemReporter, this.environment,
            this.resourceLoader, this.componentScanBeanNameGenerator, registry);

    Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
    Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
    do {
        parser.parse(candidates);    // 解析過程中會將滿足條件的@Configuration class存放到configurationClasses中
        parser.validate();

        // 滿足條件的@Configuration class 都存放在了parser的configurationClasses中
        Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
        configClasses.removeAll(alreadyParsed);

        // Read the model and create bean definitions based on its content
        // 讀取@Configuration class中的配置(各個@Bean),並創建對應的bean definition(後續創建bean實例會用到bean定義)
        if (this.reader == null) {
            this.reader = new ConfigurationClassBeanDefinitionReader(
                    registry, this.sourceExtractor, this.resourceLoader, this.environment,
                    this.importBeanNameGenerator, parser.getImportRegistry());
        }
        this.reader.loadBeanDefinitions(configClasses);    // 載入全部@Configuration class中的配置
        alreadyParsed.addAll(configClasses);

        candidates.clear();
        if (registry.getBeanDefinitionCount() > candidateNames.length) {
            String[] newCandidateNames = registry.getBeanDefinitionNames();
            Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
            Set<String> alreadyParsedClasses = new HashSet<>();
            for (ConfigurationClass configurationClass : alreadyParsed) {
                alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
            }
            for (String candidateName : newCandidateNames) {
                if (!oldCandidateNames.contains(candidateName)) {
                    BeanDefinition bd = registry.getBeanDefinition(candidateName);
                    if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                            !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                        candidates.add(new BeanDefinitionHolder(bd, candidateName));
                    }
                }
            }
            candidateNames = newCandidateNames;
        }
    }
    while (!candidates.isEmpty());

    // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
    if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
        sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
    }

    if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
        // Clear cache in externally provided MetadataReaderFactory; this is a no-op
        // for a shared cache since it'll be cleared by the ApplicationContext.
        ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
    }
}
View Code

  @Conditional是何時生效、如何生效的

    這個問題再上面已經全部得到體現,Spring不會無腦的載入所有的@Configuration class,只會載入滿足條件的@Configuration class,而@Conditional就是條件標誌,至於條件匹配規則這有Condition提供;shouldSkip方法中用到Conditional和Condition,完成條件的匹配處理。

總結

  1、@Configuration和@Bean組成了基於java類的配置,與xml中的<Beans>、<Bean>功能一致,Spring推薦java類的配置;

  2、Condition與@Conditional實現了條件配置,只有滿足了條件的@Configuration class和@Bean才會被註冊到Spring容器;

  3、Spring以我們的應用啟動類為基礎來遞歸掃描配置類,包括我們應用中的配置類、Spring自己的以及第三方的配置類(springboot集成的各種配置類(spring-boot-autoconfigure-xxx.RELEASE.jar下的spring.factories文件中的Auto Configure),還有pageHelper的自動配置,等等);前提是需要開啟自動配置(@EnableAutoConfiguration)。

參考

  SpringBoot源碼分析之條件註解的底層實現

  Spring 工具類 ConfigurationClassParser 分析得到


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

-Advertisement-
Play Games
更多相關文章
  • 今天童心未泯,玩起了大風車,特別好玩,大家一定不要忘記兒時的夢想,吹吹大風車,轉出好心情 CSS代碼是用less生成的,所以有些繁瑣,大家見諒^_^ JS代碼是實現效果的代碼,絕對不能少 ...
  • mockjs 官網:http://mockjs.com/ 之前沒有使用過 mockjs 的同學,請參考官網文檔,數據生成規則和方法的調用都有詳細說明。 一、通過npm安裝依賴包 1. 進入到項目目錄,執行指令: npm install mockjs -D 由於mockjs是用來模擬數據的,只有是開發 ...
  • ElementUI 官網: http://element-cn.eleme.io/#/zh-CN/component/installation 一、通過npm安裝依賴包 1. 進入到項目目錄,執行指令 : npm i element-ui -S 2. 安裝成功後 package.json 中可以看到 ...
  • 實現效果是在要素點的四周不同位置添加標簽。 此節需要註意一個問題,書寫的先後順序可能會影響運行速度。要註意正確的書寫先後。 1、定義涉及到的所有變數 var minScale = 2500000; var serviceUrl = "https://services.arcgis.com/V6ZHF ...
  • js String擴展方法 'asdasdasd'.repalceA()"AsdAsdAsd" ...
  • 簡介 處理併發問題的重點不在於你的設計是怎樣的,而在於你要評估你的併發,併在併發範圍內處理。你預估你的併發是多少,然後測試r+m是否支持。緩存的目的是為了應對普通對象資料庫的讀寫限制,依托與nosql的優勢進行高速讀寫。 redis本身也有併發瓶頸。所以你要把讀寫和併發區分開來處理。只讀業務是不是可 ...
  • [TOC] JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案,本文介紹它的原理和用法。 一、跨域認證的問題 互聯網服務離不開用戶認證。一般流程是下麵這樣。 1. 用戶向伺服器發送用戶名和密碼。 2. 伺服器驗證通過後,在當前對話(session)裡面保存相關數據,比如用戶角 ...
  • Python環境的安裝 安裝Python: windows: 1、下載安裝包 https://www.python.org/downloads/ 2、安裝 預設安裝路徑:C:\python27 3、配置環境變數 【右鍵電腦】--》【屬性】--》【高級系統設置】--》【高級】--》【環境變數】--》 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...