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

青石路發表於2021-10-08

開心一刻

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

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

  媽:你小時候愛趴著睡覺

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

  媽:那你不是凌晨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 息息相關,其類繼承結構圖如下:

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

  它實現了 BeanFactoryPostProcessor 介面和 PriorityOrdered 介面,關於 BeanFactoryPostProcessor ,可以看看鄙人的Spring擴充介面之BeanFactoryPostProcessor,佔位符與敏感資訊解密原理

  那麼我們從 AbstractApplicationContext 的 refresh 方法呼叫的 invokeBeanFactoryPostProcessors(beanFactory) 開始,來跟下原始碼

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

  此時完成了 com.lee.qsl 包下的 component scan , com.lee.qsl 包及子包下的 UserConfig 、 UserController 和 UserManager 都被掃描出來

  注意,此刻 @Bean 的處理還未開始, UserManager 是通過 @Component 而被掃描出來的;此時 Spring 容器中 beanDefinitionMap 中的 UserManager 是這樣的

  接下來一步很重要,與我們想要的答案息息相關

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

  迴圈遞迴處理 UserConfig 、 UserController 和 UserManager ,把它們都封裝成 ConfigurationClass ,遞迴掃描 BeanDefinition 

  迴圈完之後,我們來看看 configClasses 

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

   UserConfig bean 定義資訊中 beanMethods 中有一個元素 [BeanMethod:name=userManager,declaringClass=com.lee.qsl.config.UserConfig] 

  然後我們接著往下走,來仔細看看答案出現的環節

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

  是不是有什麼發現? @Component 修飾的 UserManager 定義直接被覆蓋成了 @Configuration + @Bean 修飾的 UserManager 定義

  Bean 定義型別也由 ScannedGenericBeanDefinition 替換成了 ConfigurationClassBeanDefinition 

  後續通過 BeanDefinition 建立例項的時候,建立的自然就是 @Configuration + @Bean 修飾的 UserManager ,也就是會反射呼叫 UserManager 的有參構造方法

  自此,答案也就清楚了

  Spring 其實給出了提示

關於 Spring Boot 中建立物件的疑慮 → @Bean 與 @Component 同時作用同一個類,會怎麼樣?
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 Boot 中建立物件的疑慮 → @Bean 與 @Component 同時作用同一個類,會怎麼樣?

  啟動直接報錯,Spring 也給出了提示

關於 Spring Boot 中建立物件的疑慮 → @Bean 與 @Component 同時作用同一個類,會怎麼樣?
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 的區別

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

  新增了配置項 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 ,將主動權交給了開發者,由開發者自己決定是否允許覆蓋

相關文章