前言
在今天,依然有許多人對循環(huán)依賴有著爭論,也有許多面試官愛問循環(huán)依賴的問題,更甚至是在Spring中只問循環(huán)依賴,在國內(nèi),這彷佛成了Spring的必學知識點,一大特色,也被眾多人津津樂道。而我認為,這稱得上Spring框架里眾多優(yōu)秀設(shè)計中的一點污漬,一個為不良設(shè)計而妥協(xié)的實現(xiàn),要知道,Spring整個項目里也沒有出現(xiàn)循環(huán)依賴的地方,這是因為Spring項目太簡單了嗎?恰恰相反,Spring比絕大多數(shù)項目要復雜的多。同樣,在Spring-Boot 2.6.0 Realease Note中也說明不再默認支持循環(huán)依賴,如要支持需手動開啟(以前是默認開啟),但強烈建議通過修改項目來打破循環(huán)依賴。
本篇文章我想來分享一下關(guān)于我對循環(huán)依賴的思考,當然,在這之前,我會先帶大家溫故一些關(guān)于循環(huán)依賴的知識。
依賴注入
由于循環(huán)依賴是在依賴注入的過程中發(fā)生的,我們先簡單回顧一下依賴注入的過程。
案例:
@Component
public class Bar {
}
@Component
public class Foo {
@Autowired
private Bar bar;
}
@ComponentScan(basePackages = "com.my.demo")
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
context.getBean("foo");
}
}
以上為一個非常簡單的Spring入門案例,其中Foo注入了Bar, 該注入過程發(fā)生于context.getBean("foo")中。
過程如下:
1、通過傳入的"foo", 查找對應的BeanDefinition, 如果你不知道什么是BeanDefinition,那你可以把它理解成封裝了bean對應Class信息的對象,通過它Spring可以得到beanClass以及beanClass標識的一些注解。
2、使用BeanDefinition中的beanClass,通過反射的方式進行實例化,得到我們所謂的bean(foo)。
3、解析beanClass信息,得到標識了Autowired注解的屬性(bar)
4、使用屬性名稱(bar),再次調(diào)用context.getBean('bar'),重復以上步驟
5、將得到的bean(bar)設(shè)值到foo的屬性(bar)中
[圖片上傳失敗...(image-63fad4-1653811523515)]
以上為簡單的流程描述
什么是循環(huán)依賴
循環(huán)依賴其實就是A依賴B, B也依賴A,從而構(gòu)成了循環(huán),從以上例子來講,如果bar里面也依賴了foo,那么就產(chǎn)生了循環(huán)依賴。
[圖片上傳失敗...(image-4e6c38-1653811523515)]
Spring是如何解決循環(huán)依賴的
getBean這個過程可以說是一個遞歸函數(shù),既然是遞歸函數(shù),那必然要有一個遞歸終止的條件,在getBean中,很顯然這個終止條件就是在填充屬性過程中有所返回。那如果是現(xiàn)有的流程出現(xiàn)Foo依賴Bar,Bar依賴Foo的情況會發(fā)生什么呢?
1、創(chuàng)建Foo對象
2、填充屬性時發(fā)現(xiàn)Foo對象依賴Bar
3、創(chuàng)建Bar對象
4、填充屬性時發(fā)現(xiàn)Bar對象依賴Foo
5、創(chuàng)建Foo對象
6、填充屬性時發(fā)現(xiàn)Foo對象依賴Bar....
[圖片上傳失敗...(image-ec8cc3-1653811523515)]
很顯然,此時遞歸成為了死循環(huán),該如何解決這樣的問題呢?
添加緩存
我們可以給該過程添加一層緩存,在實例化foo對象后將對象放入到緩存中,每次getBean時先從緩存中取,取不到再進行創(chuàng)建對象。
緩存是一個Map,key為beanName, value為Bean,添加緩存后的過程如下:
1、getBean('foo')
2、從緩存中獲取foo,未找到,創(chuàng)建foo
3、創(chuàng)建完畢,將foo放入緩存
4、填充屬性時發(fā)現(xiàn)Foo對象依賴Bar
5、getBean('bar')
6、從緩存中獲取bar,未找到,創(chuàng)建bar
7、創(chuàng)建完畢,將bar放入緩存
8、填充屬性時發(fā)現(xiàn)Bar對象依賴Foo
9、getBean('foo')
10、從緩存中獲取foo,獲取到foo, 返回
11、將foo設(shè)值到bar屬性中,返回bar對象
12、將bar設(shè)置到foo屬性中,返回
[圖片上傳失敗...(image-7c730f-1653811523515)]
以上流程在添加一層緩存之后我們發(fā)現(xiàn)確實可以解決循環(huán)依賴的問題。
多線程出現(xiàn)空指針
你可能注意到了, 當出現(xiàn)多線程情況時,這一設(shè)計就出現(xiàn)了問題。
我們假設(shè)有兩個線程正在getBean('foo')
1、線程一正在運行的代碼為填充屬性,也就是剛剛將foo放入緩存之后
2、線程二稍微慢一些,正在運行的代碼是:從緩存中獲取foo
此時,我們假設(shè)線程一掛起,線程二正在運行,那么它將執(zhí)行從緩存中獲取foo這一邏輯,這時你就會發(fā)現(xiàn),線程二得到了foo,因為線程一剛剛將foo放入了緩存,而且此時foo還沒有被填充屬性!
如果說線程二得到這個還沒有設(shè)值(bar)的foo對象去使用,并且剛好用了foo對象里面的bar屬性,那么就會得到空指針異常,這是不能為允許的!
[圖片上傳失敗...(image-e9a3ee-1653811523515)]
那么我們又當如何解決這個新的問題呢?
加鎖
解決多線程問題最簡單的方式便是加鎖。
我們可以在【從緩存獲取】前加鎖,在【填充屬性】后解鎖。
[圖片上傳失敗...(image-bb7abe-1653811523515)]
如此,線程二就必須等待線程一完成整個getBean流程之后才在緩存中獲取foo對象。
我們知道加鎖可以解決多線程的問題,但同樣也知道加鎖會引起性能問題。
試想,加鎖是為了保證緩存里的對象是一個完備的對象,但如果當緩存里的所有對象都是完備的了呢?或者說有部分對象已經(jīng)是完備了的呢?
假設(shè)我們有A、B、C三個對象
1、A對象已經(jīng)創(chuàng)建完畢,緩存中的A對象是完備的
2、B對象還在創(chuàng)建中,緩存中的B對象有些屬性還沒填充完畢
3、C對象還未創(chuàng)建
此時我們想要getBean('A'), 那我們應該期望什么?我們是否期望直接從緩存中獲取到A對象返回?或者還是等待獲取鎖之后才能得到A對象?
很顯然我們更加期望直接獲取到A對象返回就可以了,因為我們知道A對象是完備的,不需要去獲取鎖。
但以上的設(shè)計也很顯然無法達到該要求。
二級緩存
以上問題其實可以簡化成如何將完備對象和不完備的對象區(qū)分開來?因為只要我們知道這個是完備對象,那么直接返回,如果是不完備的對象,那么就需要獲取鎖。
我們可以這樣,再加一級緩存,第一級緩存存放完備對象,第二級緩存存放不完備的對象,由于此類對象是在Bean剛創(chuàng)建時放入緩存中的,所以我們這里把它稱作早期對象。
此時,當我們需要獲取A對象時,我們只需判斷第一級緩存有沒有A對象,如果有,說明A對象是完備的,可直接返回使用,如果沒有,說明A對象可能還沒創(chuàng)建或者是創(chuàng)建中,就繼續(xù)加鎖-->從二級緩存獲取對象-->創(chuàng)建對象的邏輯
此時流程如下:
1、getBean('foo')
2、從一級緩存中獲取foo,未獲取到
3、加鎖
4、從二級緩存中獲取foo,未獲取到
5、創(chuàng)建foo對象
6、將foo對象放入二級緩存
7、填充屬性
8、將foo對象放入一級緩存,此時foo對象已經(jīng)是個完備對象了
9、刪除二級緩存中的foo對象
10、解鎖返回
[圖片上傳失敗...(image-46de6a-1653811523515)]
基于現(xiàn)有流程,我們再來模擬一下循壞依賴時的情況
[圖片上傳失敗...(image-d1da8c-1653811523515)]
現(xiàn)在,既能解決對象的完備性問題,又能滿足我們的性能要求。perfect!
代理對象
要知道,Java里不僅有普通對象,還有代理對象,那么創(chuàng)建代理對象發(fā)生循環(huán)依賴時是否能夠滿足要求呢?
我們先來了解一下代理對象是什么時候創(chuàng)建的?
在Spring中,創(chuàng)建代理對象邏輯是在最后一步,也就是我們常常說的【初始化后】
[圖片上傳失敗...(image-fe4538-1653811523515)]
現(xiàn)在,我們嘗試把這部分邏輯加入到之前的流程中
[圖片上傳失敗...(image-a55029-1653811523515)]
顯而易見,最后的foo對象實際已經(jīng)是個代理對象了,但bar依賴的對象依舊是個普通的foo對象!
所以,當出現(xiàn)代理對象循環(huán)依賴時,之前的流程并不能滿足要求!
那么這個問題又應當如何解決呢?
思路
問題出現(xiàn)的原因就在于bar對象去獲取foo對象時,從二級緩存中得到的foo對象是個普通的對象。
那么有沒有辦法在這里添加一些判斷,比如說判斷foo對象是不是要進行代理,如果是的話就去創(chuàng)建foo的代理對象,然后將代理對象proxy_foo返回。
我們先假設(shè)這個方案是可行的,再來看有沒有其他的問題
[圖片上傳失敗...(image-df1fff-1653811523515)]
根據(jù)流程圖我們可以發(fā)現(xiàn)出一個問題:創(chuàng)建了兩次proxy_foo!
1、getBean('foo')流程中,填充屬性之后創(chuàng)建了一次proxy_foo
2、getBean('bar')的填充屬性時,從緩存中獲取foo時,也創(chuàng)建了一次proxy_foo
而這兩個proxy_foo是不相同的!雖然proxy_foo中引用的foo對象是相同的,但這也是不可接受的。
這個問題又當如何解決?
三級緩存
我們知道這兩次創(chuàng)建的proxy_foo是不相同的,那么程序應當如何知道呢?也就是說,我們?nèi)绻梢约右粋€標識,標識這個foo對象已經(jīng)被代理過了,讓程序直接使用這個代理的就可以了,不要再去創(chuàng)建代理了。是不是就解決這個問題了呢?
這個標識可不是什么flag=ture or false之類的,因為就算程序知道foo已經(jīng)被代理過了,那程序還是得把proxy_foo拿到才行,也就是說,我們還得找個地方把proxy_foo存起來。
這個時候我們就需要再加一級緩存。
邏輯如下:
1、當從緩存中獲取foo時,且foo被代理了之后,就將proxy_foo放入這一級緩存中。
2、在getBean('foo')流程中,創(chuàng)建代理對象時,先在緩存中查看是否有代理對象,如果有則使用該代理對象
[圖片上傳失敗...(image-3abe1e-1653811523515)]
這里你可能會有疑問:不是說先判斷三級緩存有沒有,沒有再去創(chuàng)建proxy_foo嘛?怎么不管有沒有都去創(chuàng)建?
是的,這里不管如何都去創(chuàng)建了proxy_foo,只是最后判斷三級緩存有沒有,有的話就使用三級緩存里的,之前創(chuàng)建的proxy_foo就不要了。
原因是這樣的,我們知道創(chuàng)建代理對象的邏輯是在Bean【初始化后】這一流程當中的某個后置處理器當中完成的,而后置處理器是可以由用戶自定義實現(xiàn)的,那么反過來說就表示Spring是無法控制這一部分邏輯的。
我們可以這樣假設(shè),我們自己也實現(xiàn)了一個后置處理器,這個處理器的作用不是創(chuàng)建代理對象proxy_foo,而是把foo替換成dog, 如果按之前的想法(只判斷是否為代理對象)你就會發(fā)現(xiàn)這樣的問題:getBean('foo')返回的是dog,但是bar對象依賴的是foo。
但是如果我們將【創(chuàng)建代理對象】這一邏輯看成只是眾多后置處理器中的一個實現(xiàn)。
1、在從緩存中取foo時,調(diào)用一系列的后置處理器,然后將后置處理器返回的最終結(jié)果放入三級緩存。
2、在getBean('foo')時,同樣調(diào)用一系列的后置處理器,然后從三級緩存獲取foo對應的對象,得到了就使用它,否則使用后置處理器返回結(jié)果。
你就會發(fā)現(xiàn),隨便你怎么折騰,getBean('foo')返回的對象與bar對象依賴的foo永遠是同一個對象。
[圖片上傳失敗...(image-2433f3-1653811523515)]
以上即為Spring對于循環(huán)依賴的解決方案
我對Spring這部分設(shè)計的思考
先總體回顧一下Spring的設(shè)計,Spring中采用了三級緩存
1、第一級緩存存放完備的bean對象
2、第二級緩存存放的是匿名函數(shù)
3、第三級緩存存放的是從第二級緩存中匿名函數(shù)返回的對象
是的,Spring將我們說的[從二級緩存中獲取foo, 調(diào)用后置處理器]這兩個步驟直接做成了一個匿名函數(shù)
它的結(jié)構(gòu)如下:
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
@FunctionalInterface
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}
函數(shù)內(nèi)容即為調(diào)用一系列后置處理器
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
對于這部分設(shè)計,一直存在著一些爭議:Spring中到底使用幾級緩存可以解決循環(huán)依賴?
觀點一
普通對象發(fā)生循環(huán)依賴時二級緩存即可以解決,但代理對象發(fā)生循環(huán)依賴時需要三級緩存才可以
這也算是一個普遍的觀點
這個觀點的角度是用二級緩存時,發(fā)生循環(huán)依賴會不會出bug,認為是普通對象不會,代理對象會。
換句話說:在發(fā)生多循環(huán)依賴時,多次從緩存中獲取對象,每次得到的對象是否相同?
舉例來說,A對象依賴B對象,B對象依賴A對象和C對象,C對象依賴A對象。
getBean('A')流程如下
[圖片上傳失敗...(image-b0b1a-1653811523515)]
在該流程中,A對象從緩存中獲取了兩次。
現(xiàn)在,我們結(jié)合從緩存中獲取對象的過程來思考一下。
當只有二級緩存時的邏輯:
1、調(diào)用二級緩存中的匿名函數(shù)獲取對象
2、返回對象
假設(shè)匿名函數(shù)中返回原對象,沒有創(chuàng)建代理邏輯——這里嚴格來說是沒有后置處理器的邏輯
那么每次【調(diào)用二級緩存中的匿名函數(shù)獲取對象】時返回的A對象都是同一個。
所以得出普通對象在只有二級緩存時沒有問題。
假設(shè)匿名函數(shù)中會觸發(fā)創(chuàng)建代理的邏輯,匿名函數(shù)返回的是代理對象。
那么每次【調(diào)用二級緩存中的匿名函數(shù)獲取對象】是都會創(chuàng)建代理對象。
每次創(chuàng)建的代理對象都是個新對象,故每次返回的A對象都不是同一個。
所以得出代理對象在只有二級緩存時會出現(xiàn)問題。
那么為什么三級緩存可以呢?
三級緩存時的邏輯:
1、先嘗試從三級緩存中獲取,未獲取到
2、調(diào)用二級緩存中的匿名函數(shù)獲取對象
3、將對象放入三級緩存
4、刪除二級緩存中的匿名函數(shù)
5、返回對象
所以在第一次從緩存獲取時會調(diào)用匿名函數(shù)創(chuàng)建代理對象,往后每次獲取時都是直接從第三級緩存取出返回。
綜上所述,該觀點是占得住腳的。
但我更希望這個觀點換個更嚴謹說法:當每次匿名函數(shù)返回的對象是一致時,二級緩存足以;當每次匿名函數(shù)返回的對象不一致時,需要有第三級緩存
觀點二
該觀點也是我自己的觀點:從設(shè)計的角度出發(fā),只有三級緩存才能保證框架的擴展性和健壯性。
當我們回顧觀點一的結(jié)論,你就會發(fā)現(xiàn)一個十分矛盾的地方:Spring如何才能得知匿名函數(shù)返回的對象是一致的?
匿名函數(shù)中的邏輯是調(diào)用一系列的后置處理器,而后置處理器是可自定義的。
意思就是匿名函數(shù)返回了什么,這件事本身就不受Spring所控制。
這時我們再借用三級緩存看這個問題,就會發(fā)現(xiàn):無論匿名函數(shù)返回的對象是否一致,三級緩存都能有效的解決循環(huán)依賴的問題。
從設(shè)計來看,三級緩存的設(shè)計是可以包含二級緩存所達到的需求的。
所以我們可以得出:使用三級緩存的設(shè)計將比二級緩存的設(shè)計有更好的擴展性和健壯性。
如果用觀點一的看法去設(shè)計Spring框架,那得加一大堆邏輯判斷,如果用觀點二,那只需加一層緩存。
小結(jié)
本篇文章的初衷是想寫我對Spring循環(huán)依賴的思考,但為了能夠說清楚這件事,還是詳細的描述了Spring解決循環(huán)依賴的設(shè)計。
以至于最后我想表達自己的思考時,只有寥寥幾句,因為大部分思考我已寫在了【Spring是如何解決循環(huán)依賴的】章節(jié)。
最后,希望大家有所收獲,如果有疑問可找我詢問,或者在評論區(qū)留下你的思考。
如果我的文章對你有所幫助,還請幫忙點贊、關(guān)注、轉(zhuǎn)發(fā)一下,你的支持就是我更新的動力,非常感謝!
個人博客空間:https://zijiancode.cn