1. 程式人生 > >spring boot 自動配置功能解剖

spring boot 自動配置功能解剖

    說起spring boot和spring的區別,大家第一反應就是spring boot 少了很多配置,但不是說少了很多配置很多功能就沒有了,或者比spring就少了很多功能,而是spring boot 自己約定了很多預設配置,讓大家感覺不到其中的一些資訊

問題來了,spring boot的自動配置怎麼實現的?今天我將自己這2天學習到的東西分享一下,有不對的地方可以說出來一起討論

首先我們先看一下下面這個註解:

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

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

    String[] excludeName() default {};
}

看到@import那個註解了嗎?這個註解就是會把第三方jar的類載入到當前spring容器,接下來我們看一下import的這個類,原始碼如下

@Deprecated
public class EnableAutoConfigurationImportSelector extends AutoConfigurationImportSelector {
    public EnableAutoConfigurationImportSelector() {
    }

    protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass().equals(EnableAutoConfigurationImportSelector.class) ? ((Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true)).booleanValue() : true;
    }
}

下面是父類AutoConfigurationImportSelector 中關鍵2個方法的原始碼

   public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) { 

//判斷 enableautoconfiguration註解有沒有開啟,預設開啟

            return NO_IMPORTS;
        } else {
            try {
//第一部分 :獲取 META-INF/spring-autoconfigure-metadata.properties 的配置資料
                AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);

                AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//第二部分 :獲取 META-INF/spring.factoies 的類相關資料
                List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//去重
                configurations = this.removeDuplicates(configurations);、
//排序
                configurations = this.sort(configurations, autoConfigurationMetadata);
//第三部分:去除一些多餘的類,根據EnableAutoConfiguration 註解的一個exclusions屬性
                Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
                this.checkExcludedClasses(configurations, exclusions);
                configurations.removeAll(exclusions);
//第四部:根據OnClassCondition註解過濾調一些條件沒有滿足的
                configurations = this.filter(configurations, autoConfigurationMetadata);
//第五部:廣播事件
                this.fireAutoConfigurationImportEvents(configurations, exclusions);
                return (String[])configurations.toArray(new String[configurations.size()]);
            } catch (IOException var6) {
                throw new IllegalStateException(var6);
            }
        }
    }

    protected boolean isEnabled(AnnotationMetadata metadata) {
        return true;
    }

首先程式是先呼叫EnableAutoConfigurationImportSelector 的selectImport方法,也就是父類的這個方法,下面我們先研究一下程式碼

大家看上面原始碼的第一部分,獲取spring-autoconfigure-metadata.properties的程式碼,深究進去的程式碼如下

 public static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
        return loadMetadata(classLoader, "META-INF/spring-autoconfigure-metadata.properties");
    }

    static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
        try {
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources(path) : ClassLoader.getSystemResources(path);
            Properties properties = new Properties();

            while(urls.hasMoreElements()) {
                properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource((URL)urls.nextElement())));
            }

            return loadMetadata(properties);
        } catch (IOException var4) {
            throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", var4);
        }
    }

ClassLoader.getResource()方法找到具有給定名稱的資源。資源是一些資料(影象,音訊,文字等),返回URL物件讀取資源。

該方法就是獲取該目錄下的配置資料

第二部分:道理跟第一部分一樣獲取相關類的資料

第三步就是去除一些不用的class,這是具體過濾的程式碼

  private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
        long startTime = System.nanoTime();
        String[] candidates = (String[])configurations.toArray(new String[configurations.size()]);
        boolean[] skip = new boolean[candidates.length];
        boolean skipped = false;
        Iterator var8 = this.getAutoConfigurationImportFilters().iterator();

        while(var8.hasNext()) {
            AutoConfigurationImportFilter filter = (AutoConfigurationImportFilter)var8.next();
            this.invokeAwareMethods(filter);
            boolean[] match = filter.match(candidates, autoConfigurationMetadata);

            for(int i = 0; i < match.length; ++i) {
                if (!match[i]) {
                    skip[i] = true;
                    skipped = true;
                }
            }
        }

        if (!skipped) {
            return configurations;
        } else {
            List<String> result = new ArrayList(candidates.length);

            int numberFiltered;
            for(numberFiltered = 0; numberFiltered < candidates.length; ++numberFiltered) {
                if (!skip[numberFiltered]) {
                    result.add(candidates[numberFiltered]);
                }
            }

            if (logger.isTraceEnabled()) {
                numberFiltered = configurations.size() - result.size();
                logger.trace("Filtered " + numberFiltered + " auto configuration class in " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
            }

            return new ArrayList(result);
        }
    }

