關於 Spring Boot 中創建對象的疑慮 → @Bean 與 @Component 同時作用同一個類,會怎麼樣?

開心一刻

  今天放學回家,氣憤憤地找到我媽

  我:媽,我們班同學都說我五官長得特別平

  媽:你小時候愛趴着睡覺

  我:你怎麼不把我翻過來呢

  媽:那你不是凌晨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 ,將主動權交給了開發者,由開發者自己決定是否允許覆蓋