Spring循環依賴的三種方式以及解決辦法


https://www.cnblogs.com/liuqing576598117/p/11227007.html

一. 什么是循環依賴?
循環依賴其實就是循環引用,也就是兩個或者兩個以上的bean互相持有對方,最終形成閉環。比如A依賴於B,B依賴於C,C又依賴於A。如下圖:
注意,這里不是函數的循環調用,是對象的相互依賴關系。循環調用其實就是一個死循環,除非有終結條件。
Spring中循環依賴場景有: 
(1)構造器的循環依賴 
(2)field屬性的循環依賴
其中,構造器的循環依賴問題無法解決,只能拋出BeanCurrentlyInCreationException異常,在解決屬性循環依賴時,spring采用的是提前暴露對象的方法。
二. 怎么檢測是否存在循環依賴
檢測循環依賴相對比較容易, Bean在創建的時候可以給該Bean打標,如果遞歸調用回來發現正在創建中的話,即說明了循環依賴了
三、三種循環依賴
1:構造器的循環依賴。【這個Spring解決不了】
  Spring容器會將每一個正在創建的Bean 標識符放在一個“當前創建Bean池”中,Bean標識符在創建過程中將一直保持在這個池中,因此如果在創建Bean過程中發現自己已經在“當前創建Bean池”里時將拋出BeanCurrentlyInCreationException異常表示循環依賴;而對於創建完畢的Bean將從“當前創建Bean池”中清除掉。
  Spring容器先創建單例A,A依賴B,然后將A放在“當前創建Bean池”中,此時創建B,B依賴C ,然后將B放在“當前創建Bean池”中,此時創建C,C又依賴A, 但是,此時A已經在池中,所以會報錯,因為在池中的Bean都是未初始化完的,所以會依賴錯誤 ,(初始化完的Bean會從池中移除)
復制代碼
復制代碼
public class StudentA {
 
    private StudentB studentB ;
 
    public void setStudentB(StudentB studentB) {
        this.studentB = studentB;
    }
 
    public StudentA() {
    }
    
    public StudentA(StudentB studentB) {
        this.studentB = studentB;
    }
}
復制代碼
復制代碼
復制代碼
復制代碼
public class StudentB {
 
    private StudentC studentC ;
 
    public void setStudentC(StudentC studentC) {
        this.studentC = studentC;
    }
    
    public StudentB() {
    }
 
    public StudentB(StudentC studentC) {
        this.studentC = studentC;
    }
}
復制代碼
復制代碼
復制代碼
復制代碼
public class StudentC {
 
    private StudentA studentA ;
 
    public void setStudentA(StudentA studentA) {
        this.studentA = studentA;
    }
 
    public StudentC() {
    }
 
    public StudentC(StudentA studentA) {
        this.studentA = studentA;
    }
}
復制代碼
復制代碼

上面是很基本的3個類,,StudentA有參構造是StudentB。StudentB的有參構造是StudentC,StudentC的有參構造是StudentA ,這樣就產生了一個循環依賴的情況,

我們都把這三個Bean交給Spring管理,並用有參構造實例化

復制代碼
復制代碼
<bean id="a" class="com.liuqing.student.StudentA">  
    <constructor-arg index="0" ref="b"></constructor-arg>  
</bean>  
<bean id="b" class="com.liuqing.student.StudentB">  
    <constructor-arg index="0" ref="c"></constructor-arg>  
</bean>  
<bean id="c" class="com.liuqing.student.StudentC">  
    <constructor-arg index="0" ref="a"></constructor-arg>  
</bean>
復制代碼
復制代碼

下面是測試類:

復制代碼
public class Test {  
    public static void main(String[] args) {  
        ApplicationContext context = new ClassPathXmlApplicationContext("com/liuqing/student/applicationContext.xml");  
        //System.out.println(context.getBean("a", StudentA.class));  
    }  
}
復制代碼

執行結果報錯信息為:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:   
    Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

2、setter方式單例,默認方式

Spring中Bean實例化的圖

如圖中前兩步驟得知:Spring是先將Bean對象實例化【依賴無參構造函數】--->再設置對象屬性的

修改配置文件為set方式注入:
復制代碼
復制代碼
<!--scope="singleton"(默認就是單例方式)  -->
<bean id="a" class="com.liuqing.student.StudentA" scope="singleton">
    <property name="studentB" ref="b"></property>
</bean>
<bean id="b" class="com.liuqing.student.StudentB" scope="singleton">
    <property name="studentC" ref="c"></property>
</bean>
<bean id="c" class="com.liuqing.student.StudentC" scope="singleton">
    <property name="studentA" ref="a"></property>
</bean>
復制代碼
復制代碼

下面是測試類:

復制代碼
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("com/liuqing/student/applicationContext.xml");
        System.out.println(context.getBean("a", StudentA.class));
    }
}
復制代碼

打印結果為:

com.liuqing.student.StudentA@1fbfd6

我們結合上面那張圖看,Spring先是用構造實例化Bean對象 ,此時Spring會將這個實例化結束的對象放到一個Map中,並且Spring提供了獲取這個未設置屬性的實例化對象引用的方法。   結合我們的實例來看,,當Spring實例化了StudentA、StudentB、StudentC后,緊接着會去設置對象的屬性,此時StudentA依賴StudentB,就會去Map中取出存在里面的單例StudentB對象,以此類推,不會出來循環的問題
3、setter方式原型,prototype