AutoConfigurationImportFilter 是一個介面,OnClassCondition才是它的實現類,主要功能就是第二部載入的類中不是所有都是要載入的,spring boot 提供了很多條件註解,具體如下

@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應用環境下起效

以上註解都是元註解@Conditional演變而來的,過濾調一些沒有滿足條件的class

第五步:廣播事件

 private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
//獲取監聽者
        List<AutoConfigurationImportListener> listeners = this.getAutoConfigurationImportListeners();
        if (!listeners.isEmpty()) {
//產生相應的事件(自動載入類,配置資料和不滿足條件的類)
            AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
            Iterator var5 = listeners.iterator();

            while(var5.hasNext()) {
                AutoConfigurationImportListener listener = (AutoConfigurationImportListener)var5.next();
                this.invokeAwareMethods(listener);
//觸發事件
                listener.onAutoConfigurationImportEvent(event);
            }
        }

    }

關鍵的一個問題來了,回到源頭,專案啟動之後什麼時候會去執行AutoConfigurationImportSelector的selectImports方法?

查了一下資料,其實spring boot是從我們SpringApplication.run方法開始最終執行最終執行到selectImports方法,然後將selectImports方法得到的資料注入到容器裡面

springApplication.run----->refreshContext()----->AbstractApplicationContext.refresh---->PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors----->ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry----------->ConfigurationClassPostProcessor.processConfigBeanDefinitions------>ConfigurationClassParser.parse

最終到如下程式碼

    private void processDeferredImportSelectors() {
        List<ConfigurationClassParser.DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
        this.deferredImportSelectors = null;
        Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR);
        Iterator var2 = deferredImports.iterator();

        while(var2.hasNext()) {
            ConfigurationClassParser.DeferredImportSelectorHolder deferredImport = (ConfigurationClassParser.DeferredImportSelectorHolder)var2.next();
            ConfigurationClass configClass = deferredImport.getConfigurationClass();

            try {
//執行匯入自動化資料配置
                String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
//處理這些資料,注入容器
                this.processImports(configClass, this.asSourceClass(configClass), this.asSourceClasses(imports), false);
            } catch (BeanDefinitionStoreException var6) {
                throw var6;
            } catch (Throwable var7) {
                throw new BeanDefinitionStoreException("Failed to process import candidates for configuration class [" + configClass.getMetadata().getClassName() + "]", var7);
            }
        }

    }

好了,spring boot的自動化注入大致講解完了,

現在又有一個問題了,如果自己要做一個類似於第三方的jar,讓當前的容器載入我這個第三方的bean怎麼做呢?其實也是非常簡單。

第一步:肯定是新建一個spring boot專案A

第二步:定義類

public class people{
}
@Configuration
public class MyConfig {
 
    @Bean
    public People people (){
        return new People();
    }
}

第三步:將A的jar放到B中

第四部:在B中寫如下測試程式碼,並啟動

@EnableAutoConfiguration
@ComponentScan
public class Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        People people = context.getBean(People .class);
        System.out.println(people );
    }
}

不過這樣啟動會報錯,找不到這個類,那是因為你的第五步沒有做

第五步:在core-bean專案resource下新建資料夾META-INF,在資料夾下面新建spring.factories檔案,檔案中配置,key為自定配置類EnableAutoConfiguration的全路徑,value是配置類的全路徑(就上面分析spring boot自動配置的那個spring.factories檔案一樣的道理)

這樣之後就可以實現了

到此為止大部分該講的也都講完了,如果有什麼不對的地方,可以提出來討論