SpringBoot自動裝配


SpringBoot是對Spring的一種擴展,其中比較重要的擴展功能就是自動裝配:通過注解對常用的配置做默認配置,簡化xml配置內容。本文會對Spring的自動配置的原理和部分源碼進行解析,本文主要參考了Spring的官方文檔

自動裝配的組件

SpringBoot自動裝配通過多部分組件協調完成,這些組件主要有下面幾種,這幾種組件之間協調工作,最終完成了SpringBoot的自動裝配。

  1. @EnableAutoConfiguration:用於根據用戶所引用的jar包自動裝配Spring容器,比如用戶在ClassPath中包含了HSQLDB,但是沒有手動配置數據庫連接,那么Spring會自動使用HSQLDB作為數據源。
  2. @Condition:不同情況下按照條件進行裝配,Spring的JdbcTemplate是不是在Classpath里面?如果是,並且DataSource也存在,就自動配置一個JdbcTemplate的Bean
  3. @ComponentScan:掃描指定包下面的@Component注解的組件。

@EnableAutoConfiguration注解

Spring的自動裝配發展大致可以分為三個階段:

  1. 全手工配置的XML文件階段,用戶需要的Bean全部需要在XML文件中聲明,用戶手工管理全部的Bean。
  2. 半手工配置的注解階段,用戶可以安裝需求Enable對應的功能模塊,如添加@EnableWebMvc可以啟用MVC功能。
  3. 全自動配置的SpringBoot,用戶只需要引入對應的starter包,Spring會通過factories機制自動裝配需要的模塊。

全手工配置的XML文件示意圖:

xml手工裝配

半自動注解配置示意圖:

半自動配置

全自動注解配置示意圖:

全自動配置

Spring啟用全自動配置功能的注解就是@EnableAutoConfiguration,應用添加了@EnableAutoConfiguration注解之后,會讀取所有jar包下面的spring.factories文件,獲取文件中配置的自動裝配模塊,然后去裝配對應的模塊。

@EnableAutoConfiguration的功能可總結為:使Spring啟用factories機制導入各個starter模塊的配置。

原理分析

通過上面的分析我們知道Spring的@EnableAutoConfiguration主要功能是使Spring啟用factories機制導入各個starter模塊的配置。下面我們會對@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 {};
}

@EnableAutoConfiguration注解的定義有兩部分比較重要的內容:
@AutoConfigurationPackage:將添加該注解的類所在的package作為自動配置package進行管理。
@Import({AutoConfigurationImportSelector.class}):用於導入factories文件中的AutoConfiguration。

@Import({AutoConfigurationImportSelector.class})

首先我們需要知道@Import注解的作用,從字面意思就可以看出來,@Import用於把一個Bean注入到Spring的容器中,@Import可以導入三種類型的Bean:

  1. 導入普通的Bean,通常是@Configuration注解的Bean,也可以是任意的@Component組件類型的類。
  2. 導入實現了ImportSelector接口的Bean,ImportSelector接口可以根據注解信息導入需要的Bean。
  3. 導入實現了ImportBeanDefinitionRegistrar注解的Bean, ImportBeanDefinitionRegistrar接口可以直接向容器中注入指定的Bean。

@Import({AutoConfigurationImportSelector.class})中的AutoConfigurationImportSelector實現了ImportSelector接口,會按照注解內容去裝載需要的Bean。

    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            // 獲取需要自動裝配的AutoConfiguration列表
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            // 獲取自動裝配類的類名稱列表
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

    // 獲取需要自動裝配的AutoConfiguration列表
    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            // 獲取注解中的屬性
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);

            // 獲取所有META-INF/spring.factories中的AutoConfiguration類
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

            // 刪除重復的類
            configurations = this.removeDuplicates(configurations);

            // 獲取注解中Execlud的類
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);

            // 移除所有被Exclude的類
            configurations.removeAll(exclusions);

            // 使用META-INF/spring.factories中配置的過濾器
            configurations = this.getConfigurationClassFilter().filter(configurations);

            // 廣播相關的事件
            this.fireAutoConfigurationImportEvents(configurations, exclusions);

            // 返回符合條件的配置類。
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

@AutoConfigurationPackage

@AutoConfigurationPackage用於將添加該注解的類所在的package作為自動配置package進行管理,聽起來是不是和@ComponentScan功能有所重復?我們來分析一下其具體實現,可以看到這個注解依舊是通過@Import注解向容器中注冊Bean。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

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

@AutoConfigurationPackage注解導入了Registrar.class,其本質是一個ImportBeanDefinitionRegistrar,會把當前注解類所在的包注入到Spring容器中。

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
        }
}

@ComponentScan並不會把類所在的包注入到容器中,@ComponentScan只注入指定的包。類所在的包通過@AutoConfigurationPackage注入。

@Conditional注解

@Conditional注解的組件只有在滿足特定條件的情況下才會被注冊到容器中,@Conditional注解的定義如下所示,可以看到這個注解只有一個內容:Condition,所以這個注解的重點就是Condition接口。

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

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

}

Condition接口的定義如下所示,該接口只包含一個方法,輸入當前的上下文信息和注解的參數,判斷注解的Bean是否可以注冊到Spring容器中。其中上下文信息包含了:Bean定義管理器(BeanDefinitionRegistry)/BeanFactory/上下文環境Environment/資源加載器ResourceLoader/類加載器ClassLoader。

