你來說一下springboot的啟動時的一個自動裝配過程吧

紀莫發表於2020-12-14

前言

繼續總結吧,沒有面試就繼續夯實自己的基礎,前陣子的在面試過程中遇到的各種問題陸陸續續都會總結出來分享給大家,這次要說的也是面試中被問到的一個高頻的問題,我當時其實沒答好,因為很早之前是看到springboot的啟動的一個過程的原始碼的,但是時間隔得有點久了(兩年多沒用過springboot),所以當時也沒答好。這次好好總結這部分知識。

SpringApplication.run()

我看網上好多介紹springboot自動裝配過的文章時,上來就直接說@SpringBootApplication註解是一個複合註解,從這個註解開始介紹springboot是如何將配置項進行載入的。其實我覺得難道不應該是先啟動了spring的容器,然後才能掃到註解,然後才能解析註解嗎?也可能是大家覺得建立容器重新整理容器這些基礎操作都預設知道的,所以就都沒說。
但我在分析springboot自動裝配的時候,要先從SpringApplication.run()方法開始。
在這裡插入圖片描述
我們進入到SpringApplication這個類中看一下run()方法的核心實現,差不多每一行我都加上了註釋了。
在這裡插入圖片描述
SpringApplication.run()方法中,我把關鍵點用序號標識出來了。

  1. 第一個就是建立ApplicationContext容器。
  2. 第二個是重新整理ApplicationContext容器。

在建立ApplicationContext時,會根據使用者是否明確設定了ApplicationContextClass型別以及初始化階段的推斷結果,決定為當前SpringBoot應用建立什麼型別的ApplicationContext。
在這裡插入圖片描述
建立完成ApplicationContext容器後,我們接著回到SpringApplication.run()方法中。
下面開始初始化各種外掛在異常失敗後給出的提示。
然後執行準備重新整理上下文的一些操作。其實prepareContext()方法也是非常關鍵的,它起到了一個承上啟下的作用。下面我們來看一下prepareContext()方法裡面具體執行了什麼。
在這裡插入圖片描述
關鍵的地方我也標註出來了,主要就是getAllSoures()方法,這個方法中,獲取到的一個source就是啟動類DemoApplication。
在這裡插入圖片描述
這樣就通過獲取這個啟動類就可以在後load()方法中取載入這個啟動類到容器中。

然後,後面再通過listeners.contextLoaded(context);
將所有監聽器載入到ApplicationContext容器中。

最後就是我們上面說的核心的第二部重新整理ApplicationContext容器操作,如果沒有這一步操作上面的內容也都白做的,通過SpringApplication的refreshContext(context)方法完成最後一道工序將啟動類上的註解配置,重新整理到當前執行的容器環境中。

啟動類上的註解

上面我們說到在SpringApplication的run()方法中,通過呼叫自己的prepareContext()方法,在prepareContext()方法中又呼叫getAllSources()方法,然後去獲取啟動類,然後通過SpringApplication的load()方法,去載入啟動類,然後在重新整理容器的時候就會去將啟動類在容器中進行例項化。

在重新整理ApplicationContext容器時,就開始解析啟動類上的註解了。

啟動類DemoApplication就只有一個註解@SpringBootApplication,那麼下面來看一下這個註解:
在這裡插入圖片描述
可以看到這個註解是一個複合註解,有三個關鍵註解需要說明一下。

@SpringBootConfiguration

@SpringBootConfiguration這個註解說明再點進去檢視詳情發現就是一個@Configuration註解,這說明啟動類就是一個配置類。支援Spring以JavaConfig的形式啟動。

@ComponentScan

這個註解,從字面的意思上也能看出來,就是元件掃描的意思,即預設掃描當前package以及其子包下面的spring的註解,例如:@Controller@Service@Component等等註解。

@EnableAutoConfiguration

@EnableAutoConfiguration這個註解也是一個複合註解:
在這裡插入圖片描述
這個註解是比較核心的一個註解,springboot的主要自動配置原理基本上都來自@EnableAutoConfiguration這個註解的配置,那麼我們通過看這個註解的原始碼可以發現有兩個註解比較重要的。

  • 一個是@AutoConfigurationPackage,自動配置包。
  • 另一個是@Import(AutoConfigurationImportSelector.class),自動引入元件。

@AutoConfigurationPackage這個註解字面的意思是自動配置包,那麼我們點進去看看裡面是什麼樣的。
在這裡插入圖片描述
還是一個複合註解,但是最終依賴的確實@Import這個註解,這個註解後面我們會介紹,現在先明白它就是給Spring容器引入元件的功能的一個註解。
那麼我們接著來看看AutoConfigurationPackages.Registrar.class這個類裡面的程式碼。
在這裡插入圖片描述在這裡插入圖片描述
這兩張圖就是這個AutoConfigurationPackages.Registrar這個類的關鍵部分,說實話,我是沒看出來什麼東西。但是網上搜到的是這個register()方法的作用是,用來自動註冊一些元件中的配置,例如JPA的@Entity這個註解,這裡就是會開啟自動掃描這類註解的功能。

@Import(AutoConfigurationImportSelector.class)

我們接著回來看@EnableAutoConfiguration下的@Import(AutoConfigurationImportSelector.class)這個註解的功能。進入到AutoConfigurationImportSelector這個類裡面後原始碼如下:
在這裡插入圖片描述
然後我們進入getAutoConfigurationEntry()方法來看看:
在這裡插入圖片描述
我們繼續進入getCandidateConfigurations()方法:
在這裡插入圖片描述
看來最核心的方法是SpringFactroiesLoader.loadFactoryNames()方法了,我們再進入看看:
在這裡插入圖片描述
包的好深,居然還有一層,那麼繼續進入loadSpringFactories()方法。
在這裡插入圖片描述
終於到最後一層了,算是“撥開雲霧見天日,守得雲開見月明”,下面就來梳理一下loadSpringFactories()方法。
首先FACTORIES_RESOURCE_LOCATION這個常量的值是:
"META-INF/spring.factories"

/**
 * The location to look for factories.
 * <p>Can be present in multiple JAR files.
 */
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

所以第一個端核心程式碼的意思是:
啟動的時候會掃描所有jar包下META-INF/spring.factories這個檔案。第二段程式碼的意思是將這些掃描到的檔案轉成Properties物件,後面兩個核心程式碼的意思就是說將載入到的Properties物件放入到快取中。

然後getCandidateConfigurations()方法,是隻獲取了key是EnableAutoConfiguration.class的配置。
在這裡插入圖片描述
我們看到getCandidateConfigurations()方法,通過SpringFactoriesLoader.loadFactoryNames()獲取到了118個配置。
在這裡插入圖片描述
那麼我們來看一個spring.factories檔案中的內容是什麼樣子的呢?
在這裡插入圖片描述
原來是這種形式的,看來這和上一篇文章中講解的Java中的SPI機制載入介面實現很像啊,其實通過查閱資料發現,這就是一種自定義SPI的實現方式的功能。
那麼我們以第一個配置類:
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration來看一下,這些類都是如果實現的。
開啟org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration的原始碼:
在這裡插入圖片描述
我們看到這個類有三個註解@Configuration@AutoConfigureAfter@ConditionalOnProperty、因為有@Configuration註解所以它也是一個配置類,然後第二註解中的引數類JmxAutoConfiguration.class進入之後是這樣的:
在這裡插入圖片描述
也是存在@ConditionalOnProperty註解的。那看來關鍵點就是@ConditionalOnProperty這個註解了。
這個註解其實是一個條件判斷註解,這個條件註解後面的引數的意思是當存在系統屬性字首為spring.application.admin,並且屬性名稱為enabled,並且值為true時,才載入當前這個Bean並進行例項化。

這種spring4.0後面出現的的條件註解,可以極大的增加了框架的靈活性和擴充套件性,可以保證很多元件可以通過後期配置,而且閱讀原始碼的人,通過這些註解就能明白在什麼情況下才會例項化當前Bean。

後面還有不少這種條件註解呢:

@ConditionalOnBean:當容器裡有指定Bean的條件下
@ConditionalOnClass:當類路徑下有指定的類的條件下
@ConditionalOnExpression:基於SpEL表示式為true的時候作為判斷條件才去例項化
@ConditionalOnJava:基於JVM版本作為判斷條件
@ConditionalOnJndi:在JNDI存在的條件下查詢指定的位置
@ConditionalOnMissingBean:當容器裡沒有指定Bean的情況下
@ConditionalOnMissingClass:當容器裡沒有指定類的情況下
@ConditionalOnWebApplication:當前專案時Web專案的條件下
@ConditionalOnNotWebApplication:當前專案不是Web專案的條件下
@ConditionalOnProperty:指定的屬性是否有指定的值
@ConditionalOnResource:類路徑是否有指定的值
@ConditionalOnOnSingleCandidate:當指定Bean在容器中只有一個,或者有多個但是指定首選的Bean

這些註解其實都是通過@Conditional註解擴充套件而來的,只是使用了不同的組合條件來判斷是否需要載入和初始化當前Bean。

總結

好了,最後總結一下,當面試官問springboot的自動裝配原理的時候,不能這麼長篇大論的說吧,畢竟這麼多內容也記不住啊。
所以總結:
springboot啟動時,是依靠啟動類的main方法來進行啟動的,而main方法中執行的是SpringApplication.run()方法,而SpringApplication.run()方法中會建立spring的容器,並且重新整理容器。而在重新整理容器的時候就會去解析啟動類,然後就會去解析啟動類上的@SpringBootApplication註解,而這個註解是個複合註解,這個註解中有一個@EnableAutoConfiguration註解,這個註解就是開啟自動配置,這個註解中又有@Import註解引入了一個AutoConfigurationImportSelector這個類,這個類會進過一些核心方法,然後去掃描我們所有jar包下的META-INF下的spring.factories檔案,而從這個配置檔案中取找key為EnableAutoConfiguration類的全路徑的值下面的所有配置都載入,這些配置裡面都是有條件註解的,然後這些條件註解會根據你當前的專案依賴的jar包以及是否配置了符合這些條件註解的配置來進行裝載的。

這就是springboot自動配置的過程。

其實上面這些內容還是有點多,而且還有好多註解的單詞也不好記,那換成大白話,再精煉一下:

SpringBoot在啟動的時候會呼叫run()方法,run()方法會重新整理容器,重新整理容器的時候,會掃描classpath下面的的包中META-INF/spring.factories檔案,在這個檔案中記錄了好多的自動配置類,在重新整理容器的時候會將這些自動配置類載入到容器中,然後在根據這些配置類中的條件註解,來判斷是否將這些配置類在容器中進行例項化,這些條件主要是判斷專案是否有相關jar包或是否引入了相關的bean。這樣springboot就幫助我們完成了自動裝配。

相關文章