Spring6 當中的 Bean 迴圈依賴的詳細處理方案+原始碼解析

Rainbow-Sea發表於2024-05-01

1. Spring6 當中的 Bean 迴圈依賴的詳細處理方案+原始碼解析

@

目錄
  • 1. Spring6 當中的 Bean 迴圈依賴的詳細處理方案+原始碼解析
  • 每博一文案
    • 1.1 Bean的迴圈依賴
    • 1.2 singletion 下的 set 注入下的 Bean 的迴圈依賴
    • 1.3 prototype下的 set 注入下的 Bean 的迴圈依賴
    • 1.4 singleton下的構造注入產生的迴圈依賴
    • 1.5 Spring 解決迴圈依賴的原理(原始碼解析)
  • 2. 總結:
  • 3. 最後:


每博一文案

聽完這段話就勇敢起來吧,在任何猶豫的時刻,一旦抱有人生就這麼短短几十年,我不去做一定會後悔這樣的想法,就會憑空多出幾分勇氣,比如:嘗試新的穿衣風格,向喜歡的人表白,去特別貴的餐廳大吃一頓,對看不慣的人和事說不,不樂觀的想,我們其實都是沒有來路和歸途的,能擁有的就是現在,所以想做什麼就去做吧,衝動一點也沒關係,吃點虧也沒關係.

1.1 Bean的迴圈依賴

A物件中有B屬性。B物件中有A屬性。這就是迴圈依賴。我依賴你,你也依賴我。
比如:丈夫類Husband,妻子類Wife。Husband中有Wife的引用。Wife中有Husband的引用。

在這裡插入圖片描述
在這裡插入圖片描述

在這裡插入圖片描述
在這裡插入圖片描述

package com.rainbowsea.bean;

public class Wife {
    private String name;
    private Husband husband;

    public Wife() {
    }

    public Wife(String name, Husband husband) {
        this.name = name;
        this.husband = husband;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Husband getHusband() {
        return husband;
    }

    public void setHusband(Husband husband) {
        this.husband = husband;
    }

    // toString()方法重寫時需要注意:不能直接輸出husband,輸出husband.getName()。要不然會出現遞迴導致的棧記憶體溢位錯誤。
    @Override
    public String toString() {
        return "Wife{" +
                "name='" + name + '\'' +
                ", husband=" + this.husband.getName() +
                '}';
    }
}

Husband

package com.rainbowsea.bean;

public class Husband {
    private String name;
    private Wife wife;


    public Husband() {
    }

    public Husband(String name, Wife wife) {
        this.name = name;
        this.wife = wife;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Wife getWife() {
        return wife;
    }

    public void setWife(Wife wife) {
        this.wife = wife;
    }


    // toString()方法重寫時需要注意:不能直接輸出wife,輸出wife.getName()。要不然會出現遞迴導致的棧記憶體溢位錯誤
    @Override
    public String toString() {
        return "Husband{" +
                "name='" + name + '\'' +
                ", wife=" + this.wife.getName() +
                '}';
    }
}

注意點: toString()方法重寫時需要注意:不能直接輸出wife,輸出wife.getName()。要不然會出現遞迴導致的棧記憶體溢位錯誤。

1.2 singletion 下的 set 注入下的 Bean 的迴圈依賴

我們來編寫程式,測試一下在singleton+setter的模式下產生的迴圈依賴,Spring是否能夠解決?

準備工作:配置匯入 相關的 spring 框架,讓 Maven 幫我們匯入 spring的相關jar包。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>spring6-007-circular-dependency-blog</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.11</version>
        </dependency>


