談談對 Spring Boot 自動裝配機制的理解

什么是 Spring Boot 自動裝配?
@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration
AutoConfigurationImportSelector
Spring Boot 中的 SPI 機制

Spring Boot 作為現在 Java 微服務開發中的中流砥柱,其開箱即用、免去 xml 配置等特點,高效便捷的使 Java 程序員進行業務開發。每次面試 Spring Boot 相關技術點時,其自動裝配原理也是重點關注對象。下面我將結合源碼針對 Spring Boot 實現自動配置做一個詳細的介紹。
閱讀完本文你能知道:

Spring Boot 誕生背景

什么是 Spring Boot 自動裝配?
Spring Boot 啟動時的自動配置的原理知識
Spring Boot 啟動時的自動配置的流程
對于 Spring Boot 一些常用注解的了解

一步一步 debug 從淺到深。
注意:本文的 Spring Boot 版本為 2.6.3。
Spring Boot 誕生背景
使用過 Spring 的小伙伴,一定有被 XML 配置統治的恐懼。
我們回顧下原來搭建一個 Spring MVC 的 helloword 的 web 項目( xml 配置的)我們是不是要在 pom 中導入各種依賴,然后各個依賴有可能還會存在版本沖突需要各種排除。當你歷盡千辛萬苦的把依賴解決了,然后還需要編寫 web.xml 、 springmvc.xml 配置文件等。
我們只想寫個 helloword 項目而已,確把一大把的時間都花在了配置文件和 jar 包的依賴上面。大大的影響了我們開發的效率,以及加大了 web 開發的難度。

為了簡化這復雜的配置、以及各個版本的沖突依賴關系,Spring Boot 就應運而生。

我們現在通過 idea 創建一個 Spring Boot 項目只要分分鐘就解決了,你不需要關心各種配置(基本實現零配置)。讓你真正的實現了開箱即用。它的出現不僅可以讓你把更多的時間都花在你的業務邏輯開發上,而且還大大的降低了 web 開發的門檻。所以 Spring Boot 還是比較善解人意,知道開發人員的痛點在哪。
為什么 Spring Boot 使用起來這么酸爽呢? 這得益于其自動裝配。自動裝配可以說是 Spring Boot 的核心,那究竟什么是自動裝配呢?

什么是 Spring Boot 自動裝配?

Spring Boot 有一個全局配置文件: application.properties 或 application.yml 。在這個全局文件里面可以配置各種各樣的參數比如:你想改個端口啦 server.port 或者想調整下日志的級別都可以配置。
更多其他可以配置的屬性可以參照 官網 。
這么多屬性,這些屬性在項目是怎么起作用的呢?
Spring Boot 項目看下來啥配置也沒有( application.properties 或 application.yml 除外),既然從配置上面找不到突破口,那么我們就只能從啟動類上面找入口了。啟動類也就一個光禿禿的一個 main 方法,類上面僅有一個注解 SpringBootApplication 這個注解是 Spring Boot 項目必不可少的注解。那么自動配置原理一定和這個注解有著千絲萬縷的聯系!
我們下面來一起看看這個注解吧。

探究自動裝配原理

@SpringBootApplication

@SpringBootApplication 標注在某個類上說明這個類是 Spring Boot 的主配置類, Spring Boot 就應該運行這個類的 main 方法來啟動 Spring Boot 應用;它的本質是一個 組合注解 ,我們點進去查看。

@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 {

這里最上面四個注解的話沒啥好說的,基本上自己實現過自定義注解的話,都知道分別是什么意思。關鍵就在于后面三個注解:

@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration
我們逐個分析下。

@SpringBootConfiguration

這個注解我們點進去就可以發現,它實際上就是一個 @Configuration 注解,這個注解大家應該很熟悉了,加上這個注解就是為了讓當前類作為一個配置類交由 Spring 的 IOC 容器進行管理,因為 Spring Boot 本質上還是 Spring,所以原屬于 Spring 的注解 @Configuration 在 Spring Boot 中也可以直接應用。

由此可見, @SpringBootConfiguration 注解的作用與 @Configuration 注解相同,都是標識一個可以被組件掃描器掃描的配置類,只不過 @SpringBootConfiguration 是被 Spring Boot 進行了重新封裝命名而已。

@ComponentScan

這個注解也很熟悉,用于定義 Spring 的掃描路徑,等價于在 xml 文件中配置 context:component-scan ,假如不配置掃描路徑,那么 Spring 就會默認掃描當前類所在的包及其子包中的所有標注了 @Component , @Service , @Controller 等注解的類。

@EnableAutoConfiguration

這個注解才是實現自動裝配的關鍵,點進去之后發現,它是一個由 @AutoConfigurationPackage 和 @Import 注解組成的復合注解。

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

看起來很多注解,實際上關鍵在 @Import 注解,它會加載
AutoConfigurationImportSelector 類,然后就會觸發這個類的 selectImports() 方法。根據返回的 String 數組(配置類的 Class 的名稱)加載配置類。
我們重點看下
AutoConfigurationImportSelector 。

AutoConfigurationImportSelector

AutoConfigurationImportSelector中的selectImport是自動裝配的核心實現,它主要是讀取META-INF/spring.factories文件,經過去重、過濾,返回需要裝配的配置類集合。

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
 
    if (!isEnabled(annotationMetadata)) {
 
        return NO_IMPORTS;
    }
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

我們點進 getAutoConfigurationEntry() 方法:

getAttributes 獲取 @EnableAutoConfiguration 中的 exclude 、 excludeName 等。
getCandidateConfigurations 獲取所有自動裝配的配置類,也就是讀取 spring.factories 文件,后面會再次說明。
removeDuplicates 去除重復的配置項。
getExclusions 根據 @EnableAutoConfiguration 中的 exclude 、 excludeName 移除不需要的配置類。
fireAutoConfigurationImportEvents 廣播事件。
最后根據多次過濾、判重返回配置類合集。

現在我們結合 getAutoConfigurationEntry() 的源碼來詳細分析一下:

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
 
    if (!isEnabled(annotationMetadata)) {
 
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

第 1 步:判斷自動裝配開關是否打開。

默認
spring.boot.enableautoconfiguration=true ,可在 application.properties 或 application.yml 中設置。

image.png

第 2 步 :

用于獲取 EnableAutoConfiguration 注解中的 exclude 和 excludeName 。

image.png

第 3 步:

獲取需要自動裝配的所有配置類,讀取 META-INF/spring.factories 。


image.png

我們點進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;
}

獲取候選配置了使用了 Spring Framework 自定義的 SPI 機制,使用 SpringFactoriesLoader#loadFactoryNames 加載了類路徑下
/META-INF/spring.factories 文件中的配置類,里面是以 key/value 形式存儲,其中一個 key 是 EnableAutoConfiguration 類的全類名,而它的 value 是一個以 AutoConfiguration 結尾的類名的列表。以 spring-boot-autoconfigure 模塊為例,其 spring.factories 內容如下。
不光是這個依賴下的 META-INF/spring.factories 被讀取到,所有 Spring Boot Starter 下的 META-INF/spring.factories 都會被讀取到。
如果,我們自定義一個 Spring Boot Starter,就需要創建 META-INF/spring.factories 文件。
第 4 步 :
到這里可能面試官會問你:“ spring.factories 中這么多配置,每次啟動都要全部加載么?”。
很明顯,這是不現實的。我們 debug 到后面你會發現, configurations 的值變小了。

image.png

雖然 133 個全場景配置項的自動配置啟動的時候默認全部加載。但實際經過后續處理后只剩下 25 個配置項真正加載進來。很明顯,Spring Boot 只會加載實際你要用到的場景中的配置類。這是如何做到的了?

image.png

按需加載

這里我們分析剩下的 25 個自動配置類,觀察到每一個自動配置類都有著 @Conditional 或者其派生條件注解。

  • @ConditionalOnBean:當容器里有指定 Bean 的條件下
  • @ConditionalOnMissingBean:當容器里沒有指定 Bean 的情況下
  • @ConditionalOnSingleCandidate:當指定 Bean 在容器中只有一個,或者雖然有多個但是指定首選 Bean
  • @ConditionalOnClass:當類路徑下有指定類的條件下
  • @ConditionalOnMissingClass:當類路徑下沒有指定類的條件下
  • @ConditionalOnProperty:指定的屬性是否有指定的值
  • @ConditionalOnResource:類路徑是否有指定的值
  • @ConditionalOnExpression:基于 SpEL 表達式作為判斷條件
  • @ConditionalOnJava:基于 Java 版本作為判斷條件
  • @ConditionalOnJndi:在 JNDI 存在的條件下差在指定的位置
  • @ConditionalOnNotWebApplication:當前項目不是 Web 項目的條件下
  • @ConditionalOnWebApplication:當前項目是 Web 項 目的條件下
@Configuration(
    proxyBeanMethods = false
)
// 檢查是否有該類才會進行加載
@ConditionalOnClass({
 RedisOperations.class})
// 綁定默認配置信息
@EnableConfigurationProperties({
 RedisProperties.class})
@Import({
 LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
 
    public RedisAutoConfiguration() {
 
    }
   ...
}

所以當 classpath 下存在某一個 Class 時,某個配置才會生效。

上面所有的注解都在做一件事:注冊 bean 到 Spring 容器。他們通過不同的條件不同的方式來完成:

  • @SpringBootConfiguration 通過與 @Bean 結合完成 Bean 的 JavaConfig 配置;
  • @ComponentScan 通過范圍掃描的方式,掃描特定注解注釋的類,將其注冊到 Spring 容器;
  • @EnableAutoConfiguration 通過 spring.factories 的配置,并結合 @Condition 條件,完成bean的注冊;
  • @Import 通過導入的方式,將指定的 class 注冊解析到 Spring 容器;

我們在這里畫張圖把 @SpringBootApplication 注解包含的幾個注解分別解釋一下。

image.png

我們現在提到自動裝配的時候,一般會和 Spring Boot 聯系在一起。但是,實際上 Spring Framework 早就實現了這個功能。Spring Boot 只是在其基礎上,通過 SPI 的方式,做了進一步優化。

SPI 全稱為 Service Provider Interface,是一種服務發現機制。為了被第三方實現或擴展的 API,它可以用于實現框架擴展或組件替換

SPI 機制本質是 將接口實現類的全限定名配置在文件中,并由服務加載器讀取配置文件,加載文件中的實現類,這樣可以在運行時,動態為接口替換實現類 。正因此特性,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能。

用生活中的例子說就是,你買了一臺小米的手機。但是你用的充電器并不一定非要是小米充電器,你可以拿其他廠商的充電器來進行充電,只要滿足協議、端口等要求,那么就是可以充電的。這也是一種熱拔插的思想,并不是固定死的。

換成代碼來說也是一樣的,我定義了一個接口,但是不想固定死具體的實現類,因為那樣如果要更換實現類就要改動源代碼,這往往是不合適的。

那么我也可以定義一個規范,在之后需要更換實現類或增加其他實現類時,遵守這個規范,我也可以動態的去發現這些實現類。

一個接口可以有很多實現,比如數據庫驅動,有 oracle,mysql,postgress 等等,他們都遵循JDBC 規范,為了解耦,我們可以抽象出一個高層的 Driver 接口,讓各個數據庫服務商去實現各自的驅動,在使用的時候我們可以選擇加載具體的實現方式,這時候我們就可以使用 SPI 這種技術。

JDK 中的 SPI

講到 JDK 中的 SPI ,我們不得不說 java.util.ServiceLoader 這個類,我們先跑起來。

1、創建一個接口,Message

public interface Message{
 
    void send()
}

2、在 resources 資源目錄下創建 META-INF/services 文件夾

3、在 services 文件夾中創建文件,以接口全名命名

4、創建接口實現類

public class SmsMessage implements Message{
 
    public void send(){
 
        System.out.println("send sms message");
    }
}

public class EmailMessage implements Message{
    
    public void send(){
 
        System.out.println("send email message");
    }
}

5、測試

public class TestServiceLoader{
 
    public static void main(String[] args){
 
        ServiceLoader<Message> messages = ServiceLoader.load(Message.class);
        for(Message msg : messages){
 
            msg.send();
        }
    }
}

打印結果:

send email message
send sms message

簡單一點來說,就是在 META-INF/services 下面定義個文件,然后通過一個特殊的類加載器,啟動的時候加載你定義文件中的類,這樣就能擴展原有框架的功能。

Spring Boot 中的 SPI 機制

在 Spring Boot 的自動裝配過程中,最終會加載 META-INF/spring.factories 文件,Spring Boot 是通過 SpringFactoriesLoader#loadFactoryNames 方法加載的。
Spring Boot 定義了一套接口規范,這套規范規定:Spring Boot 在啟動時會掃描外部引用 jar 包中的 META-INF/spring.factories 文件,將文件中配置的類型信息加載到 Spring 容器(此處涉及到 JVM 類加載機制與 Spring 的容器知識),并執行類中定義的各種操作。對于外部 jar 來說,只需要按照 Spring Boot 定義的標準,就能將自己的功能裝置進 Spring Boot。
本文我們主要介紹了 Spring Boot 自動裝配原理。
簡單來說, Spring Boot 通過 @EnableAutoConfiguration 開啟自動裝配,通過 SpringFactoriesLoader 最終加載 META-INF/spring.factories 中的自動配置類實現自動裝配,自動配置類其實就是通過 @Conditional 按需加載的配置類,想要其生效必須引入 spring-boot-starter-xxx 包實現起步依賴。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容