一個人的價值體現在能夠幫助多少人。自己編碼好,價值能得到很好的體現。若你做出來的東西能夠幫助別人開發,大大減少開發的時間,那就功德無量。關注公眾號【BAT的烏托邦】開啟專欄式學習,拒絕淺嘗輒止。本文 https://www.yourbatman.cn 已收錄,裡面一併有Spring技術棧、MyBatis、中介軟體等小而美的專欄供以學習哦。
前言
各位小夥伴大家好,我是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時代。顯然這不是本文需要討論的內容~
正文
本文將要聊的重點是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
時,他們的執行順序是由使用者靠手動指定的,就像這樣:
// 手動控制Config1 Config2的順序
ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class, Config2.class);
當然,你可能就疑問了說:即使在傳統Spirng裡,我也從沒有自己使用過AnnotationConfigApplicationContext
來顯示載入配置啊,都是使用@Configuration
定義好配置類後,點選Run
一把唆的。沒錯,那是因為你是在web環境下使用Spring,IoC容器是藉助web容器(如Tomcat等)來驅動的,Spring對此部分封裝得非常好,所以做到了對使用者幾乎無感知。
關於這部分的內容,此處就不深究了,畢竟本文重點不在這嘛。但可以給出給小結論:@Configuration
配置被載入進容器的方式大體上可分為兩種:
- 手動。構建
ApplicationContext
時由構建者手動傳入,可手動控制順序 - 自動。被
@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的自動配置場景為例:
@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
),那就按照相對順序走
- 當碰到多個配置都是最高優先順序的時候,且互相之前沒有關係的話,順序也是不定的。但若互相之間存在依賴關係(如本利的
在WebMvcAutoConfiguration
載入後,在它之後其實還有很多配置會嘗試執行,例如:
@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環境,載入一些模版引擎的並無必要嘛。
三大註解使用的誤區(重要)
根據我的切身體會,針對這三大註解,實在有太多人把它誤用了,想用但是用了卻又不生效,於是就容易觸發一波“罵街”操作,其實這也是我書寫本文的最大動力所在:糾正你的錯誤使用,告訴你正確姿勢。
錯誤使用示例
我見到的非常多的小夥伴這麼來使用三大註解:我這裡使用“虛擬碼”進行模擬
@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作為一個參考配置,作為日誌參考使用即可)
啟動應用,控制檯列印:
配置類SonConfig構造器被執行...
配置類ParentConfig構造器被執行...
我是被自動掃描的配置,初始化啦....
Son優先於Parent被載入了,這明顯不符合要求。因此,我看到很多小夥伴就這麼幹:
@AutoConfigureBefore(A_SonConfig.class)
@Configuration
public class B_ParentConfig {
B_ParentConfig() {
System.out.println("配置類ParentConfig構造器被執行...");
}
}
通過@AutoConfigureBefore
控制,表示在A_SonConfig
之前執行此配置。語義層面上看,貌似沒有任何問題,再次啟動應用:
配置類SonConfig構造器被執行...
配置類ParentConfig構造器被執行...
我是被自動掃描的配置,初始化啦....
what a fuck。看到沒,我沒騙你吧,罵街了罵街了
竟然沒生效?程式碼不會騙人,@AutoConfigureBefore
的語義也沒有問題,而是你使用的姿勢不對,下面我會給你正確姿勢。
三大註解使用的正確姿勢
針對以上case,要想達到預期效果,正確姿勢只需要下面兩步:
- 把
A_SonConfig
和B_ParentConfig
挪動到Application掃描不到的包內,切記:一定且必須是掃描不到的包內 - 當前工程裡增加配置
META-INF/spring.factories
,內容為(配置裡Son和Parent前後順序對結果無影響):
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.fsx.autoconfig.A_SonConfig,com.fsx.autoconfig.B_ParentConfig
再次啟動應用看看,列印輸出:
我是被自動掃描的配置,初始化啦....
配置類ParentConfig構造器被執行...
配置類SonConfig構造器被執行...
完美。符合預期,Parent終於在Son之前完成了初始化,也就是說我們的@AutoConfigureBefore
註解生效了。
使用細節注意事項
針對此使用姿勢,雖然很正確,並不是完全沒有“副作用”的,有如下細節平時也需要引起注意:
- 若你不用
@AutoConfigureBefore
這個註解,單單就想依賴於spring.factories裡的先後順序的來控制實際的載入順序,答案是不可以,控制不了 - 例子中有個小細節:我每次都故意輸出了
我是被自動掃描的配置,初始化啦....
這句話,可以發現被掃描進去配置例項化是在它前面(見錯誤示例),而通過spring.factories
方式進去是在它的後面(見正確姿勢) - 從這個小細節可以衍生得到結論:
Spring Boot
的自動配置均是通過spring.factories
來指定的,它的優先順序最低(執行時機是最晚的);通過掃描進來的一般都是你自己自定義的配置類,所以優先順序是最高的,肯定在自動配置之前載入- 從這你應該學到:若你要指定掃描的包名,請千萬不要掃描到形如
org.springframework
這種包名,否則“天下大亂”(當然嘍為了防止這種情況出現,Spring Boot做了容錯的。它有一個類專門檢測這個case防止你配置錯了,具體參見ComponentScanPackageCheck
預設實現)
- 從這你應該學到:若你要指定掃描的包名,請千萬不要掃描到形如
- 請儘量不要讓自動配置類既被掃描到了,又放在
spring.factories
配置了,否則後者會覆蓋前者,很容易造成莫名其妙的錯誤
小總結,對於三大註解的正確使用姿勢是應該是:請使用在你的自動配置裡(一般是你自定義starter時使用),而不是使用在你業務工程中的@Configuration
裡,因為那會毫無效果。
三大註解解析時機淺析
為了更好的輔助理解,加強記憶,本文將這三大註解解析時機簡要的絮叨一下,知道了它被解析的時機,自然就很好解釋為何你那麼寫是無效的嘍。
這三個註解的解析都是交給AutoConfigurationSorter
來排序、處理的,做法類似於AnnotationAwareOrderComparator
去解析排序@Order
註解。核心程式碼如下:
class AutoConfigurationSorter {
// 唯一給外部呼叫的方法:返回排序好的Names,因此返回的是個List嘛(ArrayList)
List<String> getInPriorityOrder(Collection<String> 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後面執行而以為生效了),希望本文能幫助到你。