《系列二》-- 4、循環依賴及其處理方式

閱讀之前要注意的東西:本文就是主打流水賬式的源碼閱讀,主導的是一個參考,主要內容需要看官自己去源碼中驗證。全系列文章基於 spring 源碼 5.x 版本。

寫在開始前的話:

閱讀spring 源碼實在是一件龐大的工作,不說全部內容,單就最基本核心部分包含的東西就需要很長時間去消化了:

  • beans
  • core
  • context

實際上我在博客裏貼出來的還只是一部分內容,更多的內容,我放在了個人,fork自 spring 官方源碼倉了; 而且對源碼的學習,必須是要跟着實際代碼層層遞進的,不然只是乾巴巴的文字味同嚼蠟。

https://gitee.com/bokerr/spring-framework-5.0.x-study

這個倉設置的公共倉,可以直接拉取。



Spring源碼閱讀系列--全局目錄.md



1 什麼是循環依賴

簡單來說就是依賴成環了, 看如下的僞代碼:

2 Spring 中的循環依賴類型

  • 構造函數依賴: Bean 依賴的其它bean 通過 "構造函數" 注入
  • Setter 循環依賴: Bean 依賴的其它bean 通過, "set函數" 注入

2.1 Setter 循環依賴

  • Worker.class --> Car.class
  • Car.class --> Factory.class
  • Factory.class --> Worker.class

“工人”需要駕駛“汽車”,“汽車”需要被"工廠"修理、生產,而"工廠" 需要依靠“工人” 運作;這裏是不是就是閉環了呢?


@Component
public class Worker {
    private Car car;
    @Autowired
    public void setCar(Car car) {
        this.car =car;
    }
    public void drive() {
        // 工人駕駛汽車
        car.run();
    }
    public void work() {
        // 工人根據自己的職業完成工作
    }
}

@Component
public class Car {
    private Factory factory;
    @Autowired
    public void setFactory(Factory factory) {
        this.factory =factory;
    }
    
    public void fix() {
        // 汽車去工廠修理自身
        this.factory.fix(car);
    }
    
    public void run() {
        // 汽車可以運行
    }
}

@Component
public class Factory {
    private Worker workers;
    @Autowired
    public void setWorker(Worker workers) {
        this.workers = workers;
    }
    
    public void build() {
        // 工人,爲工廠工作
        worker.work();
    }
    
    public void fix(Car car) {
        // 工廠修理汽車
    }
}

綜上所述,雖然邏輯沒有那麼嚴密,這裏的三個單例 bean 簡單的構成了循環依賴.

  • 當容器注入 Worker 時發現它依賴 Car;
  • 然後去加載Car 並實例化,接下來容器發現 Car 依賴 Factory;
  • 接着又去加載並實例化 Factory ,但是問題來了,容器發現 Factory 依賴 Worker, 這不首尾串聯了麼?

無限套娃了呀,有木有? 要是這麼一直套娃下去,解決就是內存溢出了。。。

按理說spring 啓動到了這裏,就該報錯了,但是並不會,Setter 循環依賴是明確可以解決的循環依賴。

2.2 構造函數循環依賴

所謂構造函數循環依賴,就是幾個 bean 之間成環狀依賴,且是通過構造函數注入的依賴。

不同於 setter 注入可以解決,構造函數注入的循環依賴是無法處理的,只能拋出:BeanCurrentlyInCreationException。

接下來給出演示的僞代碼:還是工人、汽車、工廠,但是我們把注入的方式改變一下


@Component
public class Worker {
    private Car car;
    @Autowired
    public Worker(Car car) {
        this.car =car;
    }
    public void drive() {
        // 工人駕駛汽車
        car.run();
    }
    public void work() {
        // 工人根據自己的職業完成工作
    }
}

@Component
public class Car {
    private Factory factory;
    @Autowired
    public Car(Factory factory) {
        this.factory =factory;
    }
    
    public void fix() {
        // 汽車去工廠修理自身
        this.factory.fix(car);
    }
    
    public void run() {
        // 汽車可以運行
    }
}

@Component
public class Factory {
    private Worker workers;
    @Autowired
    public Factory(Worker workers) {
        this.workers = workers;
    }
    
    public void build() {
        // 工人,爲工廠工作
        worker.work();
    }
    
