Contents

Spring 循环依赖

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.blogdemo.circulardependency ”中:

@Configuration
@ComponentScan(basePackages = { "com.blogdemo.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,而是创建一个代理将其注入另一个 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 注入(或字段注入)而不是构造函数注入。这样,Spring 会创建 bean,但直到需要它们时才会注入依赖项。

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

@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 的实现。

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

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. 实现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;
    }
}

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