        <!-- junit4 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
</project>

配置相關的 spring.xml 檔案資訊。

在這裡插入圖片描述

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--     Husband 的配置-->
    <bean id="husbandBean" class="com.rainbowsea.bean.Husband" scope="singleton">
        <property name="name" value="小明" ></property>
        <property name="wife" ref="wifeBean"></property> <!--set 注入-->
    </bean>
    <!--    Wife 的配置-->
    <bean id="wifeBean" class="com.rainbowsea.bean.Wife" scope="singleton">
        <property name="name" value="小花"></property>
        <property name="husband" ref="husbandBean"></property>
    </bean>
</beans>

執行測試:
在這裡插入圖片描述
透過測試得知:在singleton + set注入的情況下,迴圈依賴是沒有問題的。Spring可以解決這個問題。

package com.rainbowsea.test;

import com.rainbowsea.bean.Husband;
import com.rainbowsea.bean.Wife;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class CircularDependencyTest {
    @Test
    public void testCircularDependency() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
        System.out.println(husbandBean);


        Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
        System.out.println(wifeBean);
    }


}

解決分析:

singleton + setter模式下可以解決的迴圈依賴問題

在singleton + setter 模式下,為什麼迴圈依賴不會出現問題,Spring是如何應對的?

主要原因是:在這個 singleton 單例模式下,在Spring 容器中的 bean 物件是獨一無二的物件,是唯一的一個。同志在該 singleton 單例模式下:Spring 對 Bean 的管理主要分為清晰的兩個階段

  1. 第一個階段:在Spring 容器載入的時候,例項Bean ,只要其中任意一個 Bean 例項化之後,馬上進行一個“曝光” (注意:曝光不等於屬性賦值,曝光了,但是屬性並沒有附上值的)
  2. 第二個階段:Bean “曝光”之後,再進行屬性的賦值操作(呼叫 set()方法實現對屬性的賦值操作)

核心解決方案是:例項化物件和物件的屬性賦值分為兩個階段來完成,並不是一次性完成的。

簡單來說:就是:singleton 優先被“曝光”,例項化和賦值是分開的,會優先把例項化的物件的地址曝光出來,因為在 singleton 單例模式下,bean 是唯一的一個,獨一無二的,並且早晚都要進行賦值操作。提前曝光,後面再進行賦值也是無妨的。因為你弄來弄去,就是那唯一的一個 bean。不存在多個,不知道是哪一個的問題

在這裡插入圖片描述

1.3 prototype下的 set 注入下的 Bean 的迴圈依賴

我們再來測試一下:prototype+set注入的方式下,迴圈依賴會不會出現問題?

我們只需將 spring.xml 配置檔案資訊,修改為 protoype (多例)即可。

在這裡插入圖片描述
執行測試看看。

在這裡插入圖片描述
報錯,報錯資訊如下:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husbandBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wifeBean' while setting bean property 'wife'

建立名為“husbandBean”的bean時出錯:請求的bean當前正在建立中:是否存在無法解析的迴圈引用?透過測試得知,當迴圈依賴的所有Bean的scope="prototype"的時候,產生的迴圈依賴,Spring是無法解決的,會出現BeanCurrentlyInCreationException異常。

prototype下的 set 注入下的 Bean 的迴圈依賴;並不能解決迴圈依賴,原因是:prototype 是多例的存在,多個 Bean 物件,不是唯一的一個Bean,無法確定是具體是哪個,Bean無法提前曝光。

BeanCreationException 報錯:當前的Bean正在處於建立中異常

特別的:當兩個bean的scope都是prototype的時候,才會出現異常,如果其中任意一個是singleton的,就不會出現異常了。是其中的任意一個 就行,就不會出現異常了。如果是三個 bean 的話,那就需要其中的任意兩個 是為singleton才行。

原因是:singleton 優先被“曝光”,例項化和賦值是分開的,會優先把例項化的物件的地址曝光出來,因為在 singleton 單例模式下,bean 是唯一的一個,獨一無二的,並且早晚都要進行賦值操作。提前曝光,後面再進行賦值也是無妨的。因為你弄來弄去,就是那唯一的一個 bean。不存在多個,不知道是哪一個的問題。

測試:當兩個bean的scope都是prototype的時候,才會出現異常,如果其中任意一個是singleton的,就不會出現異常了。

Husband 為 prototype ,Wife 為 singleten

在這裡插入圖片描述
反一下:Husband 為 singleten ,Wife 為 prototype

在這裡插入圖片描述

至於,三個 Bean ,需要任意兩個為 singleten ,才不會報異常,就大家自行測試了。理論上就是:n 個 就需要 N-1個為 singleten 。

1.4 singleton下的構造注入產生的迴圈依賴

如果是基於構造注入(進行賦值),很明顯,要呼叫構造方法進行賦值就一定要完完整整的進行一次性賦值+例項化,沒有分段的,所以會產生迴圈依賴並且無法解決的,
所以編寫程式碼時一定要注意。同樣是報: BeanCreationException 報錯:當前的Bean正在處於建立中異常

我們來測試一下。

在這裡插入圖片描述

1.5 Spring 解決迴圈依賴的原理(原始碼解析)

Spring 為什麼可以解決 set+sigleton 模式下迴圈依賴呢?

根本原因在於:這種方式可以做到將 “例項化 Bean” 和“給 Bean 屬性賦值” 這兩個動作分開去完成。例項化Bean的時候:呼叫無引數構造方法來完成此時可以先不給屬性賦值(因為在 singleton 單例模式下,bean 是唯一的一個,獨一無二的,並且早晚都要進行賦值操作。提前曝光,後面再進行賦值也是無妨的。因為你弄來弄去,就是那唯一的一個 bean),可以提前將Bean 物件“曝光”給外界

給Bean 屬性賦值的時候:呼叫 setter()方法來完成(set注入完成,呼叫其中 bean物件當中的 set()方法,所以千萬要記得寫 set()方法)。

兩個步驟是完全可以分離去完成的,並且這兩步不要求在同一個時間點上完成。

也就是說,Bean 都是單例的,我們可以先把所有的單例 Bean 例項化出來,放到一個集合當中(我們可以將其稱之為快取),所有的單例 Bean 全部例項化完成之後,以後我們再慢慢的呼叫 setter()方法給屬性賦值,這樣就解決了迴圈依賴的問題。

那麼在 Spring 框架底層原始碼級別上是如何實現的呢?如下:

我們先來分析一下:AbstractAutowireCapableBeanFactory類下的doCreateBean() 方法

在這裡插入圖片描述
doCreateBean() 方法 下呼叫的:addSingletonFactory() 方法,這裡原始碼上使用了正規表示式,關於Lambda 表示式,由於設定的內容較多,想要了解更多的,大家可以移步至✏️✏️✏️ 函數語言程式設計:Lambda 表示式_(ws, bs)>-CSDN部落格

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

在這裡插入圖片描述

下面這個DefaultSingletonBeanRegistry類,才是我們真正要探究的原始碼內容

在這裡插入圖片描述
在這個DefaultSingletonBeanRegistry 類當中中包含三個重要的屬性同時也是三個Map集合:

在這裡插入圖片描述

	/** Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

這三個快取其實本質上是三個Map集合。

  • Cache of singleton objects: bean name to bean instance. 單例物件的快取:key儲存bean名稱,value儲存Bean物件【一級快取】
  • Cache of early singleton objects: bean name to bean instance. 早期單例物件的快取:key儲存bean名稱,value儲存早期的Bean物件【二級快取】
  • Cache of singleton factories: bean name to ObjectFactory.單例工廠快取:key儲存bean名稱,value儲存該Bean對應的ObjectFactory物件【三級快取】
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); 一級快取
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); 二級快取
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); 三級快取
這個三個快取都是Map集合
Map集合的key 儲存的都是bean的name(bean id)
> 一級快取儲存的是:單例Bean物件,完整的單例Bean物件,也就是這個快取中的Bean物件的屬性都已經賦值了,是一個完整的Bean物件
> 二級快取儲存的是: 早期的案例Bean物件,這個快取中的單例Bean物件的屬性滅有賦值,只是一個早期的例項物件
> 三級快取儲存的是: 單例工廠物件,這個裡面儲存了大力的“工廠物件”,每一個單例Bean物件都會對應一個單例工廠物件。
> 這個集合中儲存的是,建立該單例物件時對應的那個單例工廠物件。    

我們再來看,在該類中有這樣一個方法 addSingletonFactory(),這個方法的作用是:將建立Bean物件的ObjectFactory物件提前曝光。這裡我們Debug 除錯看看。

在這裡插入圖片描述

在這裡插入圖片描述
再分析對應下面的原始碼:

在這裡插入圖片描述

在這裡插入圖片描述
從原始碼中可以看到:spring 會先從一級快取中獲取Bean 物件,如果獲取不到,則再從二級快取當中獲取 Bean 物件,如果二級快取還是獲取不到,則最後從三級快取當中獲取之前曝光的ObjectFactory 物件,透過ObjectFactory 物件獲取到對應 Bean 例項,這樣就解決了迴圈依賴的問題。

總結:

Spring只能解決setter方法注入的單例bean之間的迴圈依賴。ClassA依賴ClassB,ClassB又依賴ClassA,形成依賴閉環。Spring在建立ClassA物件後,不需要等給屬性賦值,直接將其曝光到bean快取當中。在解析ClassA的屬性時,又發現依賴於ClassB,再次去獲取ClassB,當解析ClassB的屬性時,又發現需要ClassA的屬性,但此時的ClassA已經被提前曝光加入了正在建立的bean的快取中,則無需建立新的的ClassA的例項,直接從快取中獲取即可。從而解決迴圈依賴問題。

2. 總結:

  1. Bean的迴圈依賴:A物件中有B屬性。B物件中有A屬性。這就是迴圈依賴。我依賴你,你也依賴我。

  2. singletion 下的 set 注入下的 Bean 的迴圈依賴能夠被解決。主要原因是:在這個 singleton 單例模式下,在Spring 容器中的 bean 物件是獨一無二的物件,是唯一的一個。同志在該 singleton 單例模式下:Spring 對 Bean 的管理主要分為清晰的兩個階段

    1. 第一個階段:在Spring 容器載入的時候,例項Bean ,只要其中任意一個 Bean 例項化之後,馬上進行一個“曝光” (注意:曝光不等於屬性賦值,曝光了,但是屬性並沒有附上值的)
    2. 第二個階段:Bean “曝光”之後,再進行屬性的賦值操作(呼叫 set()方法實現對屬性的賦值操作)

    核心解決方案是:例項化物件和物件的屬性賦值分為兩個階段來完成,並不是一次性完成的。

  3. prototype下的 set 注入下的 Bean 的迴圈依賴;並不能解決迴圈依賴,原因是:prototype 是多例的存在,多個 Bean 物件,不是唯一的一個Bean,無法確定是具體是哪個,Bean無法提前曝光。

  4. 特別的:當兩個bean的scope都是prototype的時候,才會出現異常,如果其中任意一個是singleton的,就不會出現異常了。是其中的任意一個 就行,就不會出現異常了。如果是三個 bean 的話,那就需要其中的任意兩個 是為singleton才行。

    1. 至於,三個 Bean ,需要任意兩個為 singleten ,才不會報異常,就大家自行測試了。理論上就是:n 個 就需要 N-1個為 singleten 。
    2. 注意報錯資訊:org.springframework.beans.factory.BeanCreationException: 當前的Bean正在處於建立中異常
  5. singleton下的構造注入產生的迴圈依賴;是基於構造注入(進行賦值),很明顯,要呼叫構造方法進行賦值就一定要完完整整的進行一次性賦值+例項化,沒有分段的,所以會產生迴圈依賴並且無法解決的,

  6. Spring 解決迴圈依賴的原理(原始碼解析):一級快取,二級快取,三級快取的存在。提前“曝光”機制

3. 最後:

“在這個最後的篇章中,我要表達我對每一位讀者的感激之情。你們的關注和回覆是我創作的動力源泉,我從你們身上吸取了無盡的靈感與勇氣。我會將你們的鼓勵留在心底,繼續在其他的領域奮鬥。感謝你們,我們總會在某個時刻再次相遇。”

在這裡插入圖片描述

相關文章