static關鍵字真能提高Bean的優先順序嗎?答:真能
- 2020 年 7 月 15 日
- 筆記
- Bean, spring, springboot, static關鍵字
生命太短暫,不要去做一些根本沒有人想要的東西。本文已被 //www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
前言
各位小夥伴大家好,我是A哥。關於Spring初始化Bean的順序問題,是個老生常談的話題了,結論可總結為一句話:全局無序,局部有序。Spring Bean
整體上是無序的,而現實是大多數情況下我們真的無需關心,無序就無序唄,無所謂嘍。但是(此處應該有但是哈),我有理由相信,對於有一定從業經驗的Javaer來說,或多或少都經歷過Bean初始化順序帶來的「困擾」,也許是因為沒有對你的功能造成影響,也許可能是你全然「不知情」,所以最終就不了了之~
隱患終歸隱患,依照墨菲定律來講,擔心的事它總歸是會發生的。A哥經常「教唆」程式設計師要面向工資編程,雖然這價值觀有點扭曲,但不可否認很多小夥伴真是這麼想的(命中你了沒有😄),稍加粉飾了而已。話粗理不粗哦,almost所有的Javaer都在用Spring,你憑什麼工資比你身邊同事的高呢?
Spring對Bean的(生命周期)管理是它最為核心的能力,同時也是很複雜、很難掌握的一個知識點。現在就可以啟動你的工程,有木有這句日誌:
"Bean 'xxx' of type [xxxx] is not eligible for getting processed by all BeanPostProcessors"
+ "(for example: not eligible for auto-proxying)"
這是一個典型的Spring Bean過早初始化問題,搜搜看你日誌里是否有此句嘍。這句日誌是由Spring的BeanPostProcessorChecker
這個類負責輸出,含義為:你的Bean xxx不能被所有的BeanPostProcessors
處理到(有的生命周期觸達不到),提醒你注意。此句日誌在低些的版本里是warn警告級別,在本文約定的版本里官方把它改為了info級別。
絕大多數情況下,此句日誌的輸出不會對你的功能造成影響,因此無需搭理。這也是Spring官方為何把它從warn調低為info級別的原因
我在CSDN上寫過一篇「Spring Bean過早初始化導致的誤傷」的文章,訪問量達近4w:
從這個數據(訪問量)上來看,這件事「並不簡單」,遇到此麻煩的小夥伴不在少數且確實難倒了一眾人。關於Spring Bean的順序,全局是不可控的,但是局部上它提供了多種方式來方便使用者提高/降低優先順序(比如前面的使用@AutoConfigureBefore調整配置順序竟沒生效?這篇文章),本文就聊聊static關鍵字對於提供Bean的優先順序的功效。
版本約定
本文內容若沒做特殊說明,均基於以下版本:
- JDK:
1.8
- Spring Framework:
5.2.2.RELEASE
正文
本文採用從 問題提出-結果分析-解決方案-原理剖析 這4個步驟,層層遞進的去感受static關鍵字在Spring Bean上的魅力~
警告一:來自BeanPostProcessorChecker
這是最為常見的一種警告,特別當你的工程使用了shiro
做鑒權框架的時候。在我記憶中這一年來有N多位小夥伴問過我此問題,可見一斑。
@Configuration
class AppConfig {
AppConfig() {
System.out.println("AppConfig init...");
}
@Bean
BeanPostProcessor postProcessor() {
return new MyBeanPostProcessor();
}
}
class MyBeanPostProcessor implements BeanPostProcessor {
MyBeanPostProcessor() {
System.out.println("MyBeanPostProcessor init...");
}
}
運行程式,輸出結果:
AppConfig init...
2020-05-31 07:40:50.979 INFO 15740 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'appConfig'
of type [com.yourbatman.config.AppConfig$$EnhancerBySpringCGLIB$$29b523c8] is not eligible for getting
processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
MyBeanPostProcessor init...
...
結果分析(問題點/衝突點):
AppConfig
優先於MyBeanPostProcessor
進行實例化- 常識是:
MyBeanPostProcessor
作為一個後置處理器理應是先被初始化的,而AppConfig
僅僅是個普通Bean而已,初始化理應靠後
- 常識是:
- 出現了
BeanPostProcessorChecker
日誌:表示AppConfig
這個Bena不能被所有的BeanPostProcessors處理,所以有可能會讓它「錯過」容器對Bean的某些生命周期管理,因此可能損失某些能力(比如不能被自動代理),存在隱患- 但凡只要你工程里出現了
BeanPostProcessorChecker
輸出日誌,理應都得引起你的注意,因為這屬於Spring的警告日誌(雖然新版本已下調為了info級別)
- 但凡只要你工程里出現了
說明:這是一個Info日誌,並非warn/error級別。絕大多數情況下你確實無需關注,但是如果你是一個容器開發者,建議請務必解決此問題(畢竟貌似大多數中間件開發者都有一定程式碼潔癖😄)
解決方案:static關鍵字提升優先順序
基於上例,我們僅需做如下小改動:
AppConfig:
//@Bean
//BeanPostProcessor postProcessor() {
// return new MyBeanPostProcessor();
//}
// 方法前面加上static關鍵字
@Bean
static BeanPostProcessor postProcessor() {
return new MyBeanPostProcessor();
}
運行程式,結果輸出:
MyBeanPostProcessor init...
...
AppConfig init...
...
那個煩人的BeanPostProcessorChecker
日誌就不見了,清爽了很多。同時亦可發現AppConfig
是在MyBeanPostProcessor
之後實例化的,這才符合我們所想的「正常」邏輯嘛。
警告二:Configuration配置類增強失敗
這個「警告」就比上一個嚴重得多了,它有極大的可能導致你程式錯誤,並且你還很難定位問題所在。
@Configuration
class AppConfig {
AppConfig() {
System.out.println("AppConfig init...");
}
@Bean
BeanDefinitionRegistryPostProcessor postProcessor() {
return new MyBeanDefinitionRegistryPostProcessor();
}
///////////////////////////////
@Bean
Son son(){
return new Son();
}
@Bean
Parent parent(){
return new Parent(son());
}
}
class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
MyBeanDefinitionRegistryPostProcessor() {
System.out.println("MyBeanDefinitionRegistryPostProcessor init...");
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
運行程式,結果輸出:
AppConfig init...
MyBeanDefinitionRegistryPostProcessor init...
2020-05-31 07:59:06.363 INFO 37512 --- [ main] o.s.c.a.ConfigurationClassPostProcessor : Cannot enhance
@Configuration bean definition 'appConfig' since its singleton instance has been created too early. The typical
cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring
such methods as 'static'.
...
son init...hashCode() = 1300528434
son init...hashCode() = 1598434875
Parent init...
結果分析(問題點/衝突點):
- AppConfig竟然比MyBeanDefinitionRegistryPostProcessor的初始化時機還早,這本就不合理
- 從
ConfigurationClassPostProcessor
的日誌中可看到:AppConfig配置類enhance增強失敗 - Son對象竟然被創建了兩個不同的實例,這將會直接導致功能性錯誤
這三步結果環環相扣,因為1導致了2的增強失敗,因為2的增強失敗導致了3的創建多個實例,真可謂一步錯,步步錯。需要注意的是:這裡ConfigurationClassPostProcessor輸出的依舊是info日誌(我個人認為,Spring把這個輸出調整為warn級別是更為合理的,因為它影響較大)。
說明:對這個結果的理解基於對Spring配置類的理解,因此強烈建議你進我公眾號參閱那個可能是寫的最全、最好的Spring配置類專欄學習(文章不多,6篇足矣)
源碼處解釋:
ConfigurationClassPostProcessor:
// 對Full模式的配置類嘗試使用CGLIB位元組碼提升
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
...
// 對Full模式的配置類有個判斷/校驗
if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
if (!(beanDef instanceof AbstractBeanDefinition)) {
throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
}
// 若判斷發現此時該配置類已經是個單例Bean了(說明已初始化完成)
// 那就不再做處理,並且輸出警告日誌告知使用者(雖然是info日誌)
else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
logger.info("Cannot enhance @Configuration bean definition '" + beanName +
"' since its singleton instance has been created too early. The typical cause " +
"is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
"return type: Consider declaring such methods as 'static'.");
}
configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
}
...
}
由於配置類增強是在BeanFactoryPostProcessor#postProcessBeanFactory()
聲明周期階段去做的,而BeanDefinitionRegistryPostProcessor
它會優先於該步驟完成實例化(其實主要是優先順序比BeanFactoryPostProcessor
高),從而間接帶動 AppConfig提前初始化導致了問題,這便是根本原因所在。
提問點:本處使用了個自定義的BeanDefinitionRegistryPostProcessor
模擬了效果,那如果你是使用的BeanFactoryPostProcessor
能出來這個效果嗎???答案是不能的,具體原因留給讀者思考,可參考:PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors
這段流程輔助理解。
解決方案:static關鍵字提升優先順序
來吧,繼續使用static關鍵字改造一下:
AppConfig:
//@Bean
//BeanDefinitionRegistryPostProcessor postProcessor() {
// return new MyBeanDefinitionRegistryPostProcessor();
//}
@Bean
static BeanDefinitionRegistryPostProcessor postProcessor() {
return new MyBeanDefinitionRegistryPostProcessor();
}
運行程式,結果輸出:
MyBeanDefinitionRegistryPostProcessor init...
...
AppConfig init...
son init...hashCode() = 2090289474
Parent init...
...
完美。
警告三:非靜態@Bean方法導致@Autowired等註解失效
@Configuration
class AppConfig {
@Autowired
private Parent parent;
@PostConstruct
void init() {
System.out.println("AppConfig.parent = " + parent);
}
AppConfig() {
System.out.println("AppConfig init...");
}
@Bean
BeanFactoryPostProcessor postProcessor() {
return new MyBeanFactoryPostProcessor();
}
@Bean
Son son() {
return new Son();
}
@Bean
Parent parent() {
return new Parent(son());
}
}
class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
MyBeanFactoryPostProcessor() {
System.out.println("MyBeanFactoryPostProcessor init...");
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
運行程式,結果輸出:
AppConfig init...
2020-05-31 08:28:06.550 INFO 1464 --- [ main] o.s.c.a.ConfigurationClassEnhancer : @Bean method
AppConfig.postProcessor is non-static and returns an object assignable to Spring's BeanFactoryPostProcessor
interface. This will result in a failure to process annotations such as @Autowired, @Resource and
@PostConstruct within the method's declaring @Configuration class. Add the 'static' modifier to
this method to avoid these container lifecycle issues; see @Bean javadoc for complete details.
MyBeanFactoryPostProcessor init...
...
son init...hashCode() = 882706486
Parent init...
結果分析(問題點/衝突點):
- AppConfig提前於
MyBeanFactoryPostProcessor
初始化 @Autowired/@PostConstruct
等註解沒有生效,這個問題很大
需要強調的是:此時的AppConfig是被enhance增強成功了的,這樣才有可能進入到
BeanMethodInterceptor
攔截裡面,才有可能輸出這句日誌(該攔截器會攔截Full模式配置列的所有的@Bean方法的執行)
這句日誌由ConfigurationClassEnhancer.BeanMethodInterceptor
輸出,含義為:你的@Bean標註的方法是非static的並且返回了一個BeanFactoryPostProcessor
類型的實例,這就導致了配置類裡面的@Autowired, @Resource,@PostConstruct
等註解都將得不到解析,這是比較危險的(所以其實這個日誌調整為warn級別也是闊儀的)。
小細節:為毛日誌看起來是ConfigurationClassEnhancer這個類輸出的呢?這是因為
BeanMethodInterceptor
是它的靜態內部類,和它共用的一個logger
源碼處解釋:
ConfigurationClassEnhancer.BeanMethodInterceptor:
if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
if (logger.isInfoEnabled() && BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
logger.info(String.format("@Bean method %s.%s is non-static and returns an object " +
"assignable to Spring's BeanFactoryPostProcessor interface. This will " +
"result in a failure to process annotations such as @Autowired, " +
"@Resource and @PostConstruct within the method's declaring " +
"@Configuration class. Add the 'static' modifier to this method to avoid " +
"these container lifecycle issues; see @Bean javadoc for complete details.",
beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName()));
}
return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}
解釋為:如果當前正在執行的@Bean方法(鐵定不是static,因為靜態方法它也攔截不到嘛)返回類型是BeanFactoryPostProcessor
類型,那就輸出此警告日誌來提醒使用者要當心。
解決方案:static關鍵字提升優先順序
AppConfig:
//@Bean
//BeanFactoryPostProcessor postProcessor() {
// return new MyBeanFactoryPostProcessor();
//}
@Bean
static BeanFactoryPostProcessor postProcessor() {
return new MyBeanFactoryPostProcessor();
}
運行程式,結果輸出:
MyBeanFactoryPostProcessor init...
AppConfig init...
son init...hashCode() = 1906549136
Parent init...
// @PostConstruct註解生效嘍
AppConfig.parent = com.yourbatman.bean.Parent@baf1bb3
...
世界一下子又清爽了有木有。
原因總結
以上三個case是有共同點的,粗略的講導致它們的原因甚至是同一個:AppConfig這個Bean被過早初始化。然而我們的解決方案似乎也是同一個:使用static提升Bean的優先順序。
那麼為何AppConfig會被提前初始化呢?為何使用static關鍵字就沒有問題了呢?根本原因可提前劇透:static靜態方法屬於類,執行靜態方法時並不需要初始化所在類的實例;而實例方法屬於實例,執行它時必須先初始化所在類的實例。聽起來是不是非常的簡單,JavaSE的東西嘛,當然只知曉到這個層次肯定是遠遠不夠的,限於篇幅原因,關於Spring是如何處理的源碼級別的分析我放在了下篇文章,請別走開喲~
static靜態方法一定優先執行嗎?
看完本文,有些小夥伴就忍不住躍躍欲試了,甚至很武斷的得出結論:static標註的@Bean方法優先順序更高,其實這是錯誤的,比如你看如下示例:
@Configuration
class AppConfig2 {
AppConfig2(){
System.out.println("AppConfig2 init...");
}
@Bean
Son son() {
return new Son();
}
@Bean
Daughter daughter() {
return new Daughter();
}
@Bean
Parent Parent() {
return new Parent();
}
}
運行程式,結果輸出:
AppConfig2 init...
son init...
Daughter init...
Parent init...
這時候你想讓Parent在Son之前初始化,因此你想著在用static關鍵字來提升優先順序,這麼做:
AppConfig2:
//@Bean
//Parent Parent() {
// return new Parent();
//}
@Bean
static Parent Parent() {
return new Parent();
}
結果:你徒勞了,static貌似並沒有生效,怎麼回事?
原因淺析
為了滿足你的好奇心,這裡給個淺析,道出關鍵因素。我們知道@Bean方法(不管是靜態方法還是實例方法)最終都會被封裝進ConfigurationClass
實例裡面,使用Set<BeanMethod> beanMethods
存儲著,關鍵點在於它是個LinkedHashSet
所以是有序的(存放順序),而存入的順序底層是由clazz.getDeclaredMethods()
來決定的,由此可知@Bean方法執行順序和有無static沒有半毛錢關係。
說明:
clazz.getDeclaredMethods()
得到的是Method[]數組,是有序的。這個順序由位元組碼(定義順序)來保證:先定義,先服務。
由此可見,static並不是真正意義上的提高Bean優先順序,對於如上你的需求case,你可以使用@DependsOn
註解來保證,它也是和Bean順序息息相關的一個註解,在本專欄後續文章中將會詳細講到。
所以關於@Bean方法的執行順序的正確結論應該是:在同一配置類內,在無其它「干擾」情況下(無@DependsOn、@Lazy等註解
),@Bean方法的執行順序遵從的是定義順序(後置處理器類型除外)。
小提問:如果是垮@Configuration配置類的情況,順序如何界定呢?那麼這就不是同一層級的問題了,首先考慮的應該是@Configuration配置類的順序問題,前面有文章提到過配置類是支援有限的的@Order註解排序的,具體分析請依舊保持關注A哥後續文章詳解哈…
static關鍵字使用注意事項
在同一個@Configuration
配置類內,對static關鍵字的使用做出如下說明,供以參考:
- 對於普通類型(非後置處理器類型)的@Bean方法,使用static關鍵字並不能改變順序(按照方法定義順序執行),所以別指望它
- static關鍵字一般有且僅用於@Bean方法返回為
BeanPostProcessor
、BeanFactoryPostProcessor
等類型的方法,並且建議此種方法請務必使用static修飾,否則容易導致隱患,埋雷
static關鍵字不要濫用(其實任何關鍵字皆勿亂用),在同一配置類內,與其說它是提升了Bean的優先順序,倒不如說它讓@Bean方法靜態化從而不再需要依賴所在類的實例即可獨立運行。另外我們知道,static關鍵還可以修飾(內部)類,那麼如果放在類上它又是什麼表現呢?同樣的,你先思考,下篇文章我們接著聊~
說明:使用static修飾Class類在Spring Boot自動配置類里特別特別常見,所以掌握起來很具價值
思考題:
今天的思考題比較簡單:為何文首三種case的警告資訊都是info級別呢?是否有級別過低之嫌?
總結
本文還是蠻幹的哈,不出意外它能夠幫你解決你工程中的某些問題,排除掉一些隱患,畢竟墨菲定律被驗證了你擔心的事它總會發生,防患於未然才能把自己置於安全高地嘛。
你可能詫異,A哥竟能把static關鍵字在Spring中的應用都能寫出個專欄出來,是的,這不是就是本公眾號的定位么 ,小而美和拒絕淺嘗輒止嘛。對於一些知識(比如本文的static關鍵字的使用)我並不推崇強行記憶,因為那真的很容易忘,快速使用可以簡單記記,但真想記得牢(甚至成為永久記憶),那必須得去深水區看看。來吧,下文將授之以漁~
很多小夥伴去強行記憶Spring Boot支援的那17種外部化配置,此時你應該問自己:現在你可能記得,一周以後呢?一個月以後呢?所以你需要另闢蹊徑,那就持續關注我吧😄