SpringBoot自動化配置之二:自動配置(AutoConfigure)原理、EnableAutoConfiguration、condition


  自動配置絕對算得上是Spring Boot的最大亮點,完美的展示了CoC約定優於配置; Spring Boot能自動配置Spring各種子項目(Spring MVC, Spring Security, Spring Data, Spring Cloud, Spring Integration, Spring Batch等)以及第三方開源框架所需要定義的各種Bean。 Spring Boot內部定義了各種各樣的XxxxAutoConfiguration配置類,預先定義好了各種所需的Bean。只有在特定的情況下這些配置類才會被起效。 

一、概述

  SpringBoot的Auto-configuration的核心實現都位於spring-boot-autoconfigure-xxx.jar;其中SpringBoot將依據classpath里面的依賴內容來自動配置bean到IoC容器,Auto-configuration會嘗試推斷哪些beans是用戶可能會需要的。

  自動化配置並不是spring boot才有的,從spring framework3.1開始,這個特性就有了,像@EnableAspectJAutoProxy、@EnableAsync都是從spring 3.1開始就有了。org.springframework.context.annotation包下面擁有自動配置的所有的相關的基礎設施。

基礎設施

org.springframework.context.annotation包下面提供了各種基於注解配置的基礎設施:
1. @Profile:可跟@Bean配合、
2. @Bean、@Scope、@DependsOn、@Primary、@Lazy、@Role、@Description:
3. @Conditional、Condition:@Conditional注解標識在類或者方法上,標識在方法上,符合條件,創建該方法的返回值類型的Bean;標識在類上,符合條件全部創建。
4. @Import(@ImportResource):
5. @Configuration表示的Class(@EnableLoadTimeWeaving)、ImportSelector接口實現(@EnableAsync)或者ImportBeanDefinitionRegistrar接口實現(@EnableAspectJAutoProxy)
6. ImportSelector、DeferredImportSelector:
7. ImportRegistry
8. ImportBeanDefinitionRegistrar:用來手動注冊bean定義的, 可以實現類似於Mybatis-Spring提供的掃描Mapper接口並注冊其bean定義, 事實上@MapperScan注解就@Import了MapperScannerRegistrar這個類, 而這個類實現了上面的接口, 來掃描Mapper並注冊bean定義.再多說點吧, Spring解析Java配置類的時候, 會判斷類是不是標注了@Import注解, 然后會判斷, 如果Import注解的value是ImportBeanDefinitionRegistrar類型, 會存到一個變量, 后面初始化bean工程完成后, 會回調ImportBeanDefinitionRegistrar.
9. @Configuration:跟@Controller、@Servcice和@Repository是一樣的套路,都用@Component注解了,作為特定類型的組件
10. @PropertySource
11. Condition、ConfigurationCondition、@Conditional

spring boot autoconfigure

  Spring Boot AutoConfigure替代了XML風格的配置文件,帶來了前所未有的體驗。Spring Boot AutoConfigure模塊基於Spring Framework和Spring Boot提供的基礎設施,構建類配置Bean+屬性文件配置行為的配置方式,Java類配置Bean為我們提供了更好的編程體驗,屬性文件配置行為的方式使這種方式擁有跟XML外部配置文件配置方式同樣的靈活性。

org.springframework.boot.autoconfigure

首先,Spring Boot AutoConfigure在Spring Framework和Spring Boot提供的基礎設施上做了很多的擴展工作
1. 順序控制:AutoConfigureOrder、AutoConfigureAfter、AutoConfigureBefore;
2. AutoConfigurationPackage:在spring boot mian class上標識EnableAutoConfiguration之后,所有子包下面的spring 組件都能被掃描到,就是這個注解的能力;
3. EnableAutoConfiguration/ImportAutoConfiguration:EnableAutoConfiguration開啟自動配置,自動應用spring.factories中配置的各種*AutoConfiguration;ImportAutoConfiguration跟EnableAutoConfiguration相比,只是沒有自動配置的功能,給ImportAutoConfiguration傳入誰的AutoConfiguration就應用誰的,單元測試等的場景用到的比較多;
4. 其他的一些工具類,過濾器之類的東西大家可以自己去看下

