天天看點

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

作者:程式猿不相信眼淚

疑慮描述

最近,在進行開發的過程中,發現之前的一個寫法,類似如下

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

以我的了解,@Configuration 加 @Bean 會建立一個 userName 不為 null 的 UserManager 對象,而 @Component 也會建立一個 userName 為 null 的 UserManager 對象

那麼我們在其他對象中注入 UserManager 對象時,到底注入的是哪個對象?

因為項目已經上線了很長一段時間了,是以這種寫法沒有編譯報錯,運作也沒有出問題

後面去找同僚了解下,實際是想讓

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

生效,而實際也确實是它生效了

那麼問題來了:Spring 容器中到底有幾個 UserManager 類型的對象?

Spring Boot 版本

項目中用的 Spring Boot 版本是:2.0.3.RELEASE

對象的 scope 是預設值,也就是 singleton

基于 Spring Boot + MyBatis Plus + Vue & Element 實作的背景管理系統 + 使用者小程式,支援 RBAC 動态權限、多租戶、資料權限、工作流、三方登入、支付、短信、商城等功能。

項目位址:https://github.com/YunaiV/ruoyi-vue-pro

結果驗證

驗證方式有很多,可以 debug 跟源碼,看看 Spring 容器中到底有幾個 UserManager 對象,也可以直接從 UserManager 構造方法下手,看看哪幾個構造方法被調用,等等

我們從構造方法下手,看看 UserManager 到底執行個體化了幾次

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

隻有有參構造方法被調用了,無參構造方法巋然不動(根本沒被調用)

既然 UserManager 構造方法隻被調用了一次,那麼前面的問題:到底注入的是哪個對象

答案也就清晰了,沒得選了呀,隻能是 @Configuration 加 @Bean 建立的 userName 不為 null 的 UserManager 對象

問題又來了:為什麼不是 @Component 建立的 userName 為 null 的 UserManager 對象?

基于微服務的思想,建構在 B2C 電商場景下的項目實戰。核心技術棧,是 Spring Boot + Dubbo 。未來,會重構成 Spring Cloud Alibaba 。

項目位址:https://github.com/YunaiV/onemall

源碼解析

@Configuration 與 @Component 關系很緊密

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

是以@Configuration 能夠被 component scan

其中 ConfigurationClassPostProcessor 與@Configuration 息息相關,其類繼承結構圖如下:

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

它實作了 BeanFactoryPostProcessor 接口和 PriorityOrdered 接口,關于 BeanFactoryPostProcessor,

那麼我們從 AbstractApplicationContext 的 refresh 方法調用的 invokeBeanFactoryPostProcessors(beanFactory)開始,來跟下源碼

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

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

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

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

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

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?
騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

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

循環完之後,我們來看看 configClasses

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

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

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

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

是不是有什麼發現?@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]]
           

隻是日志級别是 info ,太不顯眼了

Spring 更新優化

可能 Spring 團隊意識到了 info 級别太不顯眼的問題,或者說意識到了直接覆寫的處理方式不太合理

是以在 Spring 5.1.2.RELEASE (Spring Boot 則是 2.1.0.RELEASE )做出了優化處理

我們來具體看看

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

啟動直接報錯,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.
           

我們來跟下源碼,主要看看與 Spring 5.0.7.RELEASE 的差別

騰訊二面:@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 ,将主動權交給了開發者,由開發者自己決定是否允許覆寫

補充

關于 allowBeanDefinitionOverriding ,前面講的不對,後面特意去翻了下源碼,補充如下

Spring 1.2 引進 DefaultListableBeanFactory 的時候就有了 private boolean allowBeanDefinitionOverriding = true;,預設是允許 BeanDefinition 覆寫

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

Spring 4.1.2 引進了 isAllowBeanDefinitionOverriding()方法

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

Spring 自始至終預設都是允許 BeanDefinition 覆寫的,變的是 Spring Boot , Spring Boot 2.1.0 之前沒有覆寫 Spring 的 allowBeanDefinitionOverriding 預設值,仍是允許 BeanDefinition 覆寫的

Spring Boot 2.1.0 中 SpringApplication 定義了私有屬性:allowBeanDefinitionOverriding

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

沒有顯示的指定值,那麼預設值就是 false ,之後在 Spring Boot 啟動過程中,會用此值覆寫掉 Spring 中的 allowBeanDefinitionOverriding 的預設值

騰訊二面:@Bean 與 @Component 用在同一個類上,會怎麼樣?

關于 allowBeanDefinitionOverriding ,我想大家應該已經清楚了

原文連結:https://mp.weixin.qq.com/s/zDq3X-4Gzwmg_KKd07plVw

繼續閱讀