1. 程式人生 > >使用@AutoConfigureBefore調整配置順序竟沒生效?

使用@AutoConfigureBefore調整配置順序竟沒生效?

> 一個人的價值體現在能夠幫助多少人。自己編碼好,價值能得到很好的體現。若你做出來的東西能夠幫助別人開發,大大減少開發的時間,那就功德無量。 > **作者**:A哥(YourBatman) > **公眾號**:BAT的烏托邦(ID:BAT-utopia) > **文末是否有彩蛋**:有 [TOC] ![](https://img-blog.csdnimg.cn/20200707062022385.png#x200) # 前言 各位小夥伴大家好,我是A哥。`Spring Boot`是Spring家族具有劃時代意義的一款產品,它發展自`Spring Framework`卻又高於它,這種高於主要表現在其最重要的**三大特性**,而相較於這三大特性中更為重要的便是**Spring Boot的自動配置**(`AutoConfiguration`)。與其說是自動,倒不如說是“智慧”,該框架看起來好像“更聰明”了。因此它也順理成章的成為了構建微服務的基礎設施,穩坐第一寶座。 生活之道,在於取捨。程式設計何嘗不是,任何決定都會是一把雙刃劍,`Spring Boot`的自動配置解決了Spring Framework使用起來的眾多痛點,讓開發效率可以得到**指數級提升**(想一想,這不就是功德無量嗎?)。成也蕭何敗也蕭何,也正是因為它的太智慧,倘若出了問題就會讓程式設計師兩眼一抹黑,無從下手。 > 瑕不掩瑜,Spring Boot前進的步伐浩浩蕩蕩,學就完了 這不,我就在前幾天收到一個“求助”,希望使用`@AutoConfigureBefore`來**控制配置的順序**,但並未能如願。本文就針對這個場景case稍作展開,討論下使用`@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder`三大註解控制自動配置執行順序的**正確姿勢**。 > 提示:Spring Boot的自動配置是通過@EnableAutoConfiguration註解驅動的,預設是開啟狀態。你也可以通過`spring.boot.enableautoconfiguration = false`來關閉它,回退到Spring Framework時代。顯然這不是本文需要討論的內容~ ![](https://img-blog.csdnimg.cn/2020050916181352.png#x200) --- # 正文 本文將要聊的重點是Spring Boot自動配置 + 順序控制,自動配置大家都耳熟能詳,那麼“首當其衝”就是知曉這個問題:配置類的執行為何需要控制順序? --- ## 配置類為何需要順序? 我們已經知道Spring容器它對Bean的初始化是**無序**的,我們並不能想當然的通過`@Order`註解來控制其執行順序。一般來說,對於容器內普通的Bean我們只需要關注依賴關係即可,而並不需要關心其絕對的順序,而依賴關係的管理Spring的是做得很好的,這不連**迴圈依賴**它都可以搞定麼。 `@Configuration`配置類它也是一個Bean,但對於配置類來說,某些場景下的執行順序是**必須的**,是需要得到保證的。比如很典型的一個非A即B的case:若容器內已經存在A了,就不要再把B放進來。這種case即使用中文理解,就能知道對A的“判斷”必須要放在B的前面,否則可能導致程式出問題。 那麼針對於配置的執行順序,傳統Spring和Spring Boot下各自是如何處理的,表現如何呢? --- ### Spring下控制配置執行順序 在傳統的`Spring Framework`裡,一個`@Configuration`註解標註的類就代表一個配置類,當存在多個`@Configuration`時,他們的**執行順序**是由使用者靠手動指定的,就像這樣: ```java // 手動控制Config1 Config2的順序 ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class, Config2.class); ``` 當然,你可能就疑問了說:即使在傳統Spirng裡,我也從沒有自己使用過`AnnotationConfigApplicationContext`來顯示載入配置啊,都是使用`@Configuration`定義好配置類後,點選`Run`一把唆的。沒錯,那是因為你是在web環境下使用Spring,IoC容器是藉助web容器(如Tomcat等)來驅動的,Spring對此部分封裝得非常好,所以做到了對使用者幾乎無感知。 關於這部分的內容,此處就不深究了,畢竟本文重點不在這嘛。但可以給出給小結論:`@Configuration`配置被載入進容器的方式大體上可分為兩種: 1. 手動。構建`ApplicationContext `時由構建者手動傳入,可手動控制順序 2. 自動。被`@ComponentScan`自動掃描進去,無法控制順序 絕大多數情況下我們都是使用**自動**的方式,所以在Spring下對配置的順序並無感知。其實這也是需求驅使,因為在傳統Spring下我們並無此需求,所以對它無感是合乎邏輯的。另說一句,雖然我們並不能控制Bean的順序,但是我們是可以干涉它的,比如:控制依賴關係、提升優先順序、“間接”控制執行順序...當然嘍這是後面文章的內容,敬請關注。 --- ### Spring Boot下控制配置執行順序 `Spring Boot`下對**自動配置**的管理對比於Spring它就是黑盒,它會根據當前容器內的情況來**動態的**判斷自動配置類的載入與否、以及載入的順序,所以可以說:Spring Boot的自動配置它對順序是有**強要求**的。需求驅使,Spring Boot給我們提供了`@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder`(下面統稱這三個註解為“三大註解”)這三個註解來幫我們解決這種訴求。 需要注意的是:三大註解是Spring Boot提供的而非Spring Framework。其中前兩個是1.0.0就有了,`@AutoConfigureOrder`屬於1.3.0版本新增,表示絕對順序(數字越小,優先順序越高)。另外,這幾個註解**並不互斥**,可以同時標註在同一個`@Configuration`自動配置類上。 --- ## Spring Boot內建的控制配置順序舉例 為方便大家理解,我列出一個Spring Boot它自己的使用作為示例學一學。以大家最為熟悉的WebMvc的自動配置場景為例: ```java @Configuration(proxyBeanMethods = false) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { ... } @Configuration(proxyBeanMethods = false) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { ... } @Configuration(proxyBeanMethods = false) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) public class ServletWebServerFactoryAutoConfiguration { ... } ``` 這幾個配置是WebMVC的核心配置,他們之間是有順序關係的: - `WebMvcAutoConfiguration`被載入的前提是:`DispatcherServletAutoConfiguration、TaskExecutionAutoConfiguration、ValidationAutoConfiguration`這三個哥們都已經完成初始化 - `DispatcherServletAutoConfiguration`被載入的前提是:`ServletWebServerFactoryAutoConfiguration`已經完成初始化 - `ServletWebServerFactoryAutoConfiguration`被載入的前提是:`@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)`最高優先順序,也就是說它無其它依賴,**希望自己是最先被初始化的** - 當碰到多個配置都是最高優先順序的時候,且互相之前沒有關係的話,順序也是不定的。但若互相之間存在依賴關係(如本利的`DispatcherServletAutoConfiguration`和`ServletWebServerFactoryAutoConfiguration`),那就按照相對順序走 ![](https://img-blog.csdnimg.cn/20200418160820903.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Y2NDEzODU3MTI=,size_16,color_FFFFFF,t_70#x330) 在`WebMvcAutoConfiguration`載入**後**,在它**之後**其實還有很多配置會嘗試執行,例如: ```java @AutoConfigureAfter(WebMvcAutoConfiguration.class) class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration { ... } @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class GroovyTemplateAutoConfiguration { ... } @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { ... } @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class LifecycleMvcEndpointAutoConfiguration { ... } ``` 這些都很容易理解:如果都不是Web環境,載入一些模版引擎的並無必要嘛。 ---- ## 三大註解使用的誤區(重要) 根據我的切身體會,針對這三大註解,實在有太多人把它**誤用**了,想用但是用了卻又不生效,於是就容易觸發一波“罵街”操作,其實這也是我書寫本文的最大動力所在:糾正你的錯誤使用,告訴你正確姿勢。 --- ### 錯誤使用示例 我見到的非常多的小夥伴這麼來使用三大註解:我這裡使用“虛擬碼”進行模擬 ```java @Configuration public class B_ParentConfig { B_ParentConfig() { System.out.println("配置類ParentConfig構造器被執行..."); } } @Configuration public class A_SonConfig { A_SonConfig() { System.out.println("配置類SonConfig構造器被執行..."); } } @Configuration public class C_DemoConfig { public C_DemoConfig(){ System.out.println("我是被自動掃描的配置,初始化啦...."); } } @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args).close(); } } ``` 通過名稱能知道我想要的達到的效果是:**ParentConfig先載入,SonConfig後加載**。(DemoConfig作為一個參考配置,作為日誌參考使用即可) 啟動應用,控制檯列印: ```java 配置類SonConfig構造器被執行... 配置類ParentConfig構造器被執行... 我是被自動掃描的配置,初始化啦.... ``` Son優先於Parent被載入了,這明顯**不符合要求**。因此,我看到很多小夥伴就這麼幹: ```java @AutoConfigureBefore(A_SonConfig.class) @Configuration public class B_ParentConfig { B_ParentConfig() { System.out.println("配置類ParentConfig構造器被執行..."); } } ``` 通過`@AutoConfigureBefore`控制,表示在`A_SonConfig`之前執行此配置。語義層面上看,貌似沒有任何問題,再次啟動應用: ```java 配置類SonConfig構造器被執行... 配置類ParentConfig構造器被執行... 我是被自動掃描的配置,初始化啦.... ``` what a fuck。看到沒,我沒騙你吧,罵街了罵街了 ![](https://img-blog.csdnimg.cn/20200509163634888.png#x200) 竟然沒生效?程式碼不會騙人,`@AutoConfigureBefore`的語義也沒有問題,而是你使用的姿勢不對,下面我會給你正確姿勢。 ---- ## 三大註解使用的正確姿勢 針對以上case,要想達到預期效果,正確姿勢只需要下面兩步: 1. 把`A_SonConfig`和`B_ParentConfig`挪動到Application**掃描不到**的包內,切記:一定且必須是掃描不到的包內 2. 當前工程裡增加配置`META-INF/spring.factories`,內容為(配置裡Son和Parent前後順序對結果無影響): ```java org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.fsx.autoconfig.A_SonConfig,com.fsx.autoconfig.B_ParentConfig ``` 再次啟動應用看看,列印輸出: ```java 我是被自動掃描的配置,初始化啦.... 配置類ParentConfig構造器被執行... 配置類SonConfig構造器被執行... ``` **完美**。符合預期,Parent終於在Son之前完成了初始化,也就是說我們的`@AutoConfigureBefore`註解生效了。 --- ### 使用細節注意事項 針對此使用姿勢,雖然很正確,並不是完全沒有“副作用”的,有如下細節平時也需要引起注意: - 若你不用`@AutoConfigureBefore`這個註解,單單就想依賴於spring.factories裡的先後順序的來控制實際的載入順序,答案是**不可以**,控制不了 - 例子中有個小細節:我每次都故意輸出了`我是被自動掃描的配置,初始化啦....`這句話,可以發現被掃描進去配置例項化是在它前面(見錯誤示例),而通過`spring.factories`方式進去是在它的後面(見正確姿勢) - 從這個小細節可以衍生得到結論:`Spring Boot`的自動配置均是通過`spring.factories`來指定的,它的優先順序最低(執行時機是最晚的);通過掃描進來的一般都是你自己自定義的配置類,所以優先順序是最高的,**肯定在自動配置之前載入** - 從這你應該學到:若你要指定掃描的包名,請千萬不要掃描到形如`org.springframework`這種包名,否則“天下大亂”(當然嘍為了防止這種情況出現,Spring Boot做了容錯的。它有一個類專門檢測這個case防止你配置錯了,具體參見`ComponentScanPackageCheck`預設實現) - 請儘量不要讓自動配置類既被掃描到了,又放在`spring.factories`配置了,否則後者會覆蓋前者,很容易造成莫名其妙的錯誤 小總結,對於三大註解的正確使用姿勢是應該是:請使用在你的**自動配置**裡(一般是你自定義starter時使用),而不是使用在你業務工程中的`@Configuration`裡,因為那會毫無效果。 ![](https://img-blog.csdnimg.cn/20200509165554902.jpg#x200) ## 三大註解解析時機淺析 為了更好的輔助理解,加強記憶,本文將這三大註解解析時機**簡要的**絮叨一下,知道了它被解析的時機,自然就很好解釋為何你那麼寫是無效的嘍。 這三個註解的解析都是交給`AutoConfigurationSorter`來排序、處理的,做法類似於`AnnotationAwareOrderComparator`去解析排序`@Order`註解。核心程式碼如下: ```java class AutoConfigurationSorter { // 唯一給外部呼叫的方法:返回排序好的Names,因此返回的是個List嘛(ArrayList) List getInPriorityOrder(Collection classNames) { ... // 先按照自然順序排一波 Collections.sort(orderedClassNames); // 在按照@AutoConfigureBefore這三個註解排一波 orderedClassNames = sortByAnnotation(classes, orderedClassNames); return orderedClassNames; } ... } ``` 此排序器被兩個地方使用到: - `AutoConfigurationImportSelector`:Spring自動配置處理器,用於載入所有的自動配置類。它實現了`DeferredImportSelector`介面:這也順便解釋了為何自動配置是最後執行的原因~ - `AutoConfigurations`:表示自動配置@Configuration類。 這個排序的“解析/排序”過程還是比較複雜的,本文點到為止,觀其大意即可。你可以簡單粗暴的記住結論:`@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder`這三個註解只能作用於**自動配置類**,而不能是自定義的@Configuration配置類。 --- # 總結 關於Spring Boot自動配置順序相關的三大註解`@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder`就先介紹到這了,本文主要用意是為了幫助大家規範此些“常用註解”的使用,規避一些誤區,端正使用姿勢,避免犯錯時又丈二和尚。 我看到不少文章、生產上的程式碼都使用錯了(估計有沒有效果自己的都不知道,又或者剛好歪打正著確實是在xxx後面執行而以為生效了),希望本文能幫助