SpringBoot 2.x 系列:自動裝配

概覽

上一節介紹了SpringBoot中的配置體系,接下來將會介紹SpringBoot的中最基礎、最核心的自動裝配(AutoConfiguration)機制。正是有了自動裝配機制,咱們能夠很快的搭建一個Web項目,實現零配置,java

看下SpringBoot是如何幫忙咱們實現自動裝配的,咱們首先從@SpringBootApplication註解提及:git

自動裝配過程

@SpringBootApplication 啓動類註解

每一個SpringBoot項目都會有一個啓動類,該類位於代碼的根目錄下,通常用XXXApplication命名,@SpringBootApplication註解會添加到該類上。@SpringBootApplication 註解位於 spring-boot-autoconfigure 工程的 org.springframework.boot.autoconfigure 包中,定義以下:github

@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 {
 @AliasFor(annotation = EnableAutoConfiguration.class)
 Class<?>[] exclude() default {};
 
 @AliasFor(annotation = EnableAutoConfiguration.class)
 String[] excludeName() default {};
 
 @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
 String[] scanBasePackages() default {};
 
 @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
 Class<?>[] scanBasePackageClasses() default {};
}

該註解相比較顯得有點複雜,從定義上看,@SpringBootApplication註解是一個組合註解,它是由三個註解組合而成,分別是:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScanspring

咱們能夠經過exclude 和 excludeName 屬性來配置不須要實現自動裝配的類或類名,也能夠經過 scanBasePackages 和 scanBasePackageClasses 屬性來配置須要進行掃描的包路徑和類路徑。接下來咱們分別對這三個註解一一介紹:後端

@SpringBootConfiguration

@SpringBootConfiguration註解比較簡單,定義以下:springboot

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
 @AliasFor(
 annotation = Configuration.class
 )
 boolean proxyBeanMethods() default true;
}

從定義來看,@SpringBootConfiguration等同於@Configuration註解,@Configuration比較常見,用來標識該類是JavaConfig配置類ide

@ComponentScan

@ComponentScan 註解不是 Spring Boot 引入的新註解,而是屬於 Spring 容器管理的內容。@ComponentScan 註解就是掃描基於 @Component 等註解所標註的類所在包下的全部須要注入的類,並把相關 Bean 定義批量加載到容器中。模塊化

@EnableAutoConfiguration 自動配置註解

@EnableAutoConfiguration 這個註解一看名字就很牛,它是SpringBoot實現自動裝配的核心註解,先看定義:spring-boot

@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 {};
​
}

又是一個組合註解,是由@AutoConfigurationPackage和@Import(AutoConfigurationPackages.Registrar.class)組合成,下面分別來介紹:ui

@AutoConfigurationPackage 註解

@AutoConfigurationPackage註解定義以下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
​
}

該註解的做用是將添加該註解的類所在的包做爲自動裝配的包路徑進行管理。在@AutoConfigurationPackage註解的定義中,咱們又發現了一個@Import註解,@Import 註解是由 Spring 提供的,做用是將某個類實例化並加入到 Spring IoC 容器中。因此咱們要想知道 @Import(AutoConfigurationPackages.Registrar.class) 究竟作了什麼就須要瞭解Registrar這個類裏包含了哪些方法。

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
​
 @Override
 public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
 register(registry, new PackageImport(metadata).getPackageName());
 }
​
 @Override
 public Set<Object> determineImports(AnnotationMetadata metadata) {
 return Collections.singleton(new PackageImport(metadata));
 }
​
}

Registrar類裏一共有兩個方法,分別是determineImports和registerBeanDefinitions,

new PackageImport(metadata).getPackageName()返回的就是@SpringBootApplication註解所在的類的包名

@Import(AutoConfigurationImportSelecor.class) 自動導入配置文件選擇器

接下來咱們回到@EnableAutoConfiguration註解中的 @Import(AutoConfigurationImportSelecor.class) 部分來,

這個類下有個重要的方法selectImports方法,Spring會把該方法返回的全部的類加載到IOC的容器中。因此這個類的主要做用是選擇Spring加載哪些類到IOC容器中。

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
 if (!isEnabled(annotationMetadata)) {
 return NO_IMPORTS;
 }
 AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
 .loadMetadata(this.beanClassLoader);
 AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
 annotationMetadata);
 return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