    public void fix(Car car) {
        // 工廠修理汽車
    }
}

要說區別吧,跟上邊的 setter 循環依賴的去唄還真不大,就是把 setter 函數改成了構造函數而已。

2.3 總結

這裏主要的區別是,構造函數注入依賴時,必須要保證 【所依賴的bean 對象】 已經正確加載;因爲他們仨的構造函數,成環依賴註定無法創建成功;

這不就是蛋生只因,只因生蛋的問題了?

而 setter 注入的方式中,可以先使用 "無參構造函數" new 出來相關的幾個對象;當三個對象都創建之後,在後期按照依賴順序設置對象地址引用即可。

實際上 spring 中也只支持單例的 Setter 循環依賴的消解,試想一下:

  • 若上述案例的 Worker Car Factory 三者全是【原型模式】作用域的bean, 我們爲無參構造函數創建出來的bean 注入循環依賴時,
    必定會再次陷入,只因生蛋,蛋生只因的死循環中。因爲 【原型模式】 作用域bean被當作依賴時,必須創建一個新的 bean,
    這樣勢必導致無限創建依賴環中的bean,內存會被快速消耗殆盡。

這裏留下一個思考問題, "spring只能消解單例的 Setter 注入的循環依賴",這個說法來源說得不甚明瞭。

  • 理論上來說,不論是3個bean,抑或是更多的 bean 成環狀依賴,只要這個環狀依賴中,
    存在至少一個單例bean 時,那麼這個無限循環就可以被這個單例bean 通過無參構造函數創建的提前暴露的bean 所消解。

3 Spring 對bean 及其依賴bean 的加載順序

以Worker、Car、Factory 爲例,當程序啓動可能會以如下順序進行bean 的加載:

  1. Spring 容器加載 Worker_Bean, 發現它依賴於 Car_Bean, 於是在 "當前正在創建bean池" 中記錄,Worker_Bean 轉而去加載 Car_bean

  2. 同理加載 Car_bean 時發現它依賴於 Factory_Bean, 重複上述操作:在 "當前正在創建bean池" 記錄 Car_bean, 轉而加載 Factory_bean

  3. 然後 Spring 容器加載 Factory_bean,並向 "當前正在創建bean池" 記錄Factory_bean;
    接着spring容器解析 Factory_bean 的依賴時,發現它依賴於:Worker_bean;
    此時 Worker_bean 已經存在於 "當前正在創建bean池" 中了;
    一般情況下這時候應該拋出 "循環依賴" 異常了。

不過這並不是沒有轉機的,前邊提到過,spring 可以消解單例 bean 的 Setter 循環依賴,接下來的第四節將詳細介紹具體的衝突消解原理。

4 Spring 對 Setter 依賴的消解

4.1 ObjectFactory 接口介紹

在講 spring 消解單例 Setter 循環依賴之前,我們引入一個接口

package org.springframework.beans.factory;
import org.springframework.beans.BeansException;
/**
 * <p>This interface is similar to {@link FactoryBean}, but implementations
 * of the latter are normally meant to be defined as SPI instances in a
 * {@link BeanFactory}, while implementations of this class are normally meant
 * to be fed as an API to other beans (through injection). As such, the
 * {@code getObject()} method has different exception handling behavior.
 */
@FunctionalInterface
public interface ObjectFactory<T> {
	/**
	 * Return an instance (possibly shared or independent)
	 * of the object managed by this factory.
	 */
	T getObject() throws BeansException;
}

看類註釋,講得很清楚了:

  • ObjectFactory接口,相當類似前邊提到過的, FactoryBean 接口。但是區別是 ObjectFactory 中管理的bean 更像是一箇中間結果,
    它一般會被當作 "API" 提供給別的bean. 【英文比較好的夥計可以看上邊的-類註釋】

實際上在 spring 官方給出的解釋中: ObjectFactory 用於, 提前暴露一個創建中的 bean。

重點關注關鍵詞: "提前暴露"

4.2 循環依賴的消解

我們結合最新引入的 ObjectFactory,重新梳理下 Spring 加載bean 的過程。

