基于Spring框架由ConditionalOnMissingBean注解引發(fā)的問題
問題描述
最新負責(zé)的工程在做dubbo配置disconf靜態(tài)配置托管優(yōu)化,由一個StaticConfigPropertiesFactoryBean來讀取靜態(tài)配置,它是PropertiesFactoryBean的一個子類,讀取到的所有配置放在一個Properties中。
dubbo配置直接引用這個Properties的值。
像下面這樣,dubbo接口的group通過disconf的靜態(tài)配置項dubbo.hst.pay.group定義:
工程使用Spring Boot框架,Spring版本號4.1.1,Spring Boot版本號1.1.9,spring-security版本號4.0.2,啟用了Spring Boot框架的Anto Configuration特性。
StaticConfigPropertiesFactoryBean這個bean的configSrc屬性值是個占位符,值在外部配置文件定義。
但是啟動工程時出現(xiàn)了一個問題,拋出了一個IllegalArgumentException異常,異常日志如下:
Caused by: java.lang.IllegalArgumentException: configSrc error : ${disconf.configSrc}
at com.baidu.disconf.client.haitao.properties.StaticConfigPropertiesFactoryBean.createProperties(StaticConfigPropertiesFactoryBean.java:65)
at org.springframework.beans.factory.config.PropertiesFactoryBean.afterPropertiesSet(PropertiesFactoryBean.java:71)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
... 35 more
排查過程
從日志中看到configSrc這個屬性的值不滿足條件導(dǎo)致拋出了這個異常,而且值是${disconf.configSrc},這里比較奇怪,在初始化bean的時候框架不是會對占位符進行解析求職處理的么,什么原因?qū)е抡嘉环幢惶幚韇ean就開始初始化了?
我們知道Spring框架由PropertySourcesPlaceholderConfigurer來處理占位符,PropertySourcesPlaceholderConfigurer是一個BeanFactoryPostProcessor,在容器所有BeanDefinition被加載之后,bean初始化之前,BeanFactoryPostProcessor會被激活,PropertySourcesPlaceholderConfigurer會加載所有被引入的屬性文件,并且查找占位符對應(yīng)的值,并把對應(yīng)的值設(shè)置到bean屬性對應(yīng)的PropertyValue中,這樣在bean初始化時設(shè)置每個屬性的值時設(shè)置的就是處理后的值了。
org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory, List)
就是這在觸發(fā)這個BeanFactoryPostProcessor的,注意代碼行數(shù)是162行。
但是現(xiàn)在的現(xiàn)象是configSrc這個屬性初始化之后卻是未處理的占位符字符串。
分析原因有兩種可能,第一種disconf.configSrc這個配置項在文件中未定義或未被容器加載,第二種StaticConfigPropertiesFactoryBean在屬性占位符處理之前就初始化了。
第一種原因很容易就排除了,disconf.configSrc這個配置項已經(jīng)定義了,在這次修改之前就已存在之前一直都是沒問題的,且由于ignoreUnresolvablePlaceholders這個設(shè)置默認是false,就算它未被定義容器也會提前拋出IllegalArgumentException異常:
org.springframework.util.PropertyPlaceholderHelper.parseStringValue(String, PlaceholderResolver, Set)
所以只能是第二種原因了,那就是StaticConfigPropertiesFactoryBean在屬性占位符處理之前就被容器初始化了。Spring框架如此龐大,要找原因最有效的手段就是debug了,把斷點打到StaticConfigPropertiesFactoryBean初始化代碼處,看它是從哪進來的:
org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory, List)
通過debug發(fā)現(xiàn),這個bean的初始化也是從BeanFactoryPostProcessor調(diào)進去的,而且看下代碼是94行,在占位符處理的164行前面,好了,這里可以解釋configSrc值為什么是錯的。
但是這些BeanFactoryPostProcessor是干嘛的?為什么在其它BFPP激活之前就去初始化bean了。
看一下這個BFPP是什么:
這個BFPP是ConfigurationClassPostProcessor,當(dāng)應(yīng)用使用了annotation-config或component-scan掃描注解bean時,容器會自動注冊一個ConfigurationClassPostProcessor來加載注解定義的BeanDefinition,按常理,這個BFPP也只會生成BeanDefinition,不會執(zhí)行bean初始化動作。
那么是什么地方導(dǎo)致bean發(fā)生初始化了呢?
再看下上面哪個調(diào)用棧,在OnMissingBeanCondition.matches之后就調(diào)用BeanFactory的getBeansXXX了,然后一步一步觸發(fā)了createBean。
那這個OnMissingBeanCondition從哪來的呢?
從debug的情況來看跟WebMvcSecurityConfiguration有關(guān)。
看下WebMvcSecurityConfiguration這個類,它有一個@ConditionalOnMissingBean注解,這個注解作用在bean定義上,它的作用就是在容器加載它作用的bean時,檢查容器中是否存在目標(biāo)類型(ConditionalOnMissingBean注解的value值)的bean了,如果存在這跳過原始bean的BeanDefinition加載動作。
上面說的那段邏輯就是由OnMissingBeanCondition.matches來完成的。
也就是說上面那整棵調(diào)用樹所做的事情就是檢查容器知否已存在RequestDataValueProcessor類型的bean,如果存在則不再重復(fù)重新加載這個RequestDataValueProcessor,如果是普通的bean那還好辦直接比較bean的類型是否是RequestDataValueProcessor類型就行了,不必去初始化整個bean,但是如果是FactoryBean那就壞事了,因為FactoryBean類型的bean最終創(chuàng)建的bean的類型并不是FactoryBean本身的類型,而是由它的getObject返回值來決定的,所以要拿到bean類型,會調(diào)它的getObject方法創(chuàng)建bean之后再比較類型。
而dubbo消費者bean的類型都是com.alibaba.dubbo.config.spring.ReferenceBean,這恰恰是一個FactoryBean,此時會初始化消費者bean,設(shè)置group屬性時接著初始化引用的的disconfPropertiesReader bean,所以就導(dǎo)致了悲劇。
org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch
最后只剩下最后一個問題,WebMvcSecurityConfiguration是從哪來的,搜索代碼,直接宣布結(jié)果:
由于工程使用了AutoConfiguration特性,框架查找并讀取所有名稱是*AutoConfiguration的類,所以會找到SecurityAutoConfiguration。
這個類定義了@Configuration注解,所以會加載這個類中定義的bean,發(fā)現(xiàn)了@Import注解,根據(jù)這個標(biāo)簽讀取到并解析SpringBootWebSecurityConfiguration,繼續(xù)讀取到它的內(nèi)部類WebMvcSecurityConfigurationConditions,再讀取到內(nèi)部類的內(nèi)部類DefaultWebMvcSecurityConfiguration,這個內(nèi)部類上有@EnableWebMvcSecurity注解,這個注解又@Import了WebMvcSecurityConfiguration,這個類有@EnableWebSecurity注解,注解@Import了SpringWebMvcImportSelector,這個ImportSelector會導(dǎo)入org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration:
總結(jié)產(chǎn)生問題的最終原因:
是因為工程使用了Spring Boot的Auto Configuration功能,而根據(jù)這個工程的特點會自動加載安全配置SecurityAutoConfiguration,由此經(jīng)過一系列的直接或間接的import,會導(dǎo)致框架讀取WebMvcSecurityConfiguration,而這個配置在加載RequestDataValueProcessor這個bean定義的時候由于有@ConditionalOnMissingBean的作用導(dǎo)致框架會檢查容器中是否已經(jīng)存在了RequestDataValueProcessor類型的bean實例,在檢查的過程中當(dāng)掃描到sendCouponRemoteApiImpl這個dubbo消費者bean時,由于它是一個FactoryBean,getObject方法會被調(diào)用進而執(zhí)行了bean的初始化動作,而由于此時由于處理屬性占位符的PropertySourcesPlaceholderConfigurer還未被激活,所以導(dǎo)致bean的屬性值沒有被正確初始化。
解決方案
治標(biāo)的方案,考慮到工程主要是提供dubbo服務(wù)無需啟用安全配置,可以禁調(diào)安全配置的自動加載,@EnableAutoConfiguration注解有exclude屬性,通過它可以禁用對SecurityAutoConfiguration的自動加載:
治本的方案,spring社區(qū)也有人提出了相同的問題:https://jira.spring.io/browse/SEC-3063,已確定是spring的bug,官方已在spring-security-config的4.1.0版本解決了這個問題,刪掉了
ConditionalOnMissingBean這個注解,看代碼WebMvcSecurityConfiguration也不再使用這個注解了,升級spring-security-config到4.1.0解決問題:
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java中如何快速構(gòu)建項目腳手架的實現(xiàn)
這篇文章主要介紹了Java中如何快速構(gòu)建項目腳手架,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05Spring Cloud Ribbon實現(xiàn)客戶端負載均衡的示例
本篇文章主要介紹了Spring Cloud Ribbon實現(xiàn)客戶端負載均衡的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02Spring?Boot?2.6.x整合Swagger啟動失敗報錯問題的完美解決辦法
這篇文章主要給大家介紹了關(guān)于Spring?Boot?2.6.x整合Swagger啟動失敗報錯問題的完美解決辦法,文中通過實例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-03-03把Java程序轉(zhuǎn)換成exe,可直接運行的實現(xiàn)
這篇文章主要介紹了把Java程序轉(zhuǎn)換成exe,可直接運行的實現(xiàn),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09Mybatis入門指南之實現(xiàn)對數(shù)據(jù)庫增刪改查
數(shù)據(jù)持久層主要負責(zé)數(shù)據(jù)的增、刪、改、查等功能,MyBatis 則是一款優(yōu)秀的持久層框架,下面這篇文章主要給大家介紹了關(guān)于Mybatis入門指南之實現(xiàn)對數(shù)據(jù)庫增刪改查的相關(guān)資料,需要的朋友可以參考下2022-10-10