@FunctionalInterface
public interface Condition {

    /**
     * Determine if the condition matches.
     * @param context the condition context
     * @param metadata the 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);

}

@Conditional判斷時機

@Conditionl注解作用於Spring讀取Bean定義的階段,這個階段大多數Bean尚未實例化,少數實例化的Bean屬於Spring的特殊Bean,不能條件控制是否加載。

Spring中的Bean有很多來源,如掃描包下的Component、@Bean注解的方法、@Import、用戶手工注冊Bean等方法注冊Bean,這些所有來源的Bean定義都可以使用@Conditional進行處理嗎?答案是不是所有Bean定義來源都會使用@Conditional注解進行過濾,只有掃描包或者@Configuration注解類中的的Bean會使用@Conditionl注解進行判斷。

  • 掃描包注冊Bean,我們知道基於注解的Spring容器會通過掃描包的形式去獲取路徑下面的Component,這部分Bean定義是最早被加載到Spring容器中的。Spring通過AnnotatedBeanDefinitionReader讀取並注冊@Component對應的Bean定義,其中@Conditional判斷的部分邏輯如下:

        private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
                @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
                @Nullable BeanDefinitionCustomizer[] customizers) {
    
            AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
            // 此處判斷Bean是否應該被注冊
            if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
                return;
            }
            // 省略其它注冊邏輯
        }
    
  • @Bean方法注冊,在@Configuration中的@Bean和不在@Configuration中的@Bean的模式不同,我在其它文章中有介紹,但是獲取Bean定義的邏輯是一致的,@Bean注解的方法是通過ConfigurationClassBeanDefinitionReader去讀取的,其中也包含了對@Conditional注解的判斷,其中部分關鍵判斷代碼如下所示。

        private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
            ConfigurationClass configClass = beanMethod.getConfigurationClass();
            MethodMetadata metadata = beanMethod.getMetadata();
            String methodName = metadata.getMethodName();
    
            // Do we need to mark the bean as skipped by its condition?
            if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
                configClass.skippedBeanMethods.add(methodName);
                return;
            }
            if (configClass.skippedBeanMethods.contains(methodName)) {
                return;
            }
    

    @Configuration類中的@Bean注解是在ConfigurationClassPostProcessor中進行解析和注冊的,這是一個BeanDefinitionRegistryPostProcessor,執行時機顯然晚於掃描包的時機。

從@Conditional的判斷原理可以看出,Spring應當只允許Bean定義一個個進行注冊,並且要嚴格保證讀取順序,不允許Bean定義的批量注冊。

@Conditional擴展

為了簡化用戶的使用,Spring提供了幾種常見的@Conditional的實現,我們下文中會介紹常見的幾種實現。

  • @ConditionalOnBean注解,當特定的Bean注解存在的時候注冊當前Bean,使用這個注解的時候要注解Bean的先后順序,因為@ConditionalOnBean會去當前容器中查找是否有滿足條件的Bean,后注冊的Bean會受先注冊Bean的影響。
  • @ConditionalOnClass注解,當前類路徑中包含指定類的時候注冊當前Bean。
  • @ConditionalOnMissingBean注解,當指定類型的Bean不存在的時候注冊當前Bean。
  • @ConditionalOnMissingClass注解,當前類路徑中不包含指定類的時候注冊當前Bean。
  • @ConditionalOnProperty注解,指定屬性滿足特定值是生效。

值得一提的是,@Profile注解本身也是使用@Conditional注解進行Bean的條件注冊的。

@Conditional(ProfileCondition.class)

public @interface Profile {

/**

* The set of profiles for which the annotated component should be registered.

*/

String[] value();

}

@ComponentScan組件

有些情況下,我們的組件不都定義在帶有@EnableAutoConfiguration注解的類對應的包下面,這個時候@AutoConfigurationPackage掃描的類就不能滿足用戶的需求了。Spring提供了@ComponentScan組件讓用戶添加指定包下面的Spring組件到Spring容器中。

Filter選項

掃描包過程中,Spring允許用戶按照條件過濾所需要的Bean,@SpringBootApplication中本身包含了@ComponentScan注解,並為注解配置了兩個Filter:TypeExcludeFilter和AutoConfigurationExcludeFilter注解。這兩個注解允許用戶實現特定的類,從而實現對Bean定義的過濾。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // 省略屬性
}

@Import原理

通過上面的內容,我們知道@Import在Spring的自動裝配中有很重要的作用,用於自動裝配過程中導入指定的配置類。接下來我們分析一下@Import注解的源碼及其作用機制。

@Import的解析依舊是在關鍵類ConfigurationClassPostProcessor中進行的,ConfigurationClassPostProcessor包含了@Bean、@Import等注解的解析。

    private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
            Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
            boolean checkForCircularImports) {

        if (importCandidates.isEmpty()) {
            return;
        }

        if (checkForCircularImports && isChainedImportOnStack(configClass)) {
            this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
        }
        // 其它邏輯
    }

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先發布至微信公眾號,版權所有,禁止轉載!


免責聲明!

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



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