org.springframework.boot.autoconfigure.context.condition
ConditionalOnCloudPlatform:是否在雲環境下,spring boot cloud模塊提供了兩種實現,CLOUD_FOUNDRY和HEROKU,國內應該用不到這個注解了
ConditionalOnJava:指定的Java版本
ConditionalOnWebApplication:是Web環境的時候
ConditionalOnNotWebApplication:不是web環境的時候
ConditionalOnJndi:JNDI環境下使用
ConditionalOnClass:classpath中存在某個類
ConditionalOnMissingClass:classpath中不存在某個類
ConditionalOnBean:BeanFactory中存在某個類的Bean
ConditionalOnMissingBean:BeanFactory中不存在某個類的Bean
ConditionalOnExpression:SpEL的結果
ConditionalOnProperty:Environment中是否有某個屬性的配置信息
ConditionalOnResource:classpath中是否存在指定名稱的資源
ConditionalOnSingleCandidate:指定的類在BeanFactory中只有一個候選的bean,或者有多個候選的bean,但是其中一個指定了primary時
各種*AutoConfiguration的實現:
所有的*AutoConfiguration的具體實現包括兩部分,一個是標識了@Configuration注解的配置類,另一個是Property文件。有些模塊比較復雜,像security的oauth2模塊,主要文件也是這兩類,剩下的是一些工具。

*AutoConfiguration也是Configuration,被@Configuration注解,只不過spring boot autoconfigure模塊內置的 *AutoConfiguration被配置到了 spring.factories文件中,啟動的時候自動配置。

自動配置是Spring Boot的最大亮點,完美的展示了CoC約定優於配置

二、源碼解析

2.1、從SpringBoot啟動時的自動配置加載過程

查看源碼可以看看自動配置類是如何被引入的。 
a) 應用入口 

@SpringBootApplication  
public class SpringBootDemoApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(SpringBootDemoApplication.class, args);  
    }  
  
}  

b) 類注解 @SpringBootApplication = @EnableAutoConfiguration + @ComponentScan + @Configuration(而其中的@EnableAutoConfiguration 則正是實現Auto Config的關鍵之所在)

@SpringBootConfiguration  
@EnableAutoConfiguration  
@ComponentScan(excludeFilters = {  
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),  
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })  
public @interface SpringBootApplication {  
    // ...  
}  
  
@Configuration  
public @interface SpringBootConfiguration {  
    // ...  
}  

c)開啟自動配置注解 @EnableAutoConfiguration,是auto config 關鍵所在。

@AutoConfigurationPackage  
@Import(EnableAutoConfigurationImportSelector.class)  
public @interface EnableAutoConfiguration {  
    // ...  
}  

d)導入配置類 EnableAutoConfigurationImportSelector extends AutoConfigurationImportSelector

@EnableAutoConfiguration 注解會導入AutoConfigurationImportSelector類的實例被引入到Spring容器中,而該類的繼承鏈如下:

