SpringBoot自動配置原理及手動實現自動配置

前言

之前我們學習了SpringBoot中的配置文件及外部化配置,瞭解了SpringBoot對於配置文件的功能支持與增強,本篇我們將要來學習SpringBoot的自動配置原理及手動實現自動配置。

數據庫依賴引起的bug

我們很多人在第一次使用SpringBoot的時候,往往對其原理認知不足,或者簡單的瞭解以後就開始入門使用,往往最常見的就是使用SpringBoot添加一個持久化框架的依賴,用來嘗試操作數據庫,比如:

<code>


1. `<dependency>`

2. `<groupid>org.springframework.boot/<groupid>`

3. `<artifactid>spring-boot-starter-jdbc/<artifactid>`

4. `/<dependency>`


/<code>

接著我們啟動SpringBoot,異常出現了,SpringBoot居然啟動不了?


SpringBoot自動配置原理及手動實現自動配置

看報錯信息,似乎是我沒有配置url導致數據源注入失敗,但是我僅僅是引入了一個依賴,什麼操作也沒有啊?看來SpringBoot幫我們引入的db框架嘗試進行自動注入了,我們在Spring中如何配置一個數據源的呢?

<code>


1. `<beanid>
2. `destroy-method="close">`

3. `<propertyname>`

4. `<propertyname>`

5. `<propertyname>`

6. `<propertyname>`

7. ``

/<beanid>

/<code>

那麼豈不是代表著,我們僅僅引入一個 spring-boot-starter-jdbc 的依賴,SpringBoot幫我們自動配置了一個dataSource的bean?百度了一下,我們知道,可以在springboot啟動類中排除datasource自動配置類來解決此問題,如下:

<code>


1. `@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)`


/<code>

這樣就可以使得SpringBoot啟動的時候不去自動配置數據源,但是SpringBoot是如何實現的自動配置呢?

SpringBoot中配置Bean的幾種方式

在思考這個問題之前,我們先來看看,在SpringBoot中我們如果要配置一個Bean,有哪幾種方式:

@Service/@Component

在SpringBoot中不建議使用xml配置創建實例Bean,而在Spring中我們知道,如果不使用xml配置,我們往往可以在一個類上使用 @Service或者 @Component註解,Spring啟動的時候,Spring容器會自動掃描帶有此註解的類,並且註冊該類的實例到Spring容器中,在SpringBoot也完全支持此種方式:

@Configuration /@Bean

在SpringBoot中使用了註解驅動的方式,可以給類添加 @Configuration註解標記此類為配置類,也可以給類添加 @Bean註解,代表創建當前實例bean到ioc容器中,需要注意的是使用 @Configuration註解需要配置在SpringBoot啟動類所在的包下或者其子包下,否則將無法被掃描裝載進入容器中,使用此方式創建bean如下:

<code>


1. `@Configuration`

2. `public class BeanConfig{`


3. `@Bean`

4. `public ClockService clockService() {`

5. `return new ClockService();`

6. `}`

7. `}`


/<code>

@Import註解

相信在使用SpringBoot過程中經常會發現 @EnableScheduling 、 @EnableCaching 等註解,如果我們點進此類註解查看源碼,發現使用的都是@Import註解來實現某個功能開啟的,使用此註解也可以在SpringBoot注入的時候導入一個Bean,例如:

<code>


1. `@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)`

2. `@Import(ClockService.class)`

3. `public class AutoConfDemoApplication {`

5. `public static void main(String[] args) {`

6. `SpringApplication.run(AutoConfDemoApplication.class, args);`

7. `}`

8. `}`


/<code>

xml配置/@Conditional

除此之外,SpringBoot依賴保留了Spring的xml配置方式,並且SpringBoot使用了註解驅動開發的方式,同時也可以使用@Conditional開頭的條件過濾註解來配置不同的bean。

自動配置源碼分析

瞭解了SpringBoot中的bean配置以後,我們來看看springboot最核心的註解 @SpringBootApplication ,我們知道每個SpringBoot的啟動類上都會標註這個註解,代表此類為主要運行類,似乎加上了這個註解以後,我們的一些bean就能自動的配置注入進ioc容器中了,到底是如何實現的呢?我們來點開此註解,看看內部的實現:

SpringBoot自動配置原理及手動實現自動配置


可以看到 @Import註解給我們導入了一個 AutoConfigurationImportSelector類,這個類從名字上看起來就和自動配置選擇有關係,我們繼續點進去看看,發現了一個很重要的方法--selectImports ,如下:

SpringBoot自動配置原理及手動實現自動配置


可以看到其中有一個 getCandidateConfigurations的方法,看名稱我們可以猜到此方法負責獲取所有的自動配置的信息,而此方法的代碼如圖:

SpringBoot自動配置原理及手動實現自動配置

可以看到SpringBoot調用了 SpringFactoriesLoader.loadFactoryNames方法獲取其中的所有的名稱列表,而此方法中我們可以看到一個查找路徑的常量:

<code>


1. `publicstaticfinalString FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";`


