前言
繼續總結吧,沒有面試就繼續夯實自己的基礎,前陣子的在面試過程中遇到的各種問題陸陸續續都會總結出來分享給大家,這次要說的也是面試中被問到的一個高頻的問題,我當時其實沒答好,因爲很早之前是看過springboot啓動過程的源碼,但是時間隔得有點久了(兩年多沒用過springboot),所以當時也沒答好。這次好好總結這部分知識。
SpringApplication.run()
我看網上好多介紹springboot自動裝配過的文章時,上來就直接說@SpringBootApplication
註解是一個複合註解,從這個註解開始介紹springboot是如何將配置項進行加載的。其實我覺得難道不應該是先啓動了spring的容器,然後才能掃到註解,然後才能解析註解嗎?也可能是大家覺得創建容器刷新容器這些基礎操作都默認知道的,所以就都沒說。
但我在分析springboot自動裝配的時候,要先從SpringApplication.run()
方法開始。
我們進入到SpringApplication
這個類中看一下run()
方法的核心實現,差不多每一行我都加上了註釋了。
SpringApplication.run()
方法中,我把關鍵點用序號標識出來了。
- 第一個就是創建ApplicationContext容器。
- 第二個是刷新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就幫助我們完成了自動裝配。