spring中的循環依賴解決方案

  1. 什麼是循環依賴?
    一般場景是一個Bean A依賴Bean B,而Bean B也依賴Bean A.
    Bean A → Bean B → Bean A

當然我們也可以添加更多的依賴層次,比如:
Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

  1. Spring中的循環依賴
    當Spring上下文在加載所有的bean時,他會嘗試按照他們他們關聯關係的順序進行創建。比如,如果不存在循環依賴時,例如:
    Bean A → Bean B → Bean C
    Spring會先創建Bean C,再創建Bean B(並將Bean C注入到Bean B中),最後再創建Bean A(並將Bean B注入到Bean A中)。
    但是,如果我們存在循環依賴,Spring上下文不知道應該先創建哪個Bean,因爲它們依賴於彼此。在這種情況下,Spring會在加載上下文時,拋出一個BeanCurrentlyInCreationException。

當我們使用構造方法進行注入時,也會遇到這種情況。如果您使用其它類型的注入,你應該不會遇到這個問題。因爲它是在需要時纔會被注入,而不是上下文加載被要求注入。

  1. 讓我們看一個例子
    我們定義兩個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 CircularDependencyTest {

@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
解決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注入。
簡單地說,你對你須要注入的bean是使用setter注入(或字段注入),而不是構造函數注入。通過這種方式創建Bean,實際上它此時的依賴並沒有被注入,只有在你須要的時候他纔會被注入進來。

讓我們開始動手幹吧。我們將在CircularDependencyB 中添加另一個屬性,並將我們兩個Class Bean從構造方法注入改爲setter方法注入:

@Component
public class CircularDependencyA {

private CircularDependencyB circB;
 
@Autowired
public void setCircB(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(CircularDependencyA circA) {
    this.circA = circA;
}
 
public String getMessage() {
    return message;
}

}

現在,我們對修改後的代碼進單元測試:

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

@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());
}

}
下面對上面看到的註解進行說明:
@Bean:在Spring框架中,標誌着他被創建一個Bean並交給Spring管理
@Test:測試將得到從Spring上下文中獲取CircularDependencyA bean並斷言CircularDependencyB已被正確注入,並檢查該屬性的值。

4.4 使用 @PostConstruct
打破循環的另一種方式是,在要注入的屬性(該屬性是一個bean)上使用 @Autowired ,並使用@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 實現ApplicationContextAware and InitializingBean接口
如果一個Bean實現了ApplicationContextAware,該Bean可以訪問Spring上下文,並可以從那裏獲取到其他的bean。實現InitializingBean接口,表明這個bean在所有的屬性設置完後做一些後置處理操作(調用的順序爲init-method後調用);在這種情況下,我們需要手動設置依賴。

@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;
}

}
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,所以沒有必要循環依賴:他們通常是可以提高設計的一種症狀。 但是,如果你在你的項目中確實是需要有循環依賴,那麼你可以遵循一些這裏提出的解決方法。
原文:https://www.baeldung.com/circular-dependencies-in-spring

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