/<code>

SpringBoot會查找當前工程包括所有依賴的jar的 META-INF路徑下的

spring.factories文件,似乎所有的自動配置的類都是從這裡讀取來的,而加載的過程代碼如下:

<code>


1. `private static Map<string>> loadSpringFactories(@Nullable ClassLoader classLoader) { // 若緩存裡有直接返回緩存的值`

2. `MultiValueMap<string> result = cache.get(classLoader);`

3. `if (result != null) {`

4. `return result;`

5. `}`

7. `try {`

8. `// 類加載器對象存在則用這個加載器獲取上面說的常量路徑裡的資源,不存在則用系統類加載器去獲取`

9. `Enumeration urls = (classLoader != null ?`

10. `classLoader.getResources(FACTORIES_RESOURCE_LOCATION) ://當前classloader是appclassloader,getResources能獲取所有依賴jar裡面的META-INF/spring.factories的完整路徑`

11. `ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));`

12. `result = new LinkedMultiValueMap<>();`

13. `while (urls.hasMoreElements()) { // 遍歷上述返回的url集合`

14. `URL url = urls.nextElement(); // URL類可以獲取來自流,web,甚至jar包裡面的資源`

15. `UrlResource resource = new UrlResource(url);`

16. `Properties properties = PropertiesLoaderUtils.loadProperties(resource);`

17. `for (Map.Entry, ?> entry : properties.entrySet()) { // 解析spring.factories文件`


18. `List<string> factoryClassNames = Arrays.asList(`

19. `StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));`

20. `// spring.facories中配置的不僅僅有自動配置相關的內容,還有其他比如ApplicationContextInitializer等等各種springboot啟動的時候,初始化spring環境需要的配置,自動配置只是其中一項。這個cache也是在springboot啟動階段賦值的`

21. `result.addAll((String) entry.getKey(), factoryClassNames);`

22. `}`

23. `}`

24. `cache.put(classLoader, result);`

25. `return result;`

26. `}`

27. `catch (IOException ex) {`

28. `throw new IllegalArgumentException("Unable to load factories from location [" +`

29. `FACTORIES_RESOURCE_LOCATION + "]", ex);`

30. `}`

31. `}`

/<string>
/<string>/<string>

/<code>

看到這裡我們進入當前項目的maven本地倉庫,找到我們之前的 spring-boot-starter-jdbcjar,解壓以後的確有一個META-INF目錄,但是目錄裡並沒有spring.factories文件,只有一個spring.provides 文件,當我們打開此文件後以後,發現裡面並沒有我們想象的自動配置相關的類信息,如圖:

SpringBoot自動配置原理及手動實現自動配置

這裡的provides提供者是什麼意思?難道是後面的三個名稱是jar的名稱?spring-boot-starter-jdbc.jar會幫我們自動的下載依賴這三個jar,所以我們想要的spring.factories文件就在這幾個jar裡面?

當我們查看倉庫的時候,的確發現了這幾個jar,但是很遺憾,這幾個jar僅僅就是基礎jar,並不包含這些文件,那麼,spring.factories

文件在哪?

還記得我們給SpringBoot啟動類上標記了exclude = DataSourceAutoConfiguration.class ,就可以排除了jdbc數據源自動配置帶來的問題了,那我們去看看這裡有什麼線索吧,點開 DataSourceAutoConfiguration類,我們可以看到有一個 @Conditional族的組合條件註解,限制為當DataSource.class,,EmbeddedDatabaseType.class被classloader加載,則這個配置類生效 ,那麼當我們加入 spring-boot-starter-jdbc以後,會幫我們自動依賴相關的三個jar,這個時候,這些class也會被加載進去,此條件就滿足了,自然就觸發了自動配置,那麼,這個自動配置自然由SpringBoot提供的了,我們查看SpringBoot的star,終於找到了META-INF/spring.factories文件,打開內容如下:

SpringBoot自動配置原理及手動實現自動配置