public class AutoConfigurationImportSelector
        implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
        BeanFactoryAware, EnvironmentAware, Ordered {

 

因此對於 AutoConfigurationImportSelector類, 我們重點關注的是其實現自ImportSelector接口的方法selectImports,而直接繼承的DeferredImportSelector作為一個標志性接口,主要作用是為了Defer(推遲;延期;)。

在我們繼續探索之前,讓我們暫停一下,先來回顧下Spring是如何執行到這里來的,即如何調用到AutoConfigurationImportSelector.selectImports方法的。
在selectImports方法上打個斷點,啟動任意一個springboot項目,調用鏈如下圖:

從上述堆棧中我們可以看到 ConfigurationClassParser.parse() 被調用,而其參數candidates ,作為一個集合參數其中只包含我們在啟動SpringBoot時傳入的那個AutoConfigSpringBootApplication類包裹所形成的BeanDefinitionHolder實例。

該ConfigurationClassParser.parse(Set<BeanDefinitionHolder> configCandidates)方法最終會調用到自身內部私有的processDeferredImportSelectors()方法:

// 本方法位於 protected 訪問級別的 ConfigurationClassParser 中
private void processDeferredImportSelectors() {
    // @EnableAutoConfiguration注解上修飾的@Import(AutoConfigurationImportSelector.class) 注解的解析是由 ConfigurationClassParser.parse中開始調度完成(本類中的processImports方法), 進而載入到本類的 deferredImportSelectors 字段中。
    // 這里要特別注意,正因為AutoConfigurationImportSelector是一個DeferredImportSelector實例,所以其生效時機晚於@Import生效的時機,這也使得邏輯時序可以正確地運行下去。
    List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
    this.deferredImportSelectors = null;
    Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR);
    
    for (DeferredImportSelectorHolder deferredImport : deferredImports) {
        // 這里取到的configClass就是我們自定義的 AutoConfigSpringBootApplication
        ConfigurationClass configClass = deferredImport.getConfigurationClass();
        try {
            // 核心邏輯就是下面這兩句了
            // 首先是這行, 負責回調我們上面使用@Import導入的AutoConfigurationImportSelector里的邏輯實現, 詳情將在本文接下來的內容
            // 最終的返回值是經過篩選,滿足要求的類名
            String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
            // configClass 就是我們傳遞給 SpringApplication.run 的AutoConfigSpringBootApplication類
            // 該方法最終會跳轉到 本類內部的doProcessConfigurationClass方法中,來將相應Bean注冊進容器, Auto Config完成。
            processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
        }
        catch  {
            // 異常處理略
        }
    }
}

繞了一圈終於回到本小節原本關注的內容——有關AutoConfigurationImportSelector實現的selectImports方法:

// AutoConfigurationImportSelector (位於package - org.springframework.boot.autoconfigure, 所以是SpringBoot自帶的)
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    try {
        // 加載 META-INF/spring-autoconfigure-metadata.properties 中的相關配置信息, 注意這主要是供Spring內部使用的
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        // 獲取所有通過META-INF/spring.factories配置的, 此時還不會進行過濾和篩選
        // KEY為 : org.springframework.boot.autoconfigure.EnableAutoConfiguration
        List<String> configurations = getCandidateConfigurations(annotationMetadata,
                attributes);
        // 開始對上面取到的進行過濾,去重,排序等操作
        configurations = removeDuplicates(configurations);
        configurations = sort(configurations, autoConfigurationMetadata);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        fireAutoConfigurationImportEvents(configurations, exclusions);
        // 這里返回的滿足條件, 通過篩選的配置類 
        return configurations.toArray(new String[configurations.size()]);
    }
    catch (IOException ex) {
        throw new IllegalStateException(ex);
    }
}
A、getCandidateConfigurations()方法:
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
        // 獲取所有通過META-INF/spring.factories配置的, 此時還不會進行過濾和篩選KEY為:org.springframework.boot.autoconfigure.EnableAutoConfiguration的配置的value(類路徑+類名稱)
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
                getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }
    
    //返回EnableAutoConfiguration.class
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }
    
    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        //此時為org.springframework.boot.autoconfigure.EnableAutoConfiguration
        String factoryClassName = factoryClass.getName();
        try {
            //配置項的默認位置META-INF/spring.factories
            Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            List<String> result = new ArrayList<String>();
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                //從多個配置文件中查找,例如我的有:spring-boot-admin-starter-client-1.5.6.jar!/META-INF/spring.factories和stat-log-0.0.1-SNAPSHOT.jar!/META-INF/spring.factories
                Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
                String factoryClassNames = properties.getProperty(factoryClassName);
                result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
            }
            return result;
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                    "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
    }
看上面的源碼可知通過SpringFactoriesLoader.loadFactoryNames()把多個jar的/META-INF/spring.factories配置文件中的有EnableAutoConfiguration配置項都抓出來。
spring.factories的位置截圖如下:

B、在完成了對spring.factories中所有的EnableAutoConfiguration的解析后,對其過濾,去重,排序等操作等,返回。

C、ConfigurationClassParser.processImports(),ConfigurationClassParser工具類自身的邏輯並不注冊bean定義,它的主要任務是發現@Configuration注解的所有配置類並將這些配置類交給調用者(調用者會通過其他方式注冊其中的bean定義),而對於非@Configuration注解的其他bean定義,比如@Component注解的bean定義,該工具類使用另外一個工具ComponentScanAnnotationParser掃描和注冊它們。

以上正是Springboot完成Auto Config功能的關鍵點之一了。在本實現中,SpringBoot只是告知Spring需要去加載(Import)哪些Config類,剩下的工作依然是Spring那已經經過千錘百煉的邏輯來完成; 這正是 “微核 + 擴展”的優秀架構設計經驗的極致體現。

 

2.2、springboot自動配置DIY

通過上面的源碼分析,可以將如下的spring.factories的所有配置類如下:

spring-boot-1.5.10.RELEASE.jar/META-INF/spring.factories 

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer

spring-boot-autoconfigure-1.5.10.RELEASE.jar/META-INF/spring.factories 

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer

相應在做war時也是把當前的SpringBootDemoApplication作為source傳給了ServletInitializer。 

public class ServletInitializer extends SpringBootServletInitializer {  
    @Override  
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {  
        return application.sources(SpringBootDemoApplication.class);  
    }  
}  

那么,我們可以這樣啟動springboot,注解類不用SpringApplication,配置類也可自行導入 

@Configuration  
@Import({  
        DispatcherServletAutoConfiguration.class,  
        EmbeddedServletContainerAutoConfiguration.class,  
        ErrorMvcAutoConfiguration.class,  
        HttpEncodingAutoConfiguration.class,  
        HttpMessageConvertersAutoConfiguration.class,  
        JacksonAutoConfiguration.class,  
        MultipartAutoConfiguration.class,  
        ServerPropertiesAutoConfiguration.class,  
        WebMvcAutoConfiguration.class  
})  
@ComponentScan  
public class SpringBootDemoApplication {  
    public static void main(String[] args) {  
        SpringApplication.run(SpringBootDemoApplication.class, args);  
    }  
}  

2.3、關鍵類EnableAutoConfiguration

2.3.1、EnableAutoConfiguration源碼解析

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

從上往下:

  • 首先,最關鍵的要屬@Import(AutoConfigurationImportSelector.class),借助AutoConfigurationImportSelector,@EnableAutoConfiguration可以幫助SpringBoot應用將所有符合條件的@Configuration配置都加載到當前SpringBoot創建並使用的IoC容器。(Import主要是配合Configuration來使用的,用來導出更多的Configuration類,ConfigurationClassPostProcessor會讀取Import的內容來實現具體的邏輯。)借助於Spring框架原有的一個工具類:SpringFactoriesLoader的支持,@EnableAutoConfiguration可以智能的自動配置(掃描每個jar中的spring.factories)!
  • 再看EnableAutoConfiguration的方法,就兩個方法exclude和excludeName,作用是自動配置過程中包含和排查指定的類。

2.3.2、自定義EnableAutoConfiguration示例

1、自定義EnableAutoConfiguration,這里ImportMyEnableAutoConfigurationImport

復制代碼
package com.dxz.autoconfig;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.context.annotation.Import;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(MyEnableAutoConfigurationImport.class)
public @interface MyEnableAutoConfiguration {
}
復制代碼

2、自定義EnableAutoConfigurationImport,注入了ClassLoader,並調用SpringFactoriesLoader.loadFactoryNames()方法,導出Configuration的類。

復制代碼
package com.dxz.autoconfig;

import java.util.List;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.type.AnnotationMetadata;

public class MyEnableAutoConfigurationImport implements DeferredImportSelector, BeanClassLoaderAware {
    private ClassLoader classLoader;

    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        List<String> beanNames = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader);
        return beanNames.toArray(new String[beanNames.size()]);
    }
}
復制代碼

3、入口類,這里使用了MyEnableAutoConfiguration注解。

復制代碼
package com.dxz.autoconfig;

import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Configuration
@MyEnableAutoConfiguration
public class CustomizeEnableAutoConfigure {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(CustomizeEnableAutoConfigure.class);
        application.run(args);
    }

    @Controller
    public static class MyController {
        @RequestMapping
        @ResponseBody
        public Map index() {
            Map<String, String> map = new HashMap<String, String>();
            map.put("hello", "world2");
            return map;
        }
    }
}
復制代碼

結果:

 2.4、condition包

2.4.1、condition示例講解

  springboot為我們提供了一批實用的XxxCondition,查看了他們的源碼后發現,他們都實現了spring提供的Condition接口,然后編寫對應的annotation。我們在使用他們的時候,只需要在需要的地方寫上這些annotation就好了。這些注解都在springboot提供的jar包中
package org.springframework.boot.autoconfigure.condition。

  提供這些condition主要目的:上面我們討論的AutoConfigurationImportSelector只能告訴Spring哪些類需要加載,但判斷所配置的類是否可以被加載(即Auto Config里的Auto)是一個非常繁瑣的邏輯,如果由某個中央控制系統來處理的話,必然會造成代碼耦合和復雜性猛增,因此SpringBoot最終使用了一貫的做法——將判斷是否加載的權限下放給了各個需要進行自動配置的需求方本身,這樣在springboot中擴展了很多condition。

基於Spring的@Conditional,SpringBoot提供了豐富的條件配置

@ConditionalOnClass : classpath中存在該類時起效 
@ConditionalOnMissingClass : classpath中不存在該類時起效 
@ConditionalOnBean : DI容器中存在該類型Bean時起效 
@ConditionalOnMissingBean : DI容器中不存在該類型Bean時起效 
@ConditionalOnSingleCandidate : DI容器中該類型Bean只有一個或@Primary的只有一個時起效 
@ConditionalOnExpression : SpEL表達式結果為true時 
@ConditionalOnProperty : 參數設置或者值一致時起效 
@ConditionalOnResource : 指定的文件存在時起效 
@ConditionalOnJndi : 指定的JNDI存在時起效 
@ConditionalOnJava : 指定的Java版本存在時起效 
@ConditionalOnWebApplication : Web應用環境下起效 
@ConditionalOnNotWebApplication : 非Web應用環境下起效

@AutoConfigureAfter:在指定的配置類初始化后再加載 
@AutoConfigureBefore:在指定的配置類初始化前加載 
@AutoConfigureOrder:數越小越先初始化

2.4.2、condition示例講解

1)@ConditionalOnBean/@ConditionalOnMissingBean當容器中存在/不存在某個bean時,加上此注解的bean被自動注入

package org.springframework.boot.autoconfigure.condition;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {

    Class<?>[] value() default {};

    String[] type() default {};

    Class<? extends Annotation>[] annotation() default {};

    String[] name() default {};

    SearchStrategy search() default SearchStrategy.ALL;
}

2)@ConditionalOnJava根據當前使用的JDK版本,判斷是否自動注入

    //使用jdk8才注入此bean @Bean @ConditionalOnJava(ConditionalOnJava.JavaVersion.EIGHT) public Runnable runnableBean2() { return () ->{}; } 

 3)@ConditionalOnProperty來控制Configuration是否生效

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

    String[] value() default {}; //數組,獲取對應property名稱的值,與name不可同時使用  
  
    String prefix() default "";//property名稱的前綴,可有可無
  
    String[] name() default {};//數組,property完整名稱或部分名稱(可與prefix組合使用,組成完整的property名稱),與value不可同時使用  
  
    String havingValue() default "";//可與name組合使用,比較獲取到的屬性值與havingValue給定的值是否相同,相同才加載配置  
  
    boolean matchIfMissing() default false;//缺少該property時是否可以加載。如果為true,沒有該property也會正常加載;反之報錯  
  
    boolean relaxedNames() default true;//是否可以松散匹配,至今不知道怎么使用的  
} 
通過其兩個屬性 name以及 havingValue來實現的,其中 name用來從 application.properties中讀取某個屬性值。
如果該值為空,則返回false;
如果值不為空,則將該值與havingValue指定的值進行比較,如果一樣則返回true;否則返回false。
如果返回值為false,則該configuration不生效;為true則生效。
@Configuration
//在application.properties配置"mf.assert",對應的值為true
@ConditionalOnProperty(prefix="mf",name = "assert", havingValue = "true")
public class AssertConfig {
    @Autowired
    private HelloServiceProperties helloServiceProperties;
    @Bean
    public HelloService helloService(){
        HelloService helloService = new HelloService();
        helloService.setMsg(helloServiceProperties.getMsg());
        return helloService;
    }
}
2.4.2.3、示例:(參考網絡)
    最近碰到個這樣的需求,需要同一套代碼適配個版本數據庫(數據庫不同,且部分表的字段及關聯關系可能會不同),即這套代碼配置不同的數據庫都能跑。項目采用的框架為SpringBoot+Mybatis。經過一番思考,思路如下:
    (1)在業務層(service)和數據訪問層(Mapper)之間添加一層適配層,用來屏蔽數據庫的差異
    (2)適配層中代碼均采用接口加實現類的方式,不同的數據庫用的實現類不同
    (3)業務層(service)中全部采用面向接口編程
    (4)項目啟動后只實例化和數據庫相匹配的適配層實現類
    實現上面的一個關鍵點是對bean的實例化添加一個條件判斷來控制。其實SpringBoot里面新增了很多條件注解,能實現這個功能。但是都有些局限性,最終是采用自定義條件注解的方案。
