SpringBoot是對Spring的一種擴展,其中比較重要的擴展功能就是自動裝配:通過注解對常用的配置做默認配置,簡化xml配置內容。本文會對Spring的自動配置的原理和部分源碼進行解析,本文主要參考了Spring的官方文檔。
自動裝配的組件
SpringBoot自動裝配通過多部分組件協調完成,這些組件主要有下面幾種,這幾種組件之間協調工作,最終完成了SpringBoot的自動裝配。
- @EnableAutoConfiguration:用於根據用戶所引用的jar包自動裝配Spring容器,比如用戶在ClassPath中包含了HSQLDB,但是沒有手動配置數據庫連接,那么Spring會自動使用HSQLDB作為數據源。
- @Condition:不同情況下按照條件進行裝配,Spring的JdbcTemplate是不是在Classpath里面?如果是,並且DataSource也存在,就自動配置一個JdbcTemplate的Bean
- @ComponentScan:掃描指定包下面的@Component注解的組件。
@EnableAutoConfiguration注解
Spring的自動裝配發展大致可以分為三個階段:
- 全手工配置的XML文件階段,用戶需要的Bean全部需要在XML文件中聲明,用戶手工管理全部的Bean。
- 半手工配置的注解階段,用戶可以安裝需求Enable對應的功能模塊,如添加@EnableWebMvc可以啟用MVC功能。
- 全自動配置的SpringBoot,用戶只需要引入對應的starter包,Spring會通過factories機制自動裝配需要的模塊。
全手工配置的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:
- 導入普通的Bean,通常是@Configuration注解的Bean,也可以是任意的@Component組件類型的類。
- 導入實現了ImportSelector接口的Bean,ImportSelector接口可以根據注解信息導入需要的Bean。
- 導入實現了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));
}
// 其它邏輯
}

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