天天看點

一文幫你徹底打通【循環依賴】的任督二脈

一文幫你徹底打通【循環依賴】的任督二脈

面試的重點,大廠必問之一:

循環依賴

1. 什麼是循環依賴

看下圖

一文幫你徹底打通【循環依賴】的任督二脈

  上圖是循環依賴的三種情況,雖然方式有點不一樣,但是循環依賴的本質是一樣的,就你的完整建立要依賴與我,我的完整建立也依賴于你。互相依賴進而沒法完整建立造成失敗。

2. 代碼示範

  我們再通過代碼的方式來示範下循環依賴的效果

public class CircularTest {

    public static void main(String[] args) {
        new CircularTest1();
    }
}
class CircularTest1{
    private CircularTest2 circularTest2 = new CircularTest2();
}

class CircularTest2{
    private CircularTest1 circularTest1 = new CircularTest1();
}      

執行後出現了 StackOverflowError 錯誤

一文幫你徹底打通【循環依賴】的任督二脈

  上面的就是最基本的循環依賴的場景,你需要我,我需要你,然後就報錯了。而且上面的這種設計情況我們是沒有辦法解決的。那麼針對這種場景我們應該要怎麼設計呢?這個是關鍵!

3. 分析問題

  首先我們要明确一點就是如果這個對象A還沒建立成功,在建立的過程中要依賴另一個對象B,而另一個對象B也是在建立中要依賴對象A,這種肯定是無解的,這時我們就要轉換思路,我們先把A建立出來,但是還沒有完成初始化操作,也就是這是一個半成品的對象,然後在指派的時候先把A暴露出來,然後建立B,讓B建立完成後找到暴露的A完成整體的執行個體化,這時再把B交給A完成A的後續操作,進而揭開了循環依賴的密碼。也就是如下圖:

一文幫你徹底打通【循環依賴】的任督二脈

4. 自己解決

  明白了上面的本質後,我們可以自己來嘗試解決下:

先來把上面的案例改為set/get來依賴關聯

public class CircularTest {

    public static void main(String[] args) throws Exception{
        System.out.println(getBean(CircularTest1.class).getCircularTest2());
        System.out.println(getBean(CircularTest2.class).getCircularTest1());
    }

    private static <T> T getBean(Class<T> beanClass) throws Exception{
        // 1.擷取 執行個體對象
        Object obj = beanClass.newInstance();
        // 2.完成屬性填充
        Field[] declaredFields = obj.getClass().getDeclaredFields();
        // 周遊處理
        for (Field field : declaredFields) {
            field.setAccessible(true); // 針對private修飾
            // 擷取成員變量 對應的類對象
            Class<?> fieldClass = field.getType();
            // 擷取對應的 beanName
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            // 給成員變量指派 如果 singletonObjects 中有半成品就擷取,否則建立對象
            field.set(obj,getBean(fieldClass));
        }
        return (T) obj;
    }
}

class CircularTest1{
    private CircularTest2 circularTest2;

    public CircularTest2 getCircularTest2() {
        return circularTest2;
    }

    public void setCircularTest2(CircularTest2 circularTest2) {
        this.circularTest2 = circularTest2;
    }
}

class CircularTest2{
    private CircularTest1 circularTest1;

    public CircularTest1 getCircularTest1() {
        return circularTest1;
    }

    public void setCircularTest1(CircularTest1 circularTest1) {
        this.circularTest1 = circularTest1;
    }
}      

然後我們再通過把對象執行個體化和成員變量指派拆解開來處理。進而解決循環依賴的問題

public class CircularTest {
    // 儲存提前暴露的對象,也就是半成品的對象
    private final static Map<String,Object> singletonObjects = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception{
        System.out.println(getBean(CircularTest1.class).getCircularTest2());
        System.out.println(getBean(CircularTest2.class).getCircularTest1());
    }

    private static <T> T getBean(Class<T> beanClass) throws Exception{
        //1.擷取類對象對應的名稱
        String beanName = beanClass.getSimpleName().toLowerCase();
        // 2.根據名稱去 singletonObjects 中檢視是否有半成品的對象
        if(singletonObjects.containsKey(beanName)){
            return (T) singletonObjects.get(beanName);
        }
        // 3. singletonObjects 沒有半成品的對象,那麼就反射執行個體化對象
        Object obj = beanClass.newInstance();
        // 還沒有完整的建立完這個對象就把這個對象存儲在了 singletonObjects中
        singletonObjects.put(beanName,obj);
        // 屬性填充來補全對象
        Field[] declaredFields = obj.getClass().getDeclaredFields();
        // 周遊處理
        for (Field field : declaredFields) {
            field.setAccessible(true); // 針對private修飾
            // 擷取成員變量 對應的類對象
            Class<?> fieldClass = field.getType();
            // 擷取對應的 beanName
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            // 給成員變量指派 如果 singletonObjects 中有半成品就擷取,否則建立對象
            field.set(obj,singletonObjects.containsKey(fieldBeanName)?
                    singletonObjects.get(fieldBeanName):getBean(fieldClass));
        }
        return (T) obj;
    }
}