2.4.2.3.1)、通過SpringBoot自帶的注解ConditionalOnProperty實現
        這個注解不做過多的解釋,只說通過這個注解怎么實現我們的功能。
假設我們application.properties中配置一個配置項為
#bean實例化條件配置項
conditionKey: 1.0
    那么只需要加上@ConditionalOnProperty的name和havingValue就能實現,只有配置文件中name對應的配置項的值和havingValue內容一致才實例化這個對象。
針對我們上面配置的application.properties的內容,@ConditionalOnProperty的使用案例如下面代碼所示

ManageImpl1.java代碼如下:(MyManage接口、ManageImpl2省略)

package com.dxz.palmpay.condition;

import javax.annotation.PostConstruct;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

//僅當conditionKey==1.0的時候實例化這個類
@ConditionalOnProperty(name = "conditionKey", havingValue = "1.0")
@Component
public class ManageImpl1 implements MyManage {

    @Override
    public void sayHello() {
        System.out.println("我是實現類01");
    }

    //為了效果,創建后打印一些信息
    @PostConstruct
    public void init() {
        this.sayHello();
    }

}
 結果:
該配置放在配置中心同樣有效。
    這個注解的局限性這個注解的havingValue里面只能配置一個值。
    由於項目個性化需求,希望這個havingValue可以配置多個值,name對應的配置項的Value只要滿足havingValue里面多個值的就表示匹配正確。即,havingValue里面可以配置多個值,name對應配置項的值來和havingValue匹配時,采用邏輯或匹配,滿足一個值就算匹配正確。
2.4.2.3.2)、自定義條件注解

(1)思路

        注解里面有2個屬性,具體如下
  • name:String類型,用來接受application.properties的配置項的key
  • havingValue:String數組類型,用來和name對應key的Value進行匹配

(2)定義注解

package com.dxz.palmpay.condition;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;

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

    String name() default "";

    //havingValue數組,支持or匹配
    String[] havingValue() default {};

}

(3)定義注解的匹配規則

package com.dxz.palmpay.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Map;

/**
 * 自定義條件注解的驗證規則
 */
public class CustomOnPropertyCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Map<String, Object> annotationAttributes = annotatedTypeMetadata
                .getAnnotationAttributes(CustomConditionalOnProperty.class.getName());
        String propertyName = (String) annotationAttributes.get("name");
        String[] values = (String[]) annotationAttributes.get("havingValue");
        if (0 == values.length) {
            return false;
        }

        String propertyValue = conditionContext.getEnvironment().getProperty(propertyName);
        // 有一個匹配上就ok
        for (String havingValue : values) {
            if (propertyValue.equalsIgnoreCase(havingValue)) {
                return true;
            }
        }
        return false;
    }

}
  (4)使用案例
@Component
@CustomConditionalOnProperty(name = "db.version", havingValue = {"3"})
public class ManageImpl3 implements MyManage {

@Component
@CustomConditionalOnProperty(name = "db.version", havingValue = {"1","2","4"})
public class ManageImpl4 implements MyManage {

 

自定義Condition注解,主要就2步

(1)定義一個條件注解
(2)定義一個條件的校驗規則

 

參考:https://blog.csdn.net/lqzkcx3/article/details/82807888

 參考:https://www.cnblogs.com/zeng1994/p/8c10310d8a042d56eddd40635afb6e93.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM