關於 Spring Boot 中創建對象的疑慮 → @Bean 與 @Component 同時作用同一個類,會怎麼樣?
- 2021 年 10 月 8 日
- 筆記
- @Component, @Configuration, spring, Spring Boot, spring源碼
開心一刻
今天放學回家,氣憤憤地找到我媽
我:媽,我們班同學都說我五官長得特別平
媽:你小時候愛趴著睡覺
我:你怎麼不把我翻過來呢
媽:那你不是凌晨2點時候出生的嗎
我:嗯,凌晨2點出生就愛趴著睡覺唄
爸:凌晨 2 點是丑時,丑!
媽:我把你翻過來,我看著你,我害怕呀
我內心一咯噔:敢情我不是天生的五官平呀,哎,雖不是天生,但勝似天生了
疑慮背景
疑慮描述
最近,在進行開發的過程中,發現之前的一個寫法,類似如下
以我的理解,@Configuration 加 @Bean 會創建一個 userName 不為 null 的 UserManager 對象,而 @Component 也會創建一個 userName 為 null 的 UserManager 對象
那麼我們在其他對象中注入 UserManager 對象時,到底注入的是哪個對象?
因為項目已經上線了很長一段時間了,所以這種寫法沒有編譯報錯,運行也沒有出問題
後面去找同事了解下,實際是想讓
生效,而實際也確實是它生效了
那麼問題來了: Spring 容器中到底有幾個 UserManager 類型的對象?
Spring Boot 版本
項目中用的 Spring Boot 版本是: 2.0.3.RELEASE
對象的 scope 是默認值,也就是 singleton
結果驗證
驗證方式有很多,可以 debug 跟源碼,看看 Spring 容器中到底有幾個 UserManager 對象,也可以直接從 UserManager 構造方法下手,看看哪幾個構造方法被調用,等等
我們從構造方法下手,看看 UserManager 到底實例化了幾次
只有有參構造方法被調用了,無參構造方法巋然不動(根本沒被調用)
如果想了解的更深一點,可以讀讀鄙人的:Spring 的循環依賴,源碼詳細分析 → 真的非要三級快取嗎
既然 UserManager 構造方法只被調用了一次,那麼前面的問題: 到底注入的是哪個對象
答案也就清晰了,沒得選了呀,只能是 @Configuration 加 @Bean 創建的 userName 不為 null 的 UserManager 對象
問題又來了:為什麼不是 @Component 創建的 userName 為 null 的 UserManager 對象?
源碼解析
@Configuration 與 @Component 關係很緊密
所以 @Configuration 能夠被 component scan
在spring-boot-2.0.3源碼篇 – @Configuration、Condition與@Conditional中講到了 @Configuration 的實現原理
其中 ConfigurationClassPostProcessor 與 @Configuration 息息相關,其類繼承結構圖如下:
它實現了 BeanFactoryPostProcessor 介面和 PriorityOrdered 介面,關於 BeanFactoryPostProcessor ,可以看看鄙人的Spring拓展介面之BeanFactoryPostProcessor,佔位符與敏感資訊解密原理
那麼我們從 AbstractApplicationContext 的 refresh 方法調用的 invokeBeanFactoryPostProcessors(beanFactory) 開始,來跟下源碼
此時完成了 com.lee.qsl 包下的 component scan , com.lee.qsl 包及子包下的 UserConfig 、 UserController 和 UserManager 都被掃描出來
注意,此刻 @Bean 的處理還未開始, UserManager 是通過 @Component 而被掃描出來的;此時 Spring 容器中 beanDefinitionMap 中的 UserManager 是這樣的
接下來一步很重要,與我們想要的答案息息相關
循環遞歸處理 UserConfig 、 UserController 和 UserManager ,把它們都封裝成 ConfigurationClass ,遞歸掃描 BeanDefinition
循環完之後,我們來看看 configClasses
UserConfig bean 定義資訊中 beanMethods 中有一個元素 [BeanMethod:name=userManager,declaringClass=com.lee.qsl.config.UserConfig]
然後我們接著往下走,來仔細看看答案出現的環節
是不是有什麼發現? @Component 修飾的 UserManager 定義直接被覆蓋成了 @Configuration + @Bean 修飾的 UserManager 定義
Bean 定義類型也由 ScannedGenericBeanDefinition 替換成了 ConfigurationClassBeanDefinition
後續通過 BeanDefinition 創建實例的時候,創建的自然就是 @Configuration + @Bean 修飾的 UserManager ,也就是會反射調用 UserManager 的有參構造方法
自此,答案也就清楚了
Spring 其實給出了提示


2021-10-03 20:37:33.697 INFO 13600 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'userManager' with a different definition: replacing [Generic bean: class [com.lee.qsl.manager.UserManager]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [D:\qsl-project\spring-boot-bean-component\target\classes\com\lee\qsl\manager\UserManager.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=userConfig; factoryMethodName=userManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/lee/qsl/config/UserConfig.class]]
View Code
只是日誌級別是 info ,太不顯眼了
Spring 升級優化
可能 Spring 團隊意識到了 info 級別太不顯眼的問題,或者說意識到了直接覆蓋的處理方式不太合理
所以在 Spring 5.1.2.RELEASE (Spring Boot 則是 2.1.0.RELEASE )做出了優化處理
我們來具體看看
啟動直接報錯,Spring 也給出了提示


The bean 'userManager', defined in class path resource [com/lee/qsl/config/UserConfig.class], could not be registered. A bean with that name has already been defined in file [D:\qsl-project\spring-boot-bean-component\target\classes\com\lee\qsl\manager\UserManager.class] and overriding is disabled.
View Code
我們來跟下源碼,主要看看與 Spring 5.0.7.RELEASE 的區別
新增了配置項 allowBeanDefinitionOverriding 來控制是否允許 BeanDefinition 覆蓋,默認情況下是不允許的
我們可以在配置文件中配置: spring.main.allow-bean-definition-overriding=true ,允許 BeanDefinition 覆蓋
這種處理方式是更優的,將選擇權交給開發人員,而不是自己偷偷的處理,已達到開發者想要的效果
總結
Spring 5.0.7.RELEASE ( Spring Boot 2.0.3.RELEASE ) 支援 @Configuration + @Bean 與 @Component 同時作用於同一個類
啟動時會給 info 級別的日誌提示,同時會將 @Configuration + @Bean 修飾的 BeanDefinition 覆蓋掉 @Component 修飾的 BeanDefinition
也許 Spring 團隊意識到了上述處理不太合適,於是在 Spring 5.1.2.RELEASE 做出了優化處理
增加了配置項: allowBeanDefinitionOverriding ,將主動權交給了開發者,由開發者自己決定是否允許覆蓋