知識庫 / Spring RSS 訂閱

Spring 中的循環依賴

Spring
HongKong
8
02:49 PM · Dec 06 ,2025

1. 什麼是循環依賴?

循環依賴是指 Bean A 依賴於 Bean B,而 Bean B 又依賴於 Bean A 的情況:

Bean A → Bean B → Bean A

當然,我們也可以有更多的 Bean 隱含:

Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

2. Spring 啓動時發生的情況

當 Spring 容器加載所有 Bean 時,它會按照 Bean 所需的順序創建它們。

假設我們沒有循環依賴關係,而是像這樣:

Bean A → Bean B → Bean C

Spring 會先創建 Bean C,然後創建 Bean B(並將其注入 Bean C),最後創建 Bean A(並將其注入 Bean B)。

但是,如果存在循環依賴關係,Spring 無法確定哪個 Bean 應該首先創建,因為它們相互依賴。 在這種情況下,Spring 在加載上下文時會拋出 BeanCurrentlyInCreationException

這在 Spring 使用 構造器注入 時可能會發生。 如果使用其他類型的注入方式,由於依賴項會在需要時注入,而不是在上下文加載時注入,因此通常不會出現此問題。

3. 快速示例

讓我們定義兩個依賴於彼此的 Bean(通過構造函數注入):

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(CircularDependencyB circB) {
        this.circB = circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    @Autowired
    public CircularDependencyB(CircularDependencyA circA) {
        this.circA = circA;
    }
}

現在我們可以為測試編寫一個配置類(我們暫且稱之為 TestConfig),該類指定掃描組件的基礎包。

假設我們的 Bean 定義在 “com.baeldung.circulardependency” 包中:

@Configuration
@ComponentScan(basePackages = { "com.baeldung.circulardependency" })
public class TestConfig {
}

最後,我們可以編寫一個 JUnit 測試來檢查循環依賴。

測試可以為空,因為循環依賴會在上下文加載期間被檢測到:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyIntegrationTest {

    @Test
    public void givenCircularDependency_whenConstructorInjection_thenItFails() {
        // Empty test; we just want the context to load
    }
}

如果嘗試運行此測試,將會得到以下異常:

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?

4. 規避方案

我們將展示解決該問題的一些最流行的方法。

4.1. 重新設計

當存在循環依賴時,很可能表明存在設計問題,並且組件職責劃分不明確。我們應該嘗試正確地重新設計組件,以確保其層次結構合理,避免循環依賴。

然而,由於各種原因,我們可能無法進行重新設計,例如遺留代碼、已測試且無法修改的代碼、缺乏完成重新設計的足夠時間和資源等。如果無法重新設計組件,我們可以嘗試一些解決方法。

4.2. 使用 @Lazy</h3

通過指示 Spring 惰性初始化其中一個 Bean,可以打破循環。 相反於完全初始化該 Bean,Spring 會創建一個代理來將其注入到另一個 Bean 中。 注入的 Bean 只會在首次被需要時才會被完全創建。

為了嘗試此方法,我們可以修改 CircularDependencyA

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

如果現在運行測試,我們將會看到這次錯誤不會發生。

4.3. 使用 Setter/Field 注入

一種流行的解決方案,並且正如 Spring 文檔建議的,是使用 Setter/Field 注入。

簡單來説,我們可以通過改變 Bean 的 wiring 方式來解決問題,使用 Setter 注入(或 Field 注入)代替構造器注入。 這樣 Spring 負責創建 Bean,但依賴項直到需要時才被注入。

因此,讓我們將我們的類更改為使用 Setter 注入,併為 CircularDependencyB 添加另一個字段 (message) ,以便我們可以編寫一個合適的單元測試:

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public void setCircB(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    @Autowired
    public void setCircA(@Lazy CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

現在我們需要對我們的單元測試進行一些修改:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyIntegrationTest {

    @Autowired
    ApplicationContext context;

    @Bean
    public CircularDependencyA getCircularDependencyA() {
        return new CircularDependencyA();
    }

    @Bean
    public CircularDependencyB getCircularDependencyB() {
        return new CircularDependencyB();
    }

    @Test
    public void givenCircularDependency_whenSetterInjection_thenItWorks() {
        CircularDependencyA circA = context.getBean(CircularDependencyA.class);

        Assert.assertEquals("Hi!", circA.getCircB().getMessage());
    }
}

讓我們更詳細地瞭解這些註解。

指示 Spring 框架這些方法必須用於檢索並注入 bean 的實現。 並且,使用 註解,測試將從上下文中獲取 bean,並斷言其 已被正確注入,檢查其 屬性的值。

此外,我們結合使用 註解和 setter 注入。 通過使用 註解標記 setter 方法參數,我們指示 Spring 延遲初始化依賴 bean。 延遲初始化意味着 Spring 只會在需要時才創建和注入 bean,而不是在啓動時一次性地初始化它

這種方法有效地打破了循環依賴鏈,因為每個 bean 只會在首次訪問時才進行初始化,從而避免了初始化衝突。 通過延遲初始化帶有 標記的 bean,我們確保 Spring 可以優雅地處理循環依賴,而無需遇到運行時問題。

4.4. 使用 @PostConstruct

另一種打破循環的方法是使用 @PostConstruct 註解注入依賴,然後使用一個帶有 @PostConstruct 註解的方法來設置另一個依賴項。

我們的 Bean 可以包含以下代碼:

@Component
public class CircularDependencyA {

    @Autowired
    private CircularDependencyB circB;

    @PostConstruct
    public void init() {
        circB.setCircA(this);
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;
	
    private String message = "Hi!";

    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
	
    public String getMessage() {
        return message;
    }
}

我們仍然可以運行我們之前相同的測試,以確保循環依賴異常仍然沒有被拋出,並且依賴項得到正確注入。

4.5. 實現 ApplicationContextAwareInitializingBean

如果其中一個 Bean 實現 ApplicationContextAware,則該 Bean 可以訪問 Spring 容器,並從中提取其他 Bean。

通過實現 InitializingBean,我們表明該 Bean 在所有屬性已設置後需要執行一些操作。 在這種情況下,我們希望手動設置我們的依賴項。

以下是我們的 Bean 的代碼:

@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {

    private CircularDependencyB circB;

    private ApplicationContext context;

    public CircularDependencyB getCircB() {
        return circB;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        circB = context.getBean(CircularDependencyB.class);
    }

    @Override
    public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
        context = ctx;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

再次運行之前的測試,我們看到異常未被拋出,測試結果符合預期。

5. 結論

以下有多種處理 Spring 中循環依賴的方法。

我們應該首先考慮重新設計我們的 Bean,以避免出現循環依賴。因為循環依賴通常是設計可以改進的症狀。

但是,如果我們在項目中確實需要循環依賴,我們可以遵循此處提出的一些解決方法。

首選的方法是使用 setter 注入。但還有其他替代方案,通常是阻止 Spring 管理 Bean 的初始化和注入,以及我們自己使用不同的策略來實現這一點。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.