class CircularTest1{
    private CircularTest2 circularTest2;

    public CircularTest2 getCircularTest2() {
        return circularTest2;
    }

    public void setCircularTest2(CircularTest2 circularTest2) {
        this.circularTest2 = circularTest2;
    }
}

class CircularTest2{
    private CircularTest1 circularTest1;

    public CircularTest1 getCircularTest1() {
        return circularTest1;
    }

    public void setCircularTest1(CircularTest1 circularTest1) {
        this.circularTest1 = circularTest1;
    }
}      

運作程式你會發現問題完美的解決了

一文幫你徹底打通【循環依賴】的任督二脈

  在上面的方法中的核心是getBean方法,Test1 建立後填充屬性時依賴Test2,那麼就去建立 Test2,在建立 Test2 開始填充時發現依賴于 Test1,但此時 Test1 這個半成品對象已經存放在緩存到 ​

​singletonObjects​

​ 中了,是以Test2可以正常建立,在通過遞歸把 Test1 也建立完整了。

一文幫你徹底打通【循環依賴】的任督二脈

最後總結下該案例解決的本質:

一文幫你徹底打通【循環依賴】的任督二脈

5. Spring循環依賴

  然後我們再來看看Spring中是如何解決循環依賴問題的呢?剛剛上面的案例中的對象的生命周期的核心就兩個

一文幫你徹底打通【循環依賴】的任督二脈

  而Spring建立Bean的生命周期中涉及到的方法就很多了。下面是簡單列舉了對應的方法

一文幫你徹底打通【循環依賴】的任督二脈

  基于前面案例的了解,我們知道肯定需要在調用構造方法方法建立完成後再暴露對象,在Spring中提供了三級緩存來處理這個事情,對應的處理節點如下圖:

一文幫你徹底打通【循環依賴】的任督二脈

對應到源碼中具體處理循環依賴的流程如下:

一文幫你徹底打通【循環依賴】的任督二脈

  上面就是在Spring的生命周期方法中和循環依賴出現相關的流程了。那麼源碼中的具體處理是怎麼樣的呢?我們繼續往下面看。

首先在調用構造方法的後會放入到三級緩存中

一文幫你徹底打通【循環依賴】的任督二脈

下面就是放入三級緩存的邏輯

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    // 使用singletonObjects進行加鎖,保證線程安全
    synchronized (this.singletonObjects) {
      // 如果單例對象的高速緩存【beam名稱-bean執行個體】沒有beanName的對象
      if (!this.singletonObjects.containsKey(beanName)) {
        // 将beanName,singletonFactory放到單例工廠的緩存【bean名稱 - ObjectFactory】
        this.singletonFactories.put(beanName, singletonFactory);
        // 從早期單例對象的高速緩存【bean名稱-bean執行個體】 移除beanName的相關緩存對象
        this.earlySingletonObjects.remove(beanName);
        // 将beanName添加已注冊的單例集中
        this.registeredSingletons.add(beanName);
      }
    }
  }      

然後在填充屬性的時候會存入二級緩存中

earlySingletonObjects.put(beanName,bean);
registeredSingletons.add(beanName);      

最後把建立的對象儲存在了一級緩存中

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
      // 将映射關系添加到單例對象的高速緩存中
      this.singletonObjects.put(beanName, singletonObject);
      // 移除beanName在單例工廠緩存中的資料
      this.singletonFactories.remove(beanName);
      // 移除beanName在早期單例對象的高速緩存的資料
      this.earlySingletonObjects.remove(beanName);
      // 将beanName添加到已注冊的單例集中
      this.registeredSingletons.add(beanName);
    }
  }      

6. 疑問點

這些疑問點也是面試官喜歡問的問題點

為什麼需要三級緩存

三級緩存主要處理的是AOP的代理對象,存儲的是一個ObjectFactory

三級緩存考慮的是帶你對象,而二級緩存考慮的是性能-從三級緩存的工廠裡建立出對象,再扔到二級緩存(這樣就不用每次都要從工廠裡拿)

沒有三級環境能解決嗎?

沒有三級緩存是可以解決循環依賴問題的

三級緩存分别什麼作用

一級緩存:正式對象

二級緩存:半成品對象