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

YourBatman發表於2020-07-08

一個人的價值體現在能夠幫助多少人。自己編碼好,價值能得到很好的體現。若你做出來的東西能夠幫助別人開發,大大減少開發的時間,那就功德無量。關注公眾號【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配置被載入進容器的方式大體上可分為兩種:

  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的自動配置場景為例:

@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)最高優先順序,也就是說它無其它依賴,希望自己是最先被初始化的
    • 當碰到多個配置都是最高優先順序的時候,且互相之前沒有關係的話,順序也是不定的。但若互相之間存在依賴關係(如本利的DispatcherServletAutoConfigurationServletWebServerFactoryAutoConfiguration),那就按照相對順序走

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,要想達到預期效果,正確姿勢只需要下面兩步:

  1. A_SonConfigB_ParentConfig挪動到Application掃描不到的包內,切記:一定且必須是掃描不到的包內
  2. 當前工程裡增加配置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後面執行而以為生效了),希望本文能幫助到你。

相關文章