​
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
 AnnotationMetadata annotationMetadata) {
 if (!isEnabled(annotationMetadata)) {
 return EMPTY_ENTRY;
 }
 AnnotationAttributes attributes = getAttributes(annotationMetadata);
 
 //獲取 configurations集合
 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
 configurations = removeDuplicates(configurations);
 Set<String> exclusions = getExclusions(annotationMetadata, attributes);
 checkExcludedClasses(configurations, exclusions);
 configurations.removeAll(exclusions);
 configurations = filter(configurations, autoConfigurationMetadata);
 fireAutoConfigurationImportEvents(configurations, exclusions);
 return new AutoConfigurationEntry(configurations, exclusions);
 }

這段代碼的核心是經過 getCandidateConfigurations 方法獲取 configurations 集合並進行過濾。getCandidateConfigurations 方法以下所示:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
 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()方法,該方法會從META-INF/spring.factories文件中去查找自動配置的類,在這裏,不得不提到JDK的SPI機制,由於不管從 SpringFactoriesLoader 這個類的命名上,仍是 META-INF/spring.factories 這個文件目錄,二者之間都存在很大的相通性。

Java SPI 機制和 SpringFactoriesLoader

要想理解 SpringFactoriesLoader 類,咱們首先須要瞭解 JDK 中 SPI(Service Provider Interface,服務提供者接口)機制。簡單的說SPI機制就是爲接口尋找服務實現的機制,有點相似Spring的IOC的思想,不過將自動裝配的控制權移到程序外部,能夠避免程序中使用硬編碼,在模塊化設計中這種機制尤其重要。使用Java SPI機制須要如下幾步:

  • 當服務提供者提供了接口的一種具體實現後,在jar包的META-INF/services目錄下建立一個以「接口全路徑名」爲命名的文件,內容爲實現類的全路徑名
  • 當外部程序裝配這個 jar 包時,就能經過該 jar 包 META-INF/services/ 目錄中的配置文件找到具體的實現類名,並裝載實例化,從而完成模塊的注入

SpringFactoriesLoader 相似這種 SPI 機制,只不過以服務接口命名的文件是放在 META-INF/spring.factories 文件夾下,定義了一個key爲EnableAutoConfiguration,SpringFactoriesLoader 會查找全部 META-INF/spring.factories 文件夾中的配置文件,並把 Key 爲 EnableAutoConfiguration 所對應的配置項經過反射實例化爲配置類並加載到容器中。

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
 MultiValueMap<String, String> result = cache.get(classLoader);
 if (result != null) {
 return result;
 }
​
 try {
 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 factoryTypeName = ((String) entry.getKey()).trim();
 for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
 result.add(factoryTypeName, factoryImplementationName.trim());
 }
 }
 }
 cache.put(classLoader, result);
 return result;
 }
 catch (IOException ex) {
 throw new IllegalArgumentException("Unable to load factories from location [" +
 FACTORIES_RESOURCE_LOCATION + "]", ex);
 }
}

以上就是SpringBoot中基於 @SpringBootApplication註解實現自動裝配的基本過程和原理,不過SpringBoot默認狀況下給咱們提供了100多個AutoConfiguration的類

# Auto Configure
org.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.CloudServiceConnectorsAutoConfiguration,
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,

顯然咱們不可能把全部的類所有引入,因此在自動裝配的時候,須要會根據類路徑去尋找是否有對應的配置類,若是有的話還須要按照條件進行判斷,來決定是否要裝配,這裏我要引出SpringBoot中常常要用到的另一批註解@ConditionalOn系列註解。

@ConditionalOn 系列條件註解

當咱們構建一個 Spring 應用的時候,有時咱們想在知足指定條件的時候纔將某個 bean 加載到應用上下文中, 在Spring 4.0 時代,咱們能夠經過 @Conditional 註解來實現這類操做,SpringBoot在 @Conditional註解的基礎上進行了細化,定義了一系列的@ConditionalOn條件註解:

