1. 什么是循環(huán)依賴
當(dāng)一個(gè)bean A依賴于另一個(gè)bean B,并且bean B也依賴于bean A時(shí),就會(huì)發(fā)生這種情況:
Bean A → Bean B → Bean A
當(dāng)然,我們能有更多的beans依賴如下:
Bean A → Bean B → Bean C → Bean D → Bean E → Bean A
2. Spring中發(fā)生了什么
當(dāng)Spring上下文加載所有bean時(shí),它將嘗試按照它們完全工作所需的順序創(chuàng)建bean。 例如,如果我們沒有循環(huán)依賴關(guān)系,例如以下情況:
Bean A→Bean B→Bean C
Spring將創(chuàng)建bean C,然后創(chuàng)建bean B(并將bean C注入到其中),然后創(chuàng)建bean A(并將bean B注入到其中)。
但是,當(dāng)具有循環(huán)依賴關(guān)系時(shí),Spring無法決定應(yīng)首先創(chuàng)建哪個(gè)bean,因?yàn)樗鼈兿嗷ヒ蕾嚒?在這些情況下,Spring在加載上下文時(shí)將引發(fā)BeanCurrentlyInCreationException。
使用構(gòu)造函數(shù)注入時(shí),它可能會(huì)在Spring中發(fā)生。 如果使用其他類型的注入,則應(yīng)該不會(huì)發(fā)現(xiàn)此問題,因?yàn)橐蕾図?xiàng)將在需要時(shí)注入,而不是在上下文加載時(shí)注入。
3. 一個(gè)簡(jiǎn)單的例子
讓我們定義兩個(gè)相互依賴的bean(通過構(gòu)造函數(shù)注入):
@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;
}
}
現(xiàn)在,我們可以為測(cè)試編寫一個(gè)Configuration類,將其稱為TestConfig,它指定要掃描組件的基本程序包。 假設(shè)我們的bean是在“ com.baeldung.circulardependency”包中定義的:
@Configuration
@ComponentScan(basePackages = { "com.baeldung.circulardependency" })
public class TestConfig {
}
最后,我們可以編寫一個(gè)JUnit測(cè)試來檢查循環(huán)依賴。 該測(cè)試可以為空,因?yàn)樵谏舷挛募虞d期間將檢測(cè)到循環(huán)依賴關(guān)系。
@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
}
}
If you try to run this test, you will get the following exception:
BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?
4. 解決方法
我們將展示一些最流行的方法來解決此問題。
4.1 重新設(shè)計(jì)
如果您有循環(huán)依賴關(guān)系,則可能是您遇到了設(shè)計(jì)問題,并且職責(zé)沒有很好地分開。 您應(yīng)該嘗試正確地重新設(shè)計(jì)組件,以使它們的層次結(jié)構(gòu)設(shè)計(jì)得很好,并且不需要循環(huán)依賴項(xiàng)。
如果您不能重新設(shè)計(jì)組件(可能有很多原因:遺留代碼,已經(jīng)過測(cè)試且無法修改的代碼,沒有足夠的時(shí)間或資源來進(jìn)行完全重新設(shè)計(jì)……),則可以嘗試一些變通辦法。
4.2 使用 @Lazy
打破周期的一種簡(jiǎn)單方法是說Spring懶惰地初始化一個(gè)bean。 也就是說:與其完全初始化bean,不如創(chuàng)建一個(gè)代理以將其注入到另一個(gè)bean中。 注入的Bean僅在首次使用時(shí)才完全創(chuàng)建。
要通過我們的代碼嘗試此操作,可以將CircularDependencyA更改為以下內(nèi)容:
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}
如果現(xiàn)在運(yùn)行測(cè)試,您將看到這次不會(huì)發(fā)生錯(cuò)誤。
4.3 使用 Setter/Field 注入
最受歡迎的解決方法之一,也是Spring文檔提出的建議,就是使用setter注入。
簡(jiǎn)而言之,如果您更改了使用setter注入(或字段注入)而不是構(gòu)造函數(shù)注入的方式連接bean的方式,那么確實(shí)可以解決該問題。 通過這種方式,Spring創(chuàng)建了bean,但是直到需要它們時(shí)才注入依賴項(xiàng)。
我們來做–讓我們更改類以使用setter注入,并將另一個(gè)字段(消息)添加到CircularDependencyB中,以便我們進(jìn)行適當(dāng)?shù)膯卧獪y(cè)試:
@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;
}
}
現(xiàn)在我們必須對(duì)單元測(cè)試進(jìn)行一些更改:
@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的實(shí)現(xiàn)。
@Test:測(cè)試將從上下文中獲取CircularDependencyA bean,并斷言其CircularDependencyB已被正確注入,并檢查其消息屬性的值。
4.4 使用 @PostConstruct
打破周期的另一種方法是在一個(gè)bean上使用@Autowired注入依賴項(xiàng),然后使用帶有@PostConstruct注釋的方法設(shè)置另一個(gè)依賴項(xiàng)。
我們的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;
}
}
并且我們可以運(yùn)行與之前相同的測(cè)試,因此我們檢查循環(huán)依存關(guān)系異常是否仍未拋出,并且依存關(guān)系已正確注入。
4.5 實(shí)現(xiàn)ApplicationContextAware and InitializingBean
如果其中一個(gè)bean實(shí)現(xiàn)了ApplicationContextAware,則該bean可以訪問Spring上下文,并且可以從那里提取另一個(gè)bean。 在實(shí)現(xiàn)InitializingBean時(shí),我們指示該bean必須在其所有屬性都已設(shè)置之后才能執(zhí)行一些操作; 在這種情況下,我們要手動(dòng)設(shè)置依賴關(guān)系。
我們的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;
}
}
同樣,我們可以運(yùn)行先前的測(cè)試,并查看未引發(fā)異常,并且該測(cè)試按預(yù)期運(yùn)行。
5. 總結(jié)
在Spring中,有很多方法可以處理循環(huán)依賴。 首先要考慮的是重新設(shè)計(jì)bean,因此不需要循環(huán)依賴:它們通常是可以改進(jìn)的設(shè)計(jì)的癥狀。
但是,如果您的項(xiàng)目中絕對(duì)需要循環(huán)依賴,則可以遵循此處建議的一些解決方法。
首選方法是使用二傳手進(jìn)樣。 但是還有其他選擇,通常基于阻止Spring管理Bean的初始化和注入,然后自己使用一種或多種策略來完成。
這些示例可以在GitHub項(xiàng)目中找到。
相關(guān)文章可看:Java詳解之Spring Bean的循環(huán)依賴解決方案