關(guān)于我對Spring循環(huán)依賴的思考

前言

在今天,依然有許多人對循環(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容