Java面試題:Spring中的循環依賴,給程序員帶來的心理陰影

循環依賴通常發生在兩個或多個Spring Bean之間,它們通過構造器、字段(使用@Autowired)或setter方法相互依賴,從而形成一個閉環。下面是一個使用字段注入(即使用@Autowired)導致的循環依賴的示例:

 

示例代碼: 

假設我們有兩個類,ClassA 和 ClassB,它們相互依賴:

public class ClassA {  

    @Autowired  
    private ClassB classB;  

    // ... 其他代碼 ...  
}  

@Component  
public class ClassB {  

    @Autowired  
    private ClassA classA;  

    // ... 其他代碼 ...  
}

在上面的示例中,ClassA 依賴 ClassB,而 ClassB 又依賴 ClassA。當Spring容器啓動時,它會嘗試爲這兩個類創建bean的實例。但是,由於它們之間的循環依賴,這會導致問題。

問題說明: 

當Spring容器創建ClassA的bean時,它會發現ClassA依賴於ClassB,所以它會嘗試創建ClassB的bean。當Spring容器創建ClassB的bean時,它又會發現ClassB依賴於ClassA,但此時ClassA的bean還沒有被完全初始化(因爲它正在等待ClassB的bean),這就形成了一個死循環。

 

那麼Spring是如何解決這種循環依賴問題的?

 

三級緩存機制:

Spring容器在創建bean的過程中,會維護三個緩存,分別是:singletonObjects(一級緩存)、earlySingletonObjects(二級緩存)和singletonFactories(三級緩存)。

當Spring容器開始實例化一個bean時,會先將其ObjectFactory(對象工廠)放入三級緩存中。如果在實例化過程中需要注入其他bean,並且這個bean也正在實例化中,Spring會先從一級緩存中查找該bean的實例。如果沒有找到,會到二級緩存中查找。如果二級緩存中也沒有,那麼會到三級緩存中查找ObjectFactory,並調用ObjectFactory的getObject方法來獲取bean的實例。

當獲取到bean的實例後,會將其放入二級緩存中,並從三級緩存中移除ObjectFactory。

當bean的實例化過程完成後,會將其放入一級緩存中。

通過這種方式,Spring可以在bean的實例化過程中解決循環依賴問題。

 

@Lazy註解:

在Spring中,可以使用@Lazy註解來延遲bean的初始化。當一個bean被標記爲@Lazy時,Spring容器在啓動時不會立即實例化它,而是在第一次被使用時才進行實例化。

通過將循環依賴的bean聲明爲懶加載,可以延遲它們的初始化過程,從而避免在容器啓動時發生循環依賴問題。

需要注意的是,@Lazy註解只能用於單例作用域的bean,並且要求依賴項必須是接口類型。

 

以下是使用@Lazy註解來解決循環依賴的示例代碼:

@Component  
public class ClassA {  

    private final ClassB classB;  

    // 使用@Autowired和@Lazy註解來延遲ClassB的注入  
    @Autowired  
    public ClassA(@Lazy ClassB classB) {  
        this.classB = classB;  
    }  

    // ... 其他代碼 ...  
}  

@Component  
public class ClassB {  

    private final ClassA classA;  

    // 使用@Autowired和@Lazy註解來延遲ClassA的注入  
    @Autowired  
    public ClassB(@Lazy ClassA classA) {  
        this.classA = classA;  
    }  

    // ... 其他代碼 ...  
}

在上面的示例中,@Lazy註解被用於ClassA和ClassB的構造器參數上,以延遲它們之間的依賴注入。這意味着在創建ClassA時,它不會立即嘗試去初始化ClassB,而是會得到一個代理對象。同樣,在創建ClassB時,它也不會立即初始化ClassA。

 

避免構造器循環依賴:

在Spring中,構造器循環依賴是無法解決的,因爲構造器在實例化bean的過程中被調用,如果兩個bean相互依賴對方的構造器,那麼就會形成死鎖。

因此,在設計bean的依賴關係時,應該儘量避免使用構造器注入來創建循環依賴。可以使用setter注入或字段注入來代替構造器注入。

 

下面是一個構造器循環依賴的錯誤代碼示例:

@Component  
public class ClassA {  

    private final ClassB classB;  

    // 通過構造器注入ClassB,形成循環依賴  
    @Autowired  
    public ClassA(ClassB classB) {  
        this.classB = classB;  
    }  

    // ... 其他代碼 ...  
}  

@Component  
public class ClassB {  

    private final ClassA classA;  

    // 通過構造器注入ClassA,與ClassA形成循環依賴  
    @Autowired  
    public ClassB(ClassA classA) {  
        this.classA = classA;  
    }  

    // ... 其他代碼 ...  
}

在上面的示例中,ClassA和ClassB各自在構造函數中依賴於對方,這就形成了構造器循環依賴。

 

當Spring容器嘗試創建這兩個bean的實例時,會遇到問題: 

  • Spring首先嚐試創建ClassA的實例,並發現它需要ClassB的實例。 

  • 然後,Spring嘗試創建ClassB的實例,但發現它需要ClassA的實例。 

  • 由於ClassA的實例正在等待ClassB的實例,而ClassB的實例又正在等待ClassA的實例,這導致了一個死循環,無法繼續創建bean的實例。

 

綜上所述,Spring通過三級緩存機制、@Lazy註解以及避免構造器循環依賴等方式來解決循環依賴問題。這些機制使得Spring容器能夠更加靈活地處理bean之間的依賴關係,提高系統的可維護性和可擴展性。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章