修改配置文件為:

復制代碼
復制代碼
<bean id="a" class="com.liuqing.student.StudentA" scope="prototype">
    <property name="studentB" ref="b"></property>
</bean>
<bean id="b" class="com.liuqing.student.StudentB" scope="prototype">
    <property name="studentC" ref="c"></property>
</bean>
<bean id="c" class="com.liuqing.student.StudentC" scope="prototype">
    <property name="studentA" ref="a"></property>
</bean>
復制代碼
復制代碼

scope="prototype" 意思是 每次請求都會創建一個實例對象。兩者的區別是:有狀態的bean都使用Prototype作用域,無狀態的一般都使用singleton單例作用域。

測試用例:

復制代碼
復制代碼
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("com/liuqing/student/applicationContext.xml");
        //此時必須要獲取Spring管理的實例,因為現在scope="prototype" 只有請求獲取的時候才會實例化對象
        System.out.println(context.getBean("a", StudentA.class));
    }
}
復制代碼
復制代碼

打印結果:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: 
    Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

對於“prototype”作用域Bean,Spring容器無法完成依賴注入,因為“prototype”作用域的Bean,Spring容器不進行緩存,因此無法提前暴露一個創建中的Bean
四、Spring怎么解決循環依賴

Spring的循環依賴的理論依據 基於Java的引用傳遞,當獲得對象的引用時,對象的屬性是可以延后設置的。(但是構造器必須是在獲取引用之前)
Spring的單例對象的初始化主要分為三步: 
(1)createBeanInstance:實例化,其實也就是調用對象的構造方法實例化對象
(2)populateBean:填充屬性,這一步主要是多bean的依賴屬性進行填充
(3)initializeBean:調用spring xml中的init 方法。
從上面單例bean的初始化可以知道:循環依賴主要發生在第一、二步,也就是構造器循環依賴和field循環依賴。那么我們要解決循環引用也應該從初始化過程着手,對於單例來說,在Spring容器整個生命周期內,有且只有一個對象,所以很容易想到這個對象應該存在Cache中,Spring為了解決單例的循環依賴問題,使用了 三級緩存
這三級緩存分別指: 

  三級singletonFactories : 單例對象工廠的cache 

  二級earlySingletonObjects :提前暴光的單例對象的Cache 
  一級singletonObjects:單例對象的cache

 

在創建bean的時候,首先想到的是從cache中獲取這個單例的bean,這個緩存就是singletonObjects。如果獲取不到,並且對象正在創建中,就再從二級緩存earlySingletonObjects中獲取。如果還是獲取不到且允許singletonFactories通過getObject()獲取,就從三級緩存singletonFactory.getObject()(三級緩存)獲取,如果獲取到了則:從singletonFactories中移除,並放入earlySingletonObjects中。其實也就是從三級緩存移動到了二級緩存。

 

從上面三級緩存的分析,我們可以知道,Spring解決循環依賴的訣竅就在於singletonFactories這個三級cache。這個cache的類型是ObjectFactory。這里就是解決循環依賴的關鍵,發生在createBeanInstance之后,也就是說單例對象此時已經被創建出來(調用了構造器)。這個對象已經被生產出來了,雖然還不完美(還沒有進行初始化的第二步和第三步),但是已經能被人認出來了(根據對象引用能定位到堆中的對象),所以Spring此時將這個對象提前曝光出來讓大家認識,讓大家使用。

 

這樣做有什么好處呢?讓我們來分析一下“A的某個field或者setter依賴了B的實例對象,同時B的某個field或者setter依賴了A的實例對象”這種循環依賴的情況。A首先完成了初始化的第一步,並且將自己提前曝光到singletonFactories中,此時進行初始化的第二步,發現自己依賴對象B,此時就嘗試去get(B),發現B還沒有被create,所以走create流程,B在初始化第一步的時候發現自己依賴了對象A,於是嘗試get(A),嘗試一級緩存singletonObjects(肯定沒有,因為A還沒初始化完全),嘗試二級緩存earlySingletonObjects(也沒有),嘗試三級緩存singletonFactories,由於A通過ObjectFactory將自己提前曝光了,所以B能夠通過ObjectFactory.getObject拿到A對象(雖然A還沒有初始化完全,但是總比沒有好呀),B拿到A對象后順利完成了初始化階段1、2、3,完全初始化之后將自己放入到一級緩存singletonObjects中。此時返回A中,A此時能拿到B的對象順利完成自己的初始化階段2、3,最終A也完成了初始化,進去了一級緩存singletonObjects中,而且更加幸運的是,由於B拿到了A的對象引用,所以B現在hold住的A對象完成了初始化。

 

知道了這個原理時候,肯定就知道為啥Spring不能解決“A的構造方法中依賴了B的實例對象,同時B的構造方法中依賴了A的實例對象”這類問題了!因為加入singletonFactories三級緩存的前提是執行了構造器,所以構造器的循環依賴沒法解決。
五、總結
不要使用基於構造函數的依賴注入,可以通過以下方式解決:
  1.在字段上使用 @Autowired注解,讓Spring決定在合適的時機注入
  2.用基於 setter方法的依賴注入。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM