Spring解讀@Component和@Configuration的區(qū)別以及源碼分析
之前一直搞不清 @Component 和 @Configuration 這兩個注解到底有啥區(qū)別,一直認(rèn)為被這兩修飾的類可以被 Spring 實(shí)例化嘛,不,還是見識太短,直到今天才發(fā)現(xiàn)這兩玩意有這么大區(qū)別。
很幸運(yùn)能夠及時發(fā)現(xiàn),后面可以少走點(diǎn)坑,下面就直接通過最簡單的案例來說明它兩的區(qū)別,顛覆你的認(rèn)知。
1、案例演示
定義一個 Apple 實(shí)體類,通過 @Bean 的方式交給 Spring 管理,如下:
public class Apple { }
在定義一個 AppleFactory 工廠類也可以獲取到 Apple 類實(shí)例,如下:
public class AppleFactory { private Apple apple; public Apple getApple() { return apple; } public void setApple(Apple apple) { this.apple = apple; } }
在定義一個 AppleAutoConfiguration 類,此時先用 @Configuration 注解修飾該類,如下:
@Configuration public class AppleAutoConfiguration { @Bean public Apple apple() { return new Apple(); } @Bean public AppleFactory appleFactory() { AppleFactory appleFactory = new AppleFactory(); appleFactory.setApple(apple()); return appleFactory; } }
先來分析下 AppleAutoConfiguration 類中的方法,apple() 方法中直接通過 new 關(guān)鍵字創(chuàng)建了一個 Apple 對象,這個沒啥問題
繼續(xù)看到 appleFactory() 方法內(nèi)部,又調(diào)用了 apple() 方法,此時仔細(xì)想想,Spring 是不是調(diào)用了兩次 apple() ,第一次是 Spring 掃描到 @Bean 注解調(diào)用一次,第二次是在 Spring 掃描到 @Bean 注解調(diào)用 appleFactory() 方法,appleFactory() 方法中又調(diào)用一次 apple() 方法,調(diào)兩次 new 創(chuàng)建對象,必然會出現(xiàn)兩個不一樣的 Apple 對象,這樣必然違背了 Spring 單例設(shè)計(jì)思想。
接下來測試下結(jié)果,看下是不是和我們想象的結(jié)果一樣呢?
測試如下:
@ComponentScan public class ComponentConfigurationTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ComponentConfigurationTest.class); AppleFactory appleFactory = context.getBean(AppleFactory.class); Apple apple = appleFactory.getApple(); System.out.println("從 AppleFactory 獲取的 apple = " + apple); Apple bean = context.getBean(Apple.class); System.out.println("從 Spring 容器中獲取的 apple = " + bean); } }
最終輸出結(jié)果如下:
從 AppleFactory 獲取的 apple = com.gwm.configurationanno.Apple@2f465398
從 Spring 容器中獲取的 apple = com.gwm.configurationanno.Apple@2f465398
發(fā)現(xiàn)這結(jié)果和我們剛剛的猜想不一樣啊,其實(shí)大家隱約應(yīng)該猜到,是因?yàn)檫@里使用的是 @Configuration 注解,所以這里最終你調(diào)用多少次最終都是同一個對象,原理稍后分析。
那么接下來肯定是要把 @Configuration 注解替換成 @Component 試試,修改之后的代碼如下:
@Component public class AppleAutoConfiguration { @Bean public Apple apple() { return new Apple(); } @Bean public AppleFactory appleFactory() { AppleFactory appleFactory = new AppleFactory(); appleFactory.setApple(apple()); return appleFactory; } }
??????
測試結(jié)果如下:
從 AppleFactory 獲取的 apple = com.gwm.configurationanno.Apple@2f465398
從 Spring 容器中獲取的 apple = com.gwm.configurationanno.Apple@2f465353
最終發(fā)現(xiàn)這個結(jié)果和我們在前面想象的結(jié)果一模一樣,果然就創(chuàng)建了兩個 Apple 對象,違背了 Spring 單例設(shè)計(jì)思想,這個區(qū)別非常非常的重要,因?yàn)樵?SpringBoot 中為什么配置類都是使用的 @Configuration 注解,并不是直接使用 @Component 注解,這個原因想必大家應(yīng)該也知道了。
上面這個例子在 SpringBoot 中有非常多的應(yīng)用,在來看下另一個例子,如下:
定義一個 Orange 實(shí)體類,通過 FactoryBean 接口將 Orange 類交給 Spring 管理,如下:
public class Orange { }
再定義個 OrangeFactoryBean 類實(shí)現(xiàn) FactoryBean 接口,調(diào)用 getObject() 方法可以創(chuàng)建 Orange 對象,如下:
public class OrangeFactoryBean implements FactoryBean<Orange> { @Override public Orange getObject() throws Exception { return new Orange(); } @Override public Class<?> getObjectType() { return Orange.class; } }
大家都知道實(shí)現(xiàn)了 FactoryBean 接口的類最終會去調(diào)用 getObject() 方法然后創(chuàng)建對象。然后再在 AppleAutoConfiguration 類中調(diào)用 getObject() 方法,這里直接使用 @Component 注解演示,因?yàn)?@Configuration 注解肯定是沒問題的,如下:
@Component public class AppleAutoConfiguration { @Bean public Apple apple() { return new Apple(); } @Bean public AppleFactory appleFactory() throws Exception { AppleFactory appleFactory = new AppleFactory(); appleFactory.setApple(apple()); OrangeFactoryBean orangeFactoryBean = orangeFactoryBean(); System.out.println("appleFactory orangeFactoryBean = " + orangeFactoryBean); Orange orange = orangeFactoryBean.getObject(); System.out.println("appleFactory orange="+orange); return appleFactory; } @Bean public OrangeFactoryBean orangeFactoryBean() { return new OrangeFactoryBean(); } }
這里通過 @Bean 注入 OrangeFactoryBean 實(shí)例,因?yàn)?AppleAutoConfiguration 類被 @Component 修飾,所以這里 Spring 和 手動調(diào)用兩次創(chuàng)建的 OrangeFactoryBean 不一樣,導(dǎo)致 getObject() 其實(shí)也是不一樣的。
測試代碼如下:
@ComponentScan public class ComponentConfigurationTest { public static void main(String[] args) throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ComponentConfigurationTest.class); AppleFactory appleFactory = context.getBean(AppleFactory.class); Apple apple = appleFactory.getApple(); System.out.println("從 AppleFactory 獲取的 apple = " + apple); Apple bean = context.getBean(Apple.class); System.out.println("從 Spring 容器中獲取的 apple = " + bean); Orange bean1 = context.getBean(Orange.class); System.out.println("從 Spring 容器中獲取的 orange = "+bean1); Object bean2 = context.getBean("orangeFactoryBean"); System.out.println("=>從 Spring 容器中獲取的 orange = " + bean2); OrangeFactoryBean bean3 = context.getBean(OrangeFactoryBean.class); System.out.println("bean3 = " + bean3); } }
輸出結(jié)果如下:
appleFactory orangeFactoryBean = com.gwm.configurationanno.OrangeFactoryBean@c0c2f8d
appleFactory orange=com.gwm.configurationanno.Orange@305b7c14
從 AppleFactory 獲取的 apple = com.gwm.configurationanno.Apple@484970b0
從 Spring 容器中獲取的 apple = com.gwm.configurationanno.Apple@4470f8a6
從 Spring 容器中獲取的 orange = com.gwm.configurationanno.Orange@7c83dc97
=>從 Spring 容器中獲取的 orange = com.gwm.configurationanno.Orange@7c83dc97
Spring 容器中的 orangeFactoryBean = com.gwm.configurationanno.OrangeFactoryBean@7748410a
通過以上兩個案例可以清楚的知道 @Configuration 和 @Component 注解的區(qū)別,那么這個底層是怎么實(shí)現(xiàn)的呢?
2、源碼解析
2.1、@Configuration 簡單思路分析
這里我們可以大致的思考下,產(chǎn)生多個實(shí)例無非就是沒有從 Spring 緩存中取值嘛,如果都是從 Spring 緩存中取值,那么必然就不會出現(xiàn)那么多對象。
對于 apple() 方法因?yàn)楸?@Bean 修飾,所以在 Spring 實(shí)例化過程中會被調(diào)用 ,然后創(chuàng)建完實(shí)例將其放到 Spring 單例緩沖池中,那么下次就可以直接從緩存中獲取到 Apple 實(shí)例(@Bean 注入 bean 流程看另一篇文章)。
對于 appleFactory() 方法中去調(diào)用 apple() 方法,apple() 方法又會 new 一個新的 Apple 實(shí)例,那么怎么樣避免它重復(fù)創(chuàng)建對象呢?是不是可以通過代理改變 apple() 方法內(nèi)部的邏輯,改成讓它直接從 Spring 緩沖池中獲取 Apple 的實(shí)例,這樣就避免了重復(fù)創(chuàng)建對象。
如果要把 apple() 方法改成代理方法,是不是需要將所在的類 AppleAutoConfiguration 變成代理對象即可,Spring 就是這樣干的,加上 @Configuration 注解標(biāo)識之后,Spring 就會通過 cglib 代理創(chuàng)建 AppleAutoConfiguration 實(shí)例,所以你在 Spring 容器中獲取 AppleAutoConfiguration 類是一個代理類,并不是真正的實(shí)體類。
跟著上述思路再去看源碼,就簡單多了。
2.2、@Configuration 源碼分析之普通類型方法調(diào)用
首先看 ConfigurationClassPostProcessor 類解析 @Configuration 注解的地方,會先做個 full 標(biāo)記,源碼如下:
標(biāo)記做好了之后,后面就可以通過判斷是否有這個標(biāo)記,然后要不要用代理創(chuàng)建實(shí)例,進(jìn)入到 ConfigurationClassPostProcessor 類的 postProcessBeanFactory() 方法,源碼如下:
這里會去判斷當(dāng)前 beanClass 是否有 full 標(biāo)記,有的話就加入到 configBeanDefs 容器中,準(zhǔn)備要用代理方式生成實(shí)例 bean。
然后開始遍歷 configBeanDefs 容器,通過 ConfigurationClassEnhancer 對象挨個創(chuàng)建代理類,這里增強(qiáng)的是 Class 字節(jié)碼文件,其實(shí)工作中這種方法也是可以借鑒的。
其中增強(qiáng)邏輯都放在了攔截器中(BeanMethodInterceptor、BeanFactoryAwareMethodInterceptor) 源碼如下:
從這兩個攔截器側(cè)面說明 @Configuration 和 @Bean 才是老搭檔,干活不累,緊密相連,相輔相成,所以你在使用 @Bean 的時候,最好在外層類上標(biāo)注 @Configuration,不要使用 @Component 注解。
也就是說當(dāng)你觸發(fā)了目標(biāo)方法的調(diào)用時,就會回調(diào)到這兩個攔截器鏈,但是具體執(zhí)行哪個攔截器是需要條件的,BeanMethodInterceptor 攔截器的條件需要,如下所示:
BeanFactoryAwareMethodInterceptor 攔截器需要條件,源碼如下:
我們這里滿足 BeanMethodInterceptor 攔截器的執(zhí)行條件,所以 Spring 在調(diào)用 apple() 方法的時候,會觸發(fā) BeanMethodInterceptor 攔截器增強(qiáng)邏輯的執(zhí)行,增強(qiáng)邏輯如下:
這里有一個判斷 isCurrentlyInvokedFactoryMethod() 非常關(guān)鍵,因?yàn)檫@個開關(guān)控制著你是否會多次創(chuàng)建實(shí)例,進(jìn)入該方法內(nèi)部,源碼如下:
可以發(fā)現(xiàn)這里有一個 ThreadLocal 類型的容器,那么這個容器什么時候會有值呢?這個就要需要你對 @Bean 的實(shí)例化流程非常了解了,這里簡單摘取核心部分,源碼如下:
在 @Bean 實(shí)例化流程的時候就用到了這個 currentlyInvokedFactoryMethod 容器,先把值放進(jìn)去,然后反射調(diào)用完方法之后又刪除。其實(shí)這只是一個標(biāo)記作用。
Spring 在調(diào)用 @Bean 修飾的 apple() 方法時,currentlyInvokedFactoryMethod 容器中放的就是 apple,然后通過 set() 反射調(diào)用 apple() 方法,注意此時的 AppleAutoConfiguration 是一個代理對象,所以調(diào)用 apple() 方法就會觸發(fā)走切面邏輯,因?yàn)槭?@Bean 修飾的,所以走的是 BeanMethodInterceptor 這個類的增強(qiáng)邏輯,源碼如下:
注意此時的判斷邏輯 isCurrentlyInvokedFactoryMethod() 是有值的哦,存的就是 apple,所以這里就直接走 if 邏輯,直接調(diào)用目標(biāo)方法邏輯,直接 new Apple() 對象,然后返回到 set() 反射調(diào)用處,在刪除 currentlyInvokedFactoryMethod 容器中的值 apple,然后就是 Spring 實(shí)例化 @Bean 的后續(xù)流程,最終會將這個 new 出來的實(shí)例放到 Spring 一級緩存中,源碼如下:
那么重點(diǎn)來了,Spring 在執(zhí)行 @Bean 修飾的 appleFactory() 方法時, isCurrentlyInvokedFactoryMethod 容器中那么就是存的 appleFactory,然后通過反射 set() 方法去調(diào)用 appleFactory() 方法,然后再 appleFactory() 方法中執(zhí)行邏輯時發(fā)現(xiàn)又調(diào)用了 apple() 方法,那么又會觸發(fā)進(jìn)入 apple() 方法的增強(qiáng)邏輯 BeanMethodInterceptor,源碼如下:
注意此時的 isCurrentlyInvokedFactoryMethod() 判斷邏輯,當(dāng)前入?yún)?beanMethod 是 apple,但是 isCurrentlyInvokedFactoryMethod 容器中剛剛存放的是外面方法 appleFactory() 的值,所以這里 isCurrentlyInvokedFactoryMethod() 方法判斷條件不成立,走 resolveBeanReference() 邏輯,源碼如下:
這里面這段邏輯會觸發(fā) getBean() 流程,此時 getBean() 流程去獲取 Apple 類實(shí)例,肯定是從單例緩沖池中獲取得,因?yàn)橹霸趫?zhí)行 @Bean 修飾的 apple() 方法就已將實(shí)例存入到了 Spring 的一級緩存中,所以在 appleFactory() 方法中,不管你調(diào)用多少次,都不會重復(fù)創(chuàng)建 Apple 類實(shí)例,因?yàn)樽罱K都是通過切面邏輯去調(diào)用 getBean() 從緩存中獲取得,必然是同一個實(shí)例。
2.3、@Configuration 源碼分析之 FactoryBean 類型方法調(diào)用
Apple 是一個普通類,上面已經(jīng)解析完,現(xiàn)在來解析一下實(shí)現(xiàn) FactoryBean 接口的 OrangeFactoryBean 特殊一點(diǎn)類型的看 Spring 又是如何處理的。
public class Orange { } public class OrangeFactoryBean implements FactoryBean<Orange> { @Override public Orange getObject() throws Exception { return new Orange(); } @Override public Class<?> getObjectType() { return Orange.class; } } @Configuration public class AppleAutoConfiguration { @Bean public AppleFactory appleFactory() throws Exception { OrangeFactoryBean orangeFactoryBean = orangeFactoryBean(); System.out.println("appleFactory orangeFactoryBean = " + orangeFactoryBean); Orange orange = orangeFactoryBean.getObject(); System.out.println("appleFactory orange="+orange); return appleFactory; } @Bean public OrangeFactoryBean orangeFactoryBean() { return new OrangeFactoryBean(); } }
其實(shí)只需要看切面邏輯就可以,AppleAutoConfiguration 類的實(shí)例是一個代理對象,在調(diào)用 appleFactory() 方法里面調(diào)用 orangeFactoryBean() 方法時會觸發(fā)進(jìn)入切面邏輯(BeanMethodInterceptor),因?yàn)?OrangeFactoryBean 這個類有點(diǎn)特殊,實(shí)現(xiàn)了 FactoryBean 接口,所以在切面邏輯(BeanMethodInterceptor)實(shí)現(xiàn)會有一點(diǎn)不一樣,源碼如下:
從上面源碼分析,當(dāng)在 appleFactory() 方法中調(diào)用 orangeFactoryBean() 方法時會觸發(fā)進(jìn)入 BeanMethodInterceptor 切面邏輯,然后在切面中會去判斷是否是 FactoryBean 接口類型,恰好 OrangeFactoryBean 就是 FactoryBean 類型,所以會直接調(diào)用 getBean() 流程,此時注意,beanName 是包含了 & 符號,表示是需要實(shí)例化 OrangeFactoryBean 類,這個特別注意。因?yàn)椴粠?& 符號的話會調(diào)用 getObject() 方法創(chuàng)建 Orange 實(shí)例。注意這里是包含了 & 符號,是要去創(chuàng)建 OrangeFactoryBean 實(shí)例。
當(dāng)調(diào)用 getBean() 去創(chuàng)建 OrangeFactoryBean 實(shí)例時,因?yàn)?AppleAutoConfiguration 類是一個代理類,所以在調(diào)用 AppleAutoConfiguration 類中調(diào)用 orangeFactoryBean() 方法創(chuàng)建 OrangeFactoryBean 實(shí)例時會又會觸發(fā)切面邏輯,又會走上面的邏輯,但是注意注意注意注意此時的 beanName 是不帶 & 符號的哦,此時的 beanName 就是方法名稱,所以此時就會進(jìn)入 else 邏輯進(jìn)入 enhanceFactoryBean() 方法中,源碼如下:
從源碼中可以看到又是通過 cglib 創(chuàng)建了一個 OrangeFactoryBean 的代理對象,注意這里的攔截器邏輯,只有當(dāng)你調(diào)用了 OrangeFactoryBean 類中的 getObject() 方法才會做特殊增強(qiáng),去調(diào)用 getBean() 邏輯,同時 注意 beanName 是不帶 & 符號的,也就是去創(chuàng)建 Orange 類實(shí)例,除了 getObject() 方法之外的所有方法不做任何處理,直接進(jìn)回調(diào)即可。至此 OrangeFactoryBean 類實(shí)例已經(jīng)創(chuàng)建好了,在 Spring 容器中是一個代理類。
然后再看到我們自己的代碼如下:
@Configuration public class AppleAutoConfiguration { @Bean public AppleFactory appleFactory() throws Exception { OrangeFactoryBean orangeFactoryBean = orangeFactoryBean(); System.out.println("appleFactory orangeFactoryBean = " + orangeFactoryBean); Orange orange = orangeFactoryBean.getObject(); System.out.println("appleFactory orange="+orange); return appleFactory; } @Bean public OrangeFactoryBean orangeFactoryBean() { return new OrangeFactoryBean(); } }
剛才已執(zhí)行到第一行代碼,執(zhí)行完后,獲取到一個 OrangeFactoryBean 代理對象,然后開始執(zhí)行第二行代碼,注意這里隱式調(diào)用了 toString() 方法,會觸發(fā) OrangeFactoryBean 代理類的切面邏輯,而 OrangeFactoryBean 代理類只對 getObject() 方法有特殊處理,其他的方法都不做處理,就是直接回調(diào)而已,所以 toString() 的切面邏輯不用太在乎。
接下來執(zhí)行第三行代碼,調(diào)用 getObject() 方法,getObject() 方法是 OrangeFactoryBean 代理類非常關(guān)心的方法,在源碼中寫死要對 getObject() 方法進(jìn)行處理。最終會調(diào)用到 getBean(orange) 流程,實(shí)例化 Orange bean,最終將 Orange 實(shí)例放入到 Spring 一級緩存。
所以最終在 appleFactory() 方法中執(zhí)行 Orange orange = orangeFactoryBean.getObject()
代碼和在測試類中執(zhí)行 getBean(Orange.class) 或者 getBean(“orangeFactoryBean”) 代碼都是從同一個地方獲取到的值(Spring 中的一級緩存中),雖然多個地方調(diào)用,表面上給人的感覺是調(diào)用了多次,會出現(xiàn)多個實(shí)例,但是 Spring 中用代理的方式從底層幫我們解決了這個問題。但是前提是要使用 @Configuration 注解才會生效。
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
- SpringBoot中@ConfigurationProperties自動獲取配置參數(shù)的流程步驟
- SpringBoot中的@Configuration、@MapperScan注解
- SpringBoot的ConfigurationProperties或Value注解無效問題及解決
- Springboot之@ConfigurationProperties注解解讀
- Springboot中@ConfigurationProperties輕松管理應(yīng)用程序的配置信息詳解
- 解決Spring運(yùn)行時報(bào)錯:Consider defining a bean of type ‘xxx.xxx.xxx.Xxx‘ in your configuration
相關(guān)文章
SpringBoot + Spring Cloud Consul 服務(wù)注冊和發(fā)現(xiàn)詳細(xì)解析
這篇文章主要介紹了SpringBoot + Spring Cloud Consul 服務(wù)注冊和發(fā)現(xiàn),本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07背包問題-動態(tài)規(guī)劃java實(shí)現(xiàn)的分析與代碼
這篇文章主要給大家介紹了關(guān)于背包問題動態(tài)規(guī)劃java實(shí)現(xiàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Java transient關(guān)鍵字與序列化操作實(shí)例詳解
這篇文章主要介紹了Java transient關(guān)鍵字與序列化操作,結(jié)合實(shí)例形式詳細(xì)分析了java序列化操作相關(guān)實(shí)現(xiàn)方法與操作注意事項(xiàng),需要的朋友可以參考下2019-09-09Apache?SkyWalking?修復(fù)TTL?timer?失效bug詳解
這篇文章主要為大家介紹了Apache?SkyWalking?修復(fù)TTL?timer?失效bug詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09java 解決異常 2 字節(jié)的 UTF-8 序列的字節(jié)2 無效的問題
這篇文章主要介紹了java 解決異常 2 字節(jié)的 UTF-8 序列的字節(jié) 2 無效的問題的相關(guān)資料,需要的朋友可以參考下2016-12-12解決MultipartFile.transferTo(dest) 報(bào)FileNotFoundExcep的問題
這篇文章主要介紹了解決MultipartFile.transferTo(dest) 報(bào)FileNotFoundExcep的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Spring Boot從Controller層進(jìn)行單元測試的實(shí)現(xiàn)
這篇文章主要介紹了Spring Boot從Controller層進(jìn)行單元測試的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04