spring: 我是如何解決迴圈依賴的?

蘇三說技術發表於2020-12-31

1.由同事拋的一個問題開始

最近專案組的一個同事遇到了一個問題,問我的意見,一下子引起的我的興趣,因為這個問題我也是第一次遇到。平時自認為對spring迴圈依賴問題還是比較瞭解的,直到遇到這個和後面的幾個問題後,重新重新整理了我的認識。

我們先看看當時出問題的程式碼片段:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

這兩段程式碼中定義了兩個Service類:TestService1和TestService2,在TestService1中注入了TestService2的例項,同時在TestService2中注入了TestService1的例項,這裡構成了迴圈依賴。

只不過,這不是普通的迴圈依賴,因為TestService1的test1方法上加了一個@Async註解。

大家猜猜程式啟動後執行結果會怎樣?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

報錯了。。。原因是出現了迴圈依賴。

「不科學呀,spring不是號稱能解決迴圈依賴問題嗎,怎麼還會出現?」

如果把上面的程式碼稍微調整一下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}

把TestService1的test1方法上的@Async註解去掉,TestService1和TestService2都需要注入對方的例項,同樣構成了迴圈依賴。

但是重新啟動專案,發現它能夠正常執行。這又是為什麼?

帶著這兩個問題,讓我們一起開始spring迴圈依賴的探祕之旅。

2.什麼是迴圈依賴?

迴圈依賴:說白是一個或多個物件例項之間存在直接或間接的依賴關係,這種依賴關係構成了構成一個環形呼叫。

第一種情況:自己依賴自己的直接依賴

第二種情況:兩個物件之間的直接依賴

第三種情況:多個物件之間的間接依賴

前面兩種情況的直接迴圈依賴比較直觀,非常好識別,但是第三種間接迴圈依賴的情況有時候因為業務程式碼呼叫層級很深,不容易識別出來。

3.迴圈依賴的N種場景

spring中出現迴圈依賴主要有以下場景:

單例的setter注入

這種注入方式應該是spring用的最多的,程式碼如下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

這是一個經典的迴圈依賴,但是它能正常執行,得益於spring的內部機制,讓我們根本無法感知它有問題,因為spring默默幫我們解決了。

spring內部有三級快取:

  • singletonObjects 一級快取,用於儲存例項化、注入、初始化完成的bean例項

  • earlySingletonObjects 二級快取,用於儲存例項化完成的bean例項

  • singletonFactories 三級快取,用於儲存bean建立工廠,以便於後面擴充套件有機會建立代理物件。

下面用一張圖告訴你,spring是如何解決迴圈依賴的:

                                            圖1

細心的朋友可能會發現在這種場景中第二級快取作用不大。

那麼問題來了,為什麼要用第二級快取呢?

試想一下,如果出現以下這種情況,我們要如何處理?

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}
@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;

    public void test3() {
    }
}

TestService1依賴於TestService2和TestService3,而TestService2依賴於TestService1,同時TestService3也依賴於TestService1。

按照上圖的流程可以把TestService1注入到TestService2,並且TestService1的例項是從第三級快取中獲取的。

假設不用第二級快取,TestService1注入到TestService3的流程如圖:

                                               圖2

TestService1注入到TestService3又需要從第三級快取中獲取例項,而第三級快取裡儲存的並非真正的例項物件,而是ObjectFactory物件。說白了,兩次從三級快取中獲取都是ObjectFactory物件,而通過它建立的例項物件每次可能都不一樣的。

這樣不是有問題?

為了解決這個問題,spring引入的第二級快取。上面圖1其實TestService1物件的例項已經被新增到第二級快取中了,而在TestService1注入到TestService3時,只用從第二級快取中獲取該物件即可。

                                                 圖3

還有個問題,第三級快取中為什麼要新增ObjectFactory物件,直接儲存例項物件不行嗎?

答:不行,因為假如你想對新增到三級快取中的例項物件進行增強,直接用例項物件是行不通的。

針對這種場景spring是怎麼做的呢?

答案就在AbstractAutowireCapableBeanFactory類doCreateBean方法的這段程式碼中:

它定義了一個匿名內部類,通過getEarlyBeanReference方法獲取代理物件,其實底層是通過AbstractAutoProxyCreator類的getEarlyBeanReference生成代理物件。

多例的setter注入

這種注入方法偶然會有,特別是在多執行緒的場景下,具體程式碼如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

很多人說這種情況spring容器啟動會報錯,其實是不對的,我非常負責任的告訴你程式能夠正常啟動。

為什麼呢?

其實在AbstractApplicationContext類的refresh方法中告訴了我們答案,它會呼叫finishBeanFactoryInitialization方法,該方法的作用是為了spring容器啟動的時候提前初始化一些bean。該方法的內部又呼叫了preInstantiateSingletons方法

標紅的地方明顯能夠看出:非抽象、單例 並且非懶載入的類才能被提前初始bean。

而多例即SCOPE_PROTOTYPE型別的類,非單例,不會被提前初始化bean,所以程式能夠正常啟動。

如何讓他提前初始化bean呢?

只需要再定義一個單例的類,在它裡面注入TestService1

@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;
}

重新啟動程式,執行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出現了迴圈依賴。

注意:這種迴圈依賴問題是無法解決的,因為它沒有用快取,每次都會生成一個新物件。

構造器注入

這種注入方式是spring4.x以上的版本中官方推薦的方式,具體如下程式碼:

@Service
publicclass TestService1 {

    public TestService1(TestService2 testService2) {
    }
}
@Service
publicclass TestService2 {

    public TestService2(TestService1 testService1) {
    }
}

執行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出現了迴圈依賴,為什麼呢?

從圖中的流程看出構造器注入只是新增了三級快取,並沒有使用快取,所以也無法解決迴圈依賴問題。

單例的代理物件setter注入

這種注入方式其實也比較常用,比如平時使用:@Async註解的場景,會通過AOP自動生成代理物件。

我那位同事的問題也是這種情況。

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

從前面得知程式啟動會報錯,出現了迴圈依賴:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

為什麼會迴圈依賴呢?

答案就在下面這張圖中:

說白了,bean初始化完成之後,後面還有一步去檢查:第二級快取 和 原始物件 是否相等。由於它對前面流程來說無關緊要,所以前面的流程圖中省略了,但是在這裡是關鍵點,我們重點說說:

那位同事的問題正好是走到這段程式碼,發現第二級快取 和 原始物件不相等,所以丟擲了迴圈依賴的異常。

如果這時候把TestService1改個名字,改成:TestService6,其他的都不變。

@Service
publicclass TestService6 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}

再重新啟動一下程式,神奇般的好了。

what? 這又是為什麼?

這就要從spring的bean載入順序說起了,預設情況下,spring是按照檔案完整路徑遞迴查詢的,按路徑+檔名排序,排在前面的先載入。所以TestService1比TestService2先載入,而改了檔名稱之後,TestService2比TestService6先載入。

為什麼TestService2比TestService6先載入就沒問題呢?

答案在下面這張圖中:

這種情況testService6中其實第二級快取是空的,不需要跟原始物件判斷,所以不會丟擲迴圈依賴。

DependsOn迴圈依賴

還有一種有些特殊的場景,比如我們需要在例項化Bean A之前,先例項化Bean B,這個時候就可以使用@DependsOn註解。

@DependsOn(value = "testService2")
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@DependsOn(value = "testService1")
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

程式啟動之後,執行結果:

Circular depends-on relationship between 'testService2' and 'testService1'

這個例子中本來如果TestService1和TestService2都沒有加@DependsOn註解是沒問題的,反而加了這個註解會出現迴圈依賴問題。

這又是為什麼?

答案在AbstractBeanFactory類的doGetBean方法的這段程式碼中:

它會檢查dependsOn的例項有沒有迴圈依賴,如果有迴圈依賴則拋異常。

4.出現迴圈依賴如何解決?

專案中如果出現迴圈依賴問題,說明是spring預設無法解決的迴圈依賴,要看專案的列印日誌,屬於哪種迴圈依賴。目前包含下面幾種情況:

生成代理物件產生的迴圈依賴

這類迴圈依賴問題解決方法很多,主要有:

  • 使用@Lazy註解,延遲載入
  • 使用@DependsOn註解,指定載入先後關係
  • 修改檔名稱,改變迴圈依賴類的載入順序

使用@DependsOn產生的迴圈依賴

這類迴圈依賴問題要找到@DependsOn註解迴圈依賴的地方,迫使它不迴圈依賴就可以解決問題。

多例迴圈依賴

這類迴圈依賴問題可以通過把bean改成單例的解決。

構造器迴圈依賴

這類迴圈依賴問題可以通過使用@Lazy註解解決。

當然最好的解決迴圈依賴問題最佳方案是從程式碼設計上規避,但是複雜的系統中有可能沒法避免。

最後說一句(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。

在公眾號中回覆:面試、程式碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加群,可以跟很多BAT大廠的前輩交流和學習。

相關文章