@ConditionalOnProperty:只有當所提供的屬性屬於 true 時纔會實例化 Bean @ConditionalOnBean:只有在當前上下文中存在某個對象時纔會實例化 Bean @ConditionalOnClass:只有當某個 Class 位於類路徑上時纔會實例化 Bean @ConditionalOnExpression:只有當表達式爲 true 的時候纔會實例化 Bean @ConditionalOnMissingBean:只有在當前上下文中不存在某個對象時纔會實例化 Bean @ConditionalOnMissingClass:只有當某個 Class 在類路徑上不存在的時候纔會實例化 Bean @ConditionalOnNotWebApplication:只有當不是 Web 應用時纔會實例化 Bean

因爲@ConditionalOn 系列條件註解很是多,咱們無心對全部這些組件進行展開。事實上這些註解的實現原理也大體相同,咱們只須要深刻了解其中一個就能作到舉一反三。這裏咱們挑選 @ConditionalOnClass 註解進行展開,該註解定義以下:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
  Class<?>[] value() default {};
  String[] name() default {};
}

能夠看到, @ConditionalOnClass 註解自己帶有兩個屬性,一個 Class 類型的 value,一個 String 類型的 name,因此咱們能夠採用這兩種方式中的任意一種來使用該註解。同時 ConditionalOnClass 註解自己還帶了一個 @Conditional(OnClassCondition.class) 註解。因此, ConditionalOnClass 註解的判斷條件其實就包含在 OnClassCondition 這個類中。

OnClassCondition 是 SpringBootCondition 的子類,而 SpringBootCondition 又實現了Condition 接口。Condition 接口只有一個 matches 方法,以下所示:

@Override
public final boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        String classOrMethodName = getClassOrMethodName(metadata);
        try {
            ConditionOutcome outcome = getMatchOutcome(context, metadata);
            logOutcome(classOrMethodName, outcome);
            recordEvaluation(context, classOrMethodName, outcome);
            return outcome.isMatch();
        }
        //省略其餘方法
}

這裏的 getClassOrMethodName 方法獲取被添加了@ConditionalOnClass 註解的類或者方法的名稱,而 getMatchOutcome 方法用於獲取匹配的輸出。咱們看到 getMatchOutcome 方法其實是一個抽象方法,須要交由 SpringBootCondition 的各個子類完成實現,這裏的子類就是 OnClassCondition 類。在理解 OnClassCondition 時,咱們須要明白在 Spring Boot 中,@ConditionalOnClass 或者 @ConditionalOnMissingClass 註解對應的條件類都是 OnClassCondition,因此在 OnClassCondition 的 getMatchOutcome 中會同時處理兩種狀況。這裏咱們挑選處理 @ConditionalOnClass 註解的代碼,核心邏輯以下所示:

ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {
            List<String> missing = getMatches(onClasses, MatchType.MISSING, classLoader);
            if (!missing.isEmpty()) {
                return ConditionOutcome
                        .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
                                .didNotFind("required class", "required classes")
                                .items(Style.QUOTE, missing));
            }
            matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
                    .found("required class", "required classes").items(Style.QUOTE, getMatches(onClasses, MatchType.PRESENT, classLoader));
}

這裏有兩個方法值得注意,一個是 getCandidates 方法,一個是 getMatches 方法。首先經過 getCandidates 方法獲取了 ConditionalOnClass 的 name 屬性和 value 屬性。而後經過 getMatches 方法將這些屬性值進行比對,獲得這些屬性所指定的但在類加載器中不存在的類。若是發現類加載器中應該存在但事實上又不存在的類,則返回一個匹配失敗的 Condition;反之,若是類加載器中存在對應類的話,則把匹配信息進行記錄並返回一個 ConditionOutcome。

小結

至此整個SpringBoot自動配置的所有過程和基本原理已經講完了,內容不少,整個流程總結一個圖以下:
image

項目源碼

github:https://github.com/dragon8844/springboot-learning/tree/main/java-spi

最後說一句

若是這篇文章對您有所幫助,或者有所啓發的話,幫忙關注一下,您的支持是我堅持寫做最大的動力,多謝支持。

此外,關注公衆號:黑色的燈塔,專一Java後端技術分享,涵蓋Spring,Spring Boot,SpringCloud,Docker,Kubernetes中間件等技術。

相關文章
相關標籤/搜索