SpringBoot入門(四)——自動配置

weixin_34320159發表於2018-08-24

本文來自網易雲社群


SpringBoot之所以能夠快速構建專案,得益於它的2個新特性,一個是起步依賴前面已經介紹過,另外一個則是自動配置。起步依賴用於降低專案依賴的複雜度,自動配置負責減少人工配置的工作量。


@EnableAutoConfiguration


前一篇留了一個註解沒介紹,@EnableAutoConfiguration註解是開啟自動配置的入口。其定義如下:


@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {

    ...
}


看到@Import註解,引數是AutoConfigurationImportSelector類。


先來補充下@Import的知識,最早是為了方便引入java配置類,後來進行了擴充套件,目前有一下功能:


  • 引數為@Configuration註解的bean,那麼就引入這個配置類。相當於用xml配置時的

  • 引數為ImportBeanDefinitionRegistrar介面或者ImportSelector介面的實現類,那麼就通過介面方法來實現自定義注入

  • 引數為普通類,直接將該類建立到Spring的IoC容器


這裡的AutoConfigurationImportSelector類屬於第二種情況,實現了ImportSelector介面。ImportSelector介面只宣告瞭一個方法,要求返回一個包含類全限定名的String陣列,這些類將會被Spring新增到IoC容器。


看下AutoConfigurationImportSelector的方法實現:


@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {    if (!isEnabled(annotationMetadata)) {        return NO_IMPORTS;
    }    try {
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);
        AnnotationAttributes attributes = getAttributes(annotationMetadata);        // 獲取候選配置的類路徑
        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 StringUtils.toStringArray(configurations);
    }    catch (IOException ex) {        throw new IllegalStateException(ex);
    }
}


通過某種方法獲取配置類的路徑,然後去重排序一系列處理之後返回出去交給Spring去處理。

接下來看下某種方法是個什麼操作:


    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {        // 從SpringFactoriesLoader取候選配置的類路徑
        // 第一個引數getSpringFactoriesLoaderFactoryClass()得到的是EnableAutoConfiguration.class
        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;
    }


呼叫了SpringFactoriesLoader.loadFactoryNames():


public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {        // "org.springframework.boot.autoconfigure.EnableAutoConfiguration"
        String factoryClassName = factoryClass.getName();        // 先調了下面的loadSpringFactories方法,得到一個Map
        // 從Map中用EnableAutoConfiguration類的全限定名取一個String列表(其實也是一些類的全限定名列表)
        return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
    }    // 上面方法先從這裡取了個Map
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {        // 這裡有個快取,記一下,後面會提到
        MultiValueMap<String, String> result = cache.get(classLoader);        if (result != null)            return result;        try {            // FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"
            // 載入所有META-INF包下的spring.factories檔案
            Enumeration<URL> urls = (classLoader != null ?
                    classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            result = new LinkedMultiValueMap<>();            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);                for (Map.Entry<?, ?> entry : properties.entrySet()) {                    // 逗號分隔的String轉為List
                    List<String> factoryClassNames = Arrays.asList(
                            StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
                    result.addAll((String) entry.getKey(), factoryClassNames);
                }
            }
            cache.put(classLoader, result);            return result;
        }        catch (IOException ex) {            throw new IllegalArgumentException("Unable to load factories from location [" +
                    FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
    }


整個流程其實就是讀取專案中的META-INF/spring.factories檔案,然後挑一挑交給Spring去加到上下文中。

那麼META-INF/spring.factories到底是個什麼gui呢。以之前建立的Hello World Web為例,工程中一共有2個META-INF/spring.factories檔案,一個在spring-boot包中,一個在spring-boot-autoconfigure包中。


看下spring-boot-autoconfigure包中的內容(片段):


# Initializersorg.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener# Application Listenersorg.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer# Auto Configuration Import Listenersorg.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener# Auto Configuration Import Filtersorg.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\

...


看到一堆的xxxConfiguration,沒錯,這些都註解了@Configuration。spring-boot-autoconfigure為我們引入了一大堆的java配置類作為元件的預設配置。


包括常見元件的預設配置,有rabbit相關的,有redis相關的,有Security相關的等等。


那麼問題來了,難道所有的配置類都會被載入嗎,答案是顯而易見的。那麼來看看是如何實現的,以Redis的配置類RedisAutoConfiguration為例,看下類原始碼:

@Configuration// 當存在RedisOperations時,才會載入該配置類@ConditionalOnClass(RedisOperations.class)@EnableConfigurationProperties(RedisProperties.class)@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })public class RedisAutoConfiguration {    @Bean
    // 當不存在名為"redisTemplate"的bean時,才會建立該bean
    @ConditionalOnMissingBean(name = "redisTemplate")    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);        return template;
    }    @Bean
    // 當Spring上下文中不存在StringRedisTemplate類例項的時候,才會建立該bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)    public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);        return template;
    }
}


這裡利用了Spring條件註解的特性,通過設定一定的條件來實現不同場景下載入不同的配置。


自動配置類生效的條件通常是我們引入了相關的元件,如果沒有引入元件,那麼就算包含在spring.factories檔案中也不會被載入。


而是否要注入Bean則要看當前上下文中是否已經存在相應的Bean。如果不存在,那麼由預設配置來補充。如果已經存在了,自動配置會不滿足註解條件,就不會被建立。


有了這兩點,可以做到當我們不做任何配置的時候可以用預設配置來運用新元件,而當我們需要對配置進行調整的時候用自定義的配置來覆蓋即可。

Tips


上面原始碼中標記了一個快取。在讀取META-INF/spring.factories檔案後相關資料是會儲存到SpringFactoriesLoader類的快取中的。而這裡第一次讀取META-INF/spring.factories的時機並不是在自動配置這裡,早在上一篇的SpringApplication構造方法中就已經快取了:


public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));    this.webApplicationType = deduceWebApplicationType();    // 設定初始化器,這裡就已經讀取了META-INF/spring.factories
    setInitializers((Collection) getSpringFactoriesInstances(
        ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));    this.mainApplicationClass = deduceMainApplicationClass();
}


小結


這一節通過@EnableAutoConfiguration註解分析了SpringBoot自動配置的實現。通過@Import註解新增自動配置選擇器(AutoConfigurationImportSelector),選擇器中首先讀取META-INF路徑下的spring.factories檔案。在spring.factories檔案中,SpringBoot官方提供了許多常見元件的預設配置,以java配置類形式存在。在這些java配置類中又利用了Spring的條件註解,讓我們可以在預設配置和自定義配置之間靈活切換。


可以這麼認為:SpringBoot在Spring原有的基礎上,通過拼湊組合又實現了一個強大的特性——自動配置。


自動配置讓我們可以在不做任何配置的情況下直接使用一個新的類庫(前提是足夠普遍),也能滿足我們自定義配置的需求。除此之外,我們還可以利用這個思路,實現具有團隊特色的自動配置,讓團隊開發也更加高效。


 







相關閱讀:SpringBoot入門(一)——開箱即用


SpringBoot入門(二)——起步依賴


SpringBoot入門(三)——入口類解析


SpringBoot入門(四)——自動配置


SpringBoot入門(五)——自定義配置

 

網易雲新使用者大禮包:https://www.163yun.com/gift

 

本文來自網易實踐者社群,經作者金港生授權釋出。



相關文章