至此我們的猜想已經得到驗證,SpringBoot的自動配置是依靠讀取META-INF/spring.factories文件的內容進行配置,並且根據@Conditional族的條件註解,進行限制是否自動配置,從而實現動態的配置與排除bean的功能。

動手實現自己的自動配置

瞭解了自動配置的原理,我們不禁有了想自己實現一個自動配置的想法,首先我們來整理一下自動配置所需要的步驟:

1.編寫Java Config類,使用@Configuration註解來進行bean自動配置的條件選擇

2.編寫META-INF/spring.factories文件,將我們需要自動配置的類編寫進去,使得SpringBoot在讀取的時候能將此類加載進去

首先我們創建一個自動配置的Maven工程,名稱為-- gank-spring-boot-autoconfigure,在此工程的pom中我們只要依賴Springboot的自動配置相關jar以及我們需要被自動配置進去的類所在的jar,pom依賴如下:

<code> 


1. `<dependencies>`

2. `<dependency>`

3. `<groupid>org.springframework.boot/<groupid>`

4. `<artifactid>spring-boot-autoconfigure/<artifactid>`

5. `/<dependency>`

6. ``

7. `<dependency>`

8. `<groupid>gank.spring.hello/<groupid>`

9. `<artifactid>gank/<artifactid>`

10. `<version>0.0.1-SNAPSHOT/<version>`

11. `<scope>provided/<scope>`

12. `/<dependency>`

13. `/<dependencies>`


/<code>

這裡我們實現了一個簡單的被自動配置的jar-- gank,只有一個 GankApplicationRunner類,代碼如下:

<code>


1. `@Slf4j`

2. `public class GankApplicationRunner implements ApplicationRunner {`

3. `public GankApplicationRunner() {`

4. `log.info("初始化 GankApplicationRunner.");`


5. `}`

7. `public void run(ApplicationArguments args) throws Exception {`

8. `log.info("Hello Spring! ");`

9. `}`

10. `}`


/<code>

依賴和被配置的jar都做好了,我們可以開始編寫java Config類了,在

gank-spring-boot-autoconfigure中,我們創建了 GankAutoConfiguration配置類,代碼如下:

<code>


1. `@Configuration`

2. `@ConditionalOnClass(GankApplicationRunner.class)`

3. `public class GankAutoConfiguration {`

4. `@Bean`

5. `@ConditionalOnMissingBean(GankApplicationRunner.class)`

6. `//配置文件中gank.enabled的值為true才創建,不存在默認為true`

7. `@ConditionalOnProperty(name = "gank.enabled", havingValue = "true", matchIfMissing = true)`

8. `public GankApplicationRunner gankApplicationRunner() {`

9. `return new GankApplicationRunner();`

10. `}`

11. `}`


/<code>

這裡我們需要滿足條件,即不存在GankApplicationRunner類實例,並且配置中的gank.enabled參數為true,我們才會進行GankApplicationRunner的bean創建,自動配置類編寫完畢後,我們在 resources目錄下,創建 META-INF資源包,並且在該目錄下創建 spring.factories文件,將我們剛才編寫的GankAutoConfiguration配置類編寫上去,如下:

<code>


1. `org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\`

2. `gank.spring.hello.GankAutoConfiguration`


/<code>

至此,我們的自動配置包編寫完成,接下來我們創建一個Spingboot-demo工程,pom依賴中我們將剛才的創建的兩個工程進行依賴,pom配置如下:

<code>


1. `<dependency>`

2. `<groupid>gank.spring.hello/<groupid>`

3. `<artifactid>gank-spring-boot-autoconfigure/<artifactid>`

4. `<version>0.0.1-SNAPSHOT/<version>`


5. `/<dependency>`

7. `<dependency>`

8. `<groupid>gank.spring.hello/<groupid>`

9. `<artifactid>gank/<artifactid>`

10. `<version>0.0.1-SNAPSHOT/<version>`

11. `/<dependency>`


/<code>

在這裡,我們給application.properties文件中配置一下:

<code>


1. `#這裡不配置默認也是true`

2. `gank.enabled=true`


/<code>

接著,我們啟動當前的demo工程,當SpringBoot啟動完畢,我們查看控制檯,就會發現我們編寫的log已經輸出,可見自動動手編寫的自動配置已經實現!

注:這裡需要注意一下,demo工程的啟動類所在的包名一定要和 gank-spring-boot-autoconfigure工程的一樣,或者是其父包名,否則配置類無法生效。

最後,歡迎大家在下方評論區留下你的想法,如果覺得本文不錯,那就給小on點個贊吧!


分享到:


相關文章: