展開說說,Spring Bean IOC、AOP 循環依賴

做者:小傅哥
博客:https://bugstack.cnhtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

延遲知足能給你帶來什麼?java

大學有四年時間,但幾乎全部人都是臨近畢業才發現找一份好工做費勁,尤爲是我能很是熟悉的軟件開發行業,即便是畢業了還須要額外花錢到培訓機構,在學一遍編程技術才能出去找工做。好像在校這幾年壓根就沒學到什麼!git

就我我的而言多是由於上學期間喜歡編程,也從師哥、師姐那裏聽到一些關於畢業後找工做的不容易,也瞭解了一些社會上對程序員開發技能的要求級別。也就是獲得了這些消息,又加上本身樂於折騰,我給本身定了一個天天都能完成的小目標:程序員

紅塵世界幾個王,我自不服迎頭上。
日敲代碼兩百行,衝進世界五百強。

哈哈哈,就這麼天天兩百行代碼,一個月就是6千行,一年就是6萬行,三年後開始實習就有18萬行,一個應屆實習生有將近20萬行代碼的敲擊量,幾乎已經能夠很是熟練的完成各種簡單的工做,在加上實習中對整個項目流程真正的斷鏈後,找一個正經的開發工做,仍是很容易的。github

而這時候找工做的容易,就來自於你一直以來的學習和沉澱,但若是你沒通過這些努力,可能等畢業後就會變得很是慌亂,最後沒辦法只能去一些機構再學習一遍。面試

2、面試題

謝飛機,小記!,之前感受Spring沒啥,看過一篇getBean,個人天!spring

謝飛機:面試官,最近我看了 Spring 的 getBean 發現這裏好多東西,還有一個是要解決循環依賴的,這玩意麪試有啥要問的嗎?編程

面試官:有哇,Spring 是如何解決循環依賴的?設計模式

謝飛機:嗯,經過三級緩存提早暴露對象解決的。緩存

面試官:能夠哈,那這三個緩存裏都存放了什麼樣的對象信息呢?

謝飛機:一級緩存存放的是完整對象,也叫成品對象。二級緩存存放的是半成品對象,就是那些屬性還沒賦值的對象。三級緩存存放的是 ObjectFactory<?> 類型的 lambda 表達式,就是這用於處理 AOP 循環依賴的。

面試官:能夠呀,謝飛機有所準備嘛!那若是沒有三級緩存,只有二級或者一級,能解決循環依賴嗎?

謝飛機:其實我看過資料了,能夠解決,只不過 Spring 要保證幾個事情,只有一級緩存處理流程無法拆分,複雜度也會增長,同時半成品對象可能會有空指針異常。而將半成品與成品對象分開,處理起來也更加優雅、簡單、易擴展。另外 Spring 的兩大特性中不只有 IOC 還有 AOP,也就是基於字節碼加強後的方法,該存放到哪,而三級緩存最主要,要解決的循環依賴就是對 AOP 的處理,但若是把 AOP 代理對象的建立提早,那麼二級緩存也同樣能夠解決。可是,這就違背了 Spring 建立對象的原則,Spring 更喜歡把全部的普通 Bean 都初始化完成,在處理代理對象的初始化。

面試官:飛機,不錯嘛,此次瞭解了很多。那問個簡單的,你擼過循環依賴的解決方案?

謝飛機:哦哦,這沒有,沒實踐過!!!確實應該搞一下,試試。

3、什麼是循環依賴?

1. 問題描述

瞭解問題的本質再分析問題,每每更利於對問題有更深刻的瞭解和研究。因此咱們在分析 Spring 關於循環依賴的源碼以前,先要了解下什麼是循環依賴。

  • 循環依賴分爲三種,自身依賴於自身、互相循環依賴、多組循環依賴。
  • 但不管循環依賴的數量有多少,循環依賴的本質是同樣的。就是你的完整建立依賴於我,而個人完整建立也依賴於你,但咱們互相無法解耦,最終致使依賴建立失敗。
  • 因此 Spring 提供了除了構造函數注入和原型注入外的,setter循環依賴注入解決方案。那麼咱們也能夠先來嘗試下這樣的依賴,若是是咱們本身處理的話該怎麼解決。

2. 問題體現

public class ABTest {

    public static void main(String[] args) {
        new ClazzA();
    }

}

class ClazzA {

    private ClazzB b = new ClazzB();

}

class ClazzB {

    private ClazzA a = new ClazzA();

}
  • 這段代碼就是循環依賴最初的模樣,你中有我,我中有你,運行就報錯 java.lang.StackOverflowError
  • 這樣的循環依賴代碼是無法解決的,當你看到 Spring 中提供了 get/set 或者註解,這樣之因此能解決,首先是進行了必定的解耦。讓類的建立和屬性的填充分離,先建立出半成品Bean,再處理屬性的填充,完成成品Bean的提供。

3. 問題處理

在這部分的代碼中就一個核心目的,咱們來本身解決一下循環依賴,方案以下:

public class CircleTest {

    private final static Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    public static void main(String[] args) throws Exception {
        System.out.println(getBean(B.class).getA());
        System.out.println(getBean(A.class).getB());
    }

    private static <T> T getBean(Class<T> beanClass) throws Exception {
        String beanName = beanClass.getSimpleName().toLowerCase();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        // 實例化對象入緩存
        Object obj = beanClass.newInstance();
        singletonObjects.put(beanName, obj);
        // 屬性填充補全對象
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Class<?> fieldClass = field.getType();
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            field.set(obj, singletonObjects.containsKey(fieldBeanName) ? singletonObjects.get(fieldBeanName) : getBean(fieldClass));
            field.setAccessible(false);
        }
        return (T) obj;
    }

}

class A {

    private B b;

    // ...get/set
}

class B {
    private A a;

        // ...get/set
}
  • 這段代碼提供了 A、B 兩個類,互相有依賴。但在兩個類中的依賴關係使用的是 setter 的方式進行填充。也就是隻有這樣才能避免兩個類在建立之初不非得強依賴於另一個對象。
  • getBean,是整個解決循環依賴的核心內容,A 建立後填充屬性時依賴 B,那麼就去建立 B,在建立 B 開始填充時發現依賴於 A,但此時 A 這個半成品對象已經存放在緩存到singletonObjects 中了,因此 B 能夠正常建立,在經過遞歸把 A 也建立完整了。

4、源碼分析

1. 說說細節

經過上面的例子咱們大概瞭解到,A和B互相依賴時,A建立完後填充屬性B,繼續建立B,再填充屬性A時就能夠從緩存中獲取了,以下:

那這個解決事循環依賴的事放到 Spring 中是什麼樣呢?展開細節!

雖然,解決循環依賴的核心原理同樣,但要放到支撐起整個 Spring 中 IOC、AOP 特性時,就會變得複雜一些,整個處理 Spring 循環依賴的過程以下;

  • 以上就是關於 Spring 中對於一個有循環依賴的對象獲取過程,也就是你想要的說說細節
  • 乍一看是挺多流程,可是這些也基本是你在調試代碼時候必須通過的代碼片斷,拿到這份執行流程,再調試就很是方便了。

2. 處理過程

關於本章節涉及到的案例源碼分析,已更新到 github:https://github.com/fuzhengwei/interview - interview-31

如下是單元測試中對AB依賴的獲取Bean操做,重點在於進入 getBean 的源碼跟進;

@Test
public void test_alias() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    Bean_A bean_a = beanFactory.getBean("bean_a", Bean_A.class);
    logger.info("獲取 Bean 經過別名:{}", bean_a.getBean_b());
}

org.springframework.beans.factory.support.AbstractBeanFactory.java

@Override
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    return doGetBean(name, requiredType, null, false);
}
  • 從 getBean 進入後,獲取 bean 的操做會進入到 doGetBean。
  • 之因此這樣包裝一層,是由於 doGetBean 有不少不一樣入參的重載方法,方便外部操做。

doGetBean 方法

protected <T> T doGetBean(
        final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
        throws BeansException {
    
  // 從緩存中獲取 bean 實例
    Object sharedInstance = getSingleton(beanName);
    
            // mbd.isSingleton() 用於判斷 bean 是不是單例模式
            if (mbd.isSingleton()) {
              // 獲取 bean 實例
                sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
                    @Override
                    public Object getObject() throws BeansException {
                        try {
                          // 建立 bean 實例,createBean 返回的 bean 實例化好的
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            destroySingleton(beanName);
                            throw ex;
                        }
                    }
                });
                // 後續的處理操做
                bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            }
            
    // ...

  // 返回 bean 實例
    return (T) bean;
}
  • 按照在源碼分析的流程圖中能夠看到,這一部分是從 getSingleton 先判斷是否有實例對象,對於第一次進入是確定沒有對象的,要繼續往下走。
  • 在判斷 mbd.isSingleton() 單例之後,開始使用基於 ObjectFactory 包裝的方式建立 createBean,進入後核心邏輯是開始執行 doCreateBean 操做。

doCreateBean 方法

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
        throws BeanCreationException {
    
      // 建立 bean 實例,並將 bean 實例包裝到 BeanWrapper 對象中返回
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    
        // 添加 bean 工廠對象到 singletonFactories 緩存中
        addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
              // 獲取原始對象的早期引用,在 getEarlyBeanReference 方法中,會執行 AOP 相關邏輯。若 bean 未被 AOP 攔截,getEarlyBeanReference 原樣返回 bean。
                return getEarlyBeanReference(beanName, mbd, bean);
            }
        });
        
    try {
      // 填充屬性,解析依賴關係
        populateBean(beanName, mbd, instanceWrapper);
        if (exposedObject != null) {
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
    }
    
    // 返回 bean 實例
    return exposedObject;
}
  • 在 doCreateBean 方法中包括的內容較多,但核心主要是建立實例、加入緩存以及最終進行屬性填充,屬性填充就是把一個 bean 的各個屬性字段涉及到的類填充進去。
  • createBeanInstance,建立 bean 實例,並將 bean 實例包裝到 BeanWrapper 對象中返回
  • addSingletonFactory,添加 bean 工廠對象到 singletonFactories 緩存中
  • getEarlyBeanReference,獲取原始對象的早期引用,在 getEarlyBeanReference 方法中,會執行 AOP 相關邏輯。若 bean 未被 AOP 攔截,getEarlyBeanReference 原樣返回 bean。
  • populateBean,填充屬性,解析依賴關係。也就是從這開始去找尋 A 實例中屬性 B,緊接着去建立 B 實例,最後在返回回來。

getSingleton 三級緩存

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // 從 singletonObjects 獲取實例,singletonObjects 是成品 bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // 判斷 beanName ,isSingletonCurrentlyInCreation 對應的 bean 是否正在建立中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
          // 從 earlySingletonObjects 中獲取提早曝光未成品的 bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
              // 獲取相應的 bean 工廠
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                  // 提早曝光 bean 實例,主要用於解決AOP循環依賴
                    singletonObject = singletonFactory.getObject();
                    
                    // 將 singletonObject 放入緩存中,並將 singletonFactory 從緩存中移除
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
  • singletonObjects.get(beanName),從 singletonObjects 獲取實例,singletonObjects 是成品 bean
  • isSingletonCurrentlyInCreation,判斷 beanName ,isSingletonCurrentlyInCreation 對應的 bean 是否正在建立中
  • allowEarlyReference,從 earlySingletonObjects 中獲取提早曝光未成品的 bean
  • singletonFactory.getObject(),提早曝光 bean 實例,主要用於解決AOP循環依賴

綜上,是一個處理循環依賴的代碼流程,這部分提取出來的內容主要爲核心內容,並沒與長篇大論的所有拆取出來,你們在調試的時候會涉及的比較多,儘量要本身根據流程圖操做調試幾遍。

3. 依賴解析

綜上從咱們本身去嘗試解決循環依賴,學習了循環依賴的核心解決原理。又分析了 Spring 解決的循環依賴的處理過程以及核心源碼的分析。那麼接下來咱們在總結下三級緩存分別不一樣的處理過程,算是一個總結,也方便你們理解。

1. 一級緩存能解決嗎?

  • 其實只有一級緩存並非不能解決循環依賴,就像咱們本身作的例子同樣。
  • 可是在 Spring 中若是像咱們例子裏那麼處理,就會變得很是麻煩,並且也可能會出現 NPE 問題。
  • 因此如圖按照 Spring 中代碼處理的流程,咱們去分析一級緩存這樣存放成品 Bean 的流程中,是不能解決循環依賴的問題的。由於 A 的成品建立依賴於 B,B的成品建立又依賴於 A,當須要補全B的屬性時 A 仍是沒有建立完,因此會出現死循環。

2. 二級緩存能解決嗎?

  • 有了二級緩存其實這個事處理起來就容易了,一個緩存用於存放成品對象,另一個緩存用於存放半成品對象。
  • A 在建立半成品對象後存放到緩存中,接下來補充 A 對象中依賴 B 的屬性。
  • B 繼續建立,建立的半成品一樣放到緩存中,在補充對象的 A 屬性時,能夠從半成品緩存中獲取,如今 B 就是一個完整對象了,而接下來像是遞歸操做同樣 A 也是一個完整對象了。

3. 三級緩存解決什麼?

  • 有了二級緩存都能解決 Spring 依賴了,怎麼要有三級緩存呢。其實咱們在前面分析源碼時也提到過,三級緩存主要是解決 Spring AOP 的特性。AOP 自己就是對方法的加強,是 ObjectFactory<?> 類型的 lambda 表達式,而 Spring 的原則又不但願將此類類型的 Bean 前置建立,因此要存放到三級緩存中處理。
  • 其實總體處理過程相似,惟獨是 B 在填充屬性 A 時,先查詢成品緩存、再查半成品緩存,最後在看看有沒有單例工程類在三級緩存中。最終獲取到之後調用 getObject 方法返回代理引用或者原始引用。
  • 至此也就解決了 Spring AOP 所帶來的三級緩存問題。本章節涉及到的 AOP 依賴有源碼例子,能夠進行調試

5、總結

  • 回顧本文基本以實際操做的例子開始,引導你們對循環依賴有一個總體的認識,也對它的解決方案能夠上手的例子,這樣對後續的關於 Spring 對循環依賴的解決也就不會那麼陌生了。
  • 通篇全文下來你們也能夠看到,三級緩存並非非必須不可,只不過在知足 Spring 自身建立的原則下,是必須的。若是你能夠下載 Spring 源碼對這部分代碼進行改動下,提早建立 AOP 對象保存到緩存中,那麼二級緩存同樣能夠解決循環依賴問題。
  • 關於循環依賴可能並非一個好的編碼方式,若是在本身的程序中仍是要儘量使用更合理的設計模式規避循環依賴,可能這些方式會增長代碼量,但在維護上會更加方便。固然這不是強制,能夠根據你的須要而來。

6、系列推薦

相關文章
相關標籤/搜索