Spring 是怎麼處理循環依賴的?

  • 2022 年 2 月 12 日
  • 筆記

Java語法中的循環依賴

首先看一個使用構造函數的循環依賴,如下:

public class ObjectA {
    private ObjectB b;
    public ObjectA(ObjectB b) {
        this.b = b;
    }
}

public class ObjectB {
    private ObjectA a;

    public ObjectB(ObjectA a) {
        this.a = a;
    }
}

public class Main {
    public static void main(String[] args) {
        //ObjectB b = new ObjectB(new ObjectA(new ObjectB()));
    }
}

大家可以看上面這個例子,可以看出是沒有辦法new出ObjectA或者ObjectB的

那怎麼解決上面的例子呢?如下:

public class ObjectA {
    private ObjectB b;

    public void setB(ObjectB b) {
        this.b = b;
    }

    public ObjectB getB() {
        return this.b;
    }
}

public class ObjectB {
    private ObjectA a;

    public void setA(ObjectA a) {
        this.a = a;
    }

    public ObjectA getA() {
        return this.a;
    }
}

public class Main {
    public static void main(String[] args) {
        ObjectB b = new ObjectB();
        ObjectA a = new ObjectA();
        b.setA(a);
        a.setB(b);
        System.out.println(a + " " + a.getB());
        System.out.println(b + " " + b.getA());
    }
}

輸出如下:

cn.eagle.li.spring.dependence.demo1.ObjectA@4534b60d cn.eagle.li.spring.dependence.demo1.ObjectB@3fa77460
cn.eagle.li.spring.dependence.demo1.ObjectB@3fa77460 cn.eagle.li.spring.dependence.demo1.ObjectA@4534b60d

可以看出把構造函數去掉,然後增加set方法就可以實現循環依賴的問題了。

Spring 的構造函數循環依賴

測試例子如下:

@Component
public class MyBeanOne {
    private MyBeanTwo myBeanTwo;

    @Autowired
    public MyBeanOne(MyBeanTwo myBeanTwo) {
        this.myBeanTwo = myBeanTwo;
    }
}

@Component
public class MyBeanTwo {
    private MyBeanOne myBeanOne;

    @Autowired
    public MyBeanTwo(MyBeanOne myBeanOne) {
        this.myBeanOne = myBeanOne;
    }
}

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(MyBeanOne.class, MyBeanTwo.class);
        MyBeanOne myBeanOne = context.getBean(MyBeanOne.class);
        MyBeanTwo myBeanTwo = context.getBean(MyBeanTwo.class);
        System.out.println(myBeanOne);
        System.out.println(myBeanTwo);
    }
}

輸出如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'myBeanOne': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)

在DefaultSingletonBeanRegistry.beforeSingletonCreation()里打斷點,看一下為什麼出錯了:
第一次經過:

第二次經過:

第三次經過:

我們可以猜測出錯的原因是這樣的:

去生成myBeanOne,需要生成myBeanTwo
去生成myBeanTwo,需要生成myBeanOne
去生成myBeanOne,發現myBeanOne已經在創建中了

Spring 的Set方式循環依賴

@Component
public class MyBeanOne {
    @Autowired
    @Getter
    private MyBeanTwo myBeanTwo;
}

@Component
public class MyBeanTwo {
    @Autowired
    @Getter
    private MyBeanOne myBeanOne;
}

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(MyBeanOne.class, MyBeanTwo.class);
        MyBeanOne myBeanOne = context.getBean(MyBeanOne.class);
        MyBeanTwo myBeanTwo = context.getBean(MyBeanTwo.class);
        System.out.println(myBeanOne + " " + myBeanOne.getMyBeanTwo());
        System.out.println(myBeanTwo + " " + myBeanTwo.getMyBeanOne());
    }
}

輸出:

cn.eagle.li.spring.dependence.demo3.MyBeanOne@3c407114 cn.eagle.li.spring.dependence.demo3.MyBeanTwo@35ef1869
cn.eagle.li.spring.dependence.demo3.MyBeanTwo@35ef1869 cn.eagle.li.spring.dependence.demo3.MyBeanOne@3c407114

可以看出通過set注入的方式是可以解決循環依賴問題的。

我們繼續在DefaultSingletonBeanRegistry.beforeSingletonCreation()里打斷點,看一下set方式是怎麼經過這裡的:
發現只經過了兩次,沒有第三次,如果有第三次的話,也就拋異常了,所以只經過兩次是正常的。

為什麼構造函數有三次,而set方式有兩次?

我們看一下DefaultSingletonBeanRegistry.beforeSingletonCreation的調用鏈:

AbstractBeanFactory.doGetBean
DefaultSingletonBeanRegistry.getSingleton
DefaultSingletonBeanRegistry.beforeSingletonCreation

那我們就在AbstractBeanFactory.doGetBean這裡打斷點,看一下set方式,為什麼沒有第三次
第一次:

第二次:

第三次:

我們可以看到第三次的myBeanOne已經有值了,它就不會執行到DefaultSingletonBeanRegistry.beforeSingletonCreation
如果換成構造方式來調試的話,在第三次,myBeanOne依舊是null的,就會繼續往下執行到DefaultSingletonBeanRegistry.beforeSingletonCreation,然後就會拋錯。

我們來看一下第三次的myBeanOne是怎麼獲取的:
DefaultSingletonBeanRegistry.getSingleton 方法如下:

可以看到是通過this.singletonFactories.get(beanName)得到一個工廠,通過這個工廠可以創建出對應的bean

我們再來看一下這些個工廠是什麼時候被放進去的,DefaultSingletonBeanRegistry.addSingletonFactory:

為什麼通過構造函數注入的方式,沒有提前放入一個工廠

再執行AbstractAutowireCapableBeanFactory.createBeanInstance 方法時

set方式會執行以下:

調用這個方法之後,回退到 AbstractAutowireCapableBeanFactory.doCreateBean() 繼續往下執行,會把工廠放進去。

構造函數的方式會執行以下:

調用這個方法,後面會繼續獲取myBeanTwo,如下:

然後同理再獲取myBeanOne,就會拋異常了,它不會回退到 AbstractAutowireCapableBeanFactory.doCreateBean(),自然也不會把工廠放進去。

最後理一下

對於Set方式,當類構造好之後,會提前把生成這個類的工廠放到快取中;而構造函數的方式,由於存在構造函數,必須在當下去獲取依賴類,所以就沒辦法構造類,其實原理和剛開始舉的Java的例子是一個道理。

參考

Spring 解決循環依賴必須要三級快取嗎?
Spring循環依賴三級快取是否可以去掉第三級快取?
Spring 的循環依賴,源碼詳細分析 → 真的非要三級快取嗎

Tags: