Spring中的循环依赖

2023/05/13

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。

假设我们没有产生循环依赖。例如,我们的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,它指定要扫描组件的basePackages。

假设我们的bean定义在包“cn.tuyucheng.taketoday.circulardependency”中:


@Configuration
@ComponentScan(basePackages = {"cn.tuyucheng.taketoday.circulardependency"})
public class TestConfig {

}

最后,我们可以编写一个JUnit测试来测试循环依赖。

测试方法可以保留为空,因为Spring会在上下文加载期间检测到循环依赖:


@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestConfig.class})
class CircularDependencyIntegrationTest {

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

如果我们尝试运行这个测试,会得到以下异常:

Caused by: org.springframework.beans.factory.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,而是创建一个代理将其注入另一个bean,注入的bean只有在第一次需要时才会完全创建。

要使用这种方法打破循环,我们可以更改CircularDependencyA:


@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

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

如果我们现在运行测试,会看到这次不会出现错误。

4.3 使用Setter/字段注入

最常用的解决方法之一,也是Spring文档所建议的,是使用setter注入。

简单地说,我们可以通过改变bean的注入方式来解决这个问题 - 使用setter注入(或字段注入)而不是构造注入。 这样,Spring会创建bean,但直到需要使用它们时才会注入依赖项。

因此,让我们将类更改为使用setter注入,并向CircularDependencyB添加另一个字段(message):


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

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


@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestConfig.class})
class CircularDependencyIntegrationTest {

    @Autowired
    ApplicationContext context;

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

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

    @Test
    void givenCircularDependency_whenSetterInjection_thenItWorks() {
        final CircularDependencyA circA = context.getBean(CircularDependencyA.class);
        assertEquals("Hi!", circA.getCircB().getMessage());
    }
}

@Bean注解告诉Spring框架必须使用这些方法来获取要注入的bean。

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

4.4 使用@PostConstruct

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

例如下面这样:


@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和InitializingBean

如果其中一个bean实现了ApplicationContextAware,则该bean可以访问Spring上下文并可以从中获取另一个bean。

通过实现InitializingBean,我们表明该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的初始化,以及使用不同的策略自己完成这一点。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章