什么是循环依赖

一般场景是一个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

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 中使用构造函数注入时可能会发生这种情况;如果您使用其他类型的注入,您应该不会发现此问题,因为依赖项将在需要时注入,而不是在上下文加载时注入。

一个简单的例子

让我们定义两个相互依赖的 bean(通过构造函数注入):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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;
}
}

现在我们可以为测试编写一个 Configuration 类,我们称之为TestConfig,它指定要扫描组件的基本包。假设我们的 bean 定义在包“ com.baeldung.circulardependency ”中:

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

最后我们可以编写一个 JUnit 测试来检查循环依赖。测试可以为空,因为在上下文加载期间将检测到循环依赖。

1
2
3
4
5
6
7
8
9
@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
}
}

如果您尝试运行此测试,您将收到以下异常:

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

应对办法

重新设计

当您具有循环依赖关系时,可能是您遇到了设计问题,并且职责没有很好地分开。您应该尝试正确地重新设计组件,以使它们的层次结构设计得很好,并且不需要循环依赖项。

如果您无法重新设计组件(可能有很多可能的原因:遗留代码、已经过测试且无法修改的代码、没有足够的时间或资源进行完整的重新设计……),可以尝试一些变通方法。

使用@Lazy

打破循环的一种简单方法是说 Spring 懒惰地初始化其中一个 bean。也就是说:它不会完全初始化 bean,而是创建一个代理以将其注入另一个 bean。注入的Bean仅在首次需要时才完全创建。

要使用我们的代码尝试此操作,您可以将 CircularDependencyA 更改为以下内容:

1
2
3
4
5
6
7
8
9
10
@Component
public class CircularDependencyA {

private CircularDependencyB circB;

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

如果您现在运行测试,您将看到这次错误不会发生。

使用 Setter/Field 注入

最流行的解决方法之一,也是Spring 文档提出的,是使用 setter 注入。

简单地说,如果您更改 bean 的连接方式以使用 setter 注入(或字段注入)而不是构造函数注入 - 这确实解决了问题。通过这种方式,Spring 创建了 bean,但在需要它们之前不会注入依赖项。

让我们这样做——让我们改变我们的类以使用 setter 注入,并将另一个字段 ( message )添加到CircularDependencyB以便我们可以进行适当的单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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;
}
}

现在我们必须对单元测试进行一些更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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 的实现。

@Test:测试将从上下文中获取 CircularDependencyA bean 并断言其 CircularDependencyB 已正确注入,检查其message属性的值。

使用@PostConstruct

打破循环的另一种方法是在其中一个 bean 上使用*@Autowired注入一个依赖项,然后使用一个用@PostConstruct*注释的方法来设置另一个依赖项。

我们的 bean 可以有以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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;
}
}

我们可以运行与之前相同的测试,因此我们检查循环依赖异常仍然没有被抛出并且依赖被正确注入。

实现ApplicationContextAware和InitializingBean

如果其中一个 bean 实现了ApplicationContextAware,则该 bean 可以访问 Spring 上下文并可以从那里提取另一个 bean。实现InitializingBean我们表明这个 bean 在它的所有属性都被设置后必须做一些动作;在这种情况下,我们想手动设置我们的依赖项。

我们的 bean 的代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@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;
}
}

同样,我们可以运行之前的测试,并看到没有抛出异常并且测试按预期工作。