還是以 Worker、Car、Factory 爲例,當程序啓動可能會以如下順序進行bean 的加載:

  1. Spring 容器加載 Worker_Bean, 首先利用Worker類的無參構造函數創建bean,使用 ObjectFactory 管理它,並提前暴露該創建中的bean;
    然後spring 解析依賴時,發現Worker_bean 依賴於 Car_Bean, 於是在 "當前正在創建bean池" 中記錄Worker_Bean, 轉而去加載 Car_bean。

  2. 同理加載 Car_bean,先根據無參構造函數創建Car類的 bean,然後用ObjectFactory 提前暴露該創建中的bean;
    然後spring 解析依賴時,發現Car_bean 依賴於 Factory_bean, 同樣的在 "當前正在創建bean池" 記錄 Car_bean, 轉而加載 Factory_bean。

  3. 然後 Spring 容器加載Car_bean 依賴的 Factory_bean ,重複上述流程:

    • 無參構造函數創建bean
    • ObjectFactory 管理提前暴露的 Factory_bean
    • "當前正在創建bean池" 中記錄 Factory_bean
  4. 接下來 spring 解析發現, Factory_bean 依賴通過 setter 注入的 Worker_bean;
    這時候由於,Worker_bean 已經存在於 "當前正在創建bean池" 中了,那麼就可以去獲取 ObjectFactory 管理的,提前暴露的 Worker_bean了。

  5. 最後同理:提前暴露的 Worker_bean 被加載後,繼續去加載關聯的提前暴露bean: Car_bean、 Factory_bean ...
    直至最終加載完,循環依賴中的所有bean。

5 實驗

爲了證明 2.3 小節遺留的問題,引入本節的實驗:

可以通過調整如下的變量進行對比,從而觀察,spring對 循環依賴的消解。

Bean 的注入方式:

  • Setter 注入
  • 構造函數注入
  • @LookUp 注入

Bean 作用域:

  • 單例作用域(單例)
  • 原型作用域(“多例”)

5.1 實驗場景 && 結論

結論:

當多個bean 成環狀依賴時:只需要保證這個環中有一個bean 是單列, 且它所需要依賴的其它 bean 都是被延遲注入的 (Setter注入、@LookUp注入等方式),那麼該循環依賴可以被消解。

關於這個依賴環中的其它bean:
可以是任意方式注入

  • 構造函數注入 [依賴的Bean初始化時,必須通過構造函數傳入]
  • Setter注入[可延遲注入,類似 @LookUp]

可以是任意作用域

  • singleton
  • prototype

5.3 測試代碼

參考下邊的實際測試代碼

有5個產品製造相關的 bean(Service):

- 【prototype】 Manager: 管理員,爲工廠工作,負責 NiceCar
- 【prototype】 Worker: 普通工作人員,爲工廠工作,負責 Car

- 【prototype】 Car: 普通汽車
- 【多例】 NiceCar: 精緻汽車

- 【singleton】 CarFactory: 工廠,負責所有的汽車生產

1個對外接口bean(Controller),它負責對外暴露工廠的訪問入口:訪問工廠獲取 (Car / NiceCar), 它不在循環依賴環中,故此不討論它的作用域:

- ExecuteController: 測試web程序入口,我們可以通過它得到 Car 和 NiceCar 產品

有如下依賴關係的類圖

graph LR D(ExecuteManager) --> B(CarFactory) --> C(Car) C(Car) --> A(Worker) A(Worker) --> B(CarFactory)
graph LR D(ExecuteManager) --> B(CarFactory) --> C(NiceCar) C(NiceCar) --> A(Manager) A(Manager) --> B(CarFactory)

參考下圖:

img.png

圖中 CarFactory 把 Setter 注入的代碼刪除了; 其實是可以保留的,Setter 和 @LookUp 的工作互不干擾

  • Setter 保證 CarFactory_Bean 創建並初始化之後:car 和 niceCar 有初始值 【Setter 注入後不再被更新】

  • 而 LookUp 保證 produceCar() 和 produceNiceCar() 每次從 Java 堆得到一個全新的 Car_Bean 或者 NiceCar_Bean

圖中綠色線條標記的是,"多例" bean, 且都是以最致命的 "構造函數注入"。

實際上,多例 bean想要生效,需要使用 @LookUp 註解實時讀取,但是這裏爲了便於演示功能的複雜性,特意使用了最致命的場景。

所謂致命,就是程序無法啓動,或者 bean 訪問時報錯。

演示代碼地址:

https://gitee.com/bokerr/spring-cycle-example.git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章