1. What Is a Circular Dependency?
1.什么是循环依存关系?
A circular dependency occurs when a bean A depends on another bean B, and the bean B depends on bean A as well:
当一个Bean A依赖于另一个Bean B,而Bean B也依赖于Bean A时,就会发生循环依赖。
Bean A → Bean B → Bean A
BeanA→BeanB→BeanA
Of course, we could have more beans implied:
当然,我们可以有更多的Bean暗示。
Bean A → Bean B → Bean C → Bean D → Bean E → Bean A
BeanA→BeanB→BeanC→BeanD→BeanE→BeanA
2. What Happens in Spring
2.Spring会发生什么
When the Spring context loads all the beans, it tries to create beans in the order needed for them to work completely.
当Spring上下文加载所有的Bean时,它试图按照它们完全工作所需的顺序来创建Bean。
Let’s say we don’t have a circular dependency. We instead have something like this:
假设我们没有一个循环依赖关系。相反,我们有这样的东西。
Bean A → Bean B → Bean C
BeanA→BeanB→BeanC
Spring will create bean C, then create bean B (and inject bean C into it), then create bean A (and inject bean B into it).
Spring将创建Bean C,然后创建Bean B(并将Bean C注入其中),然后创建Bean A(并将Bean B注入其中)。
But with a circular dependency, Spring cannot decide which of the beans should be created first since they depend on one another. In these cases, Spring will raise a BeanCurrentlyInCreationException while loading context.
但是在循环依赖的情况下,Spring无法决定哪个Bean应该先被创建,因为它们彼此依赖。在这种情况下,Spring会在加载上下文时引发一个BeanCurrentlyInCreationException。
It can happen in Spring when using constructor injection. If we use other types of injections, we shouldn’t have this problem since the dependencies will be injected when they are needed and not on the context loading.
如果我们使用其他类型的注入,我们应该不会有这样的问题,因为依赖将在需要时被注入,而不是在上下文加载时被注入。
3. A Quick Example
3.一个快速的例子
Let’s define two beans that depend on one another (via constructor injection):
让我们定义两个相互依赖的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;
}
}
Now we can write a Configuration class for the tests (let’s call it TestConfig) that specifies the base package to scan for components.
现在我们可以为测试编写一个配置类(我们称之为TestConfig),指定扫描组件的基础包。
Let’s assume our beans are defined in package “com.baeldung.circulardependency”:
让我们假设我们的Bean被定义在包”com.baeldung.circulardependency“中。
@Configuration
@ComponentScan(basePackages = { "com.baeldung.circulardependency" })
public class TestConfig {
}
Finally, we can write a JUnit test to check the circular dependency.
最后,我们可以写一个JUnit测试来检查循环依赖性。
The test can be empty since the circular dependency will be detected during the context loading:
测试可以是空的,因为循环依赖将在上下文加载时被检测到。
@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 we try to run this test, we will get this exception:
如果我们试图运行这个测试,我们会得到这个异常。
BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?
4. The Workarounds
4.变通方法
We’ll now show some of the most popular ways to deal with this problem.
我们现在将展示一些最流行的处理这个问题的方法。
4.1. Redesign
4.1.重新设计
When we have a circular dependency, it’s likely we have a design problem and that the responsibilities are not well separated. We should try to redesign the components properly so that their hierarchy is well designed and there is no need for circular dependencies.
当我们有一个循环的依赖关系时,很可能是我们的设计有问题,责任没有很好的分离。我们应该尝试适当地重新设计组件,使它们的层次结构设计得很好,没有必要出现循环依赖关系。
However, there are many possible reasons we may not be able to do a redesign, such as legacy code, code that has already been tested and cannot be modified, not enough time or resources for a complete redesign, etc. If we can’t redesign the components, we can try some workarounds.
然而,有许多可能的原因,我们可能无法进行重新设计,例如遗留代码、已经测试过的代码无法修改、没有足够的时间或资源进行完全的重新设计等等。如果我们不能重新设计组件,我们可以尝试一些变通方法。
4.2. Use @Lazy
4.2.使用@Lazy
A simple way to break the cycle is by telling Spring to initialize one of the beans lazily. So, instead of fully initializing the bean, it will create a proxy to inject it into the other bean. The injected bean will only be fully created when it’s first needed.
打破这个循环的一个简单方法是告诉Spring懒散地初始化其中一个Bean。因此,它不会完全初始化Bean,而是创建一个代理,将其注入到另一个Bean中。注入的Bean只有在第一次需要时才会被完全创建。
To try this with our code, we can change the CircularDependencyA:
为了在我们的代码中尝试这一点,我们可以改变CircularDependencyA。
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}
If we run the test now, we will see that the error does not happen this time.
如果我们现在运行这个测试,我们会看到这次没有发生错误。
4.3. Use Setter/Field Injection
4.3.使用设置器/字段注入
One of the most popular workarounds, and also what the Spring documentation suggests, is using setter injection.
最流行的变通方法之一,也是Spring文档所建议的,是使用setter注入。
Simply put, we can address the problem by changing the ways our beans are wired — to use setter injection (or field injection) instead of constructor injection. This way, Spring creates the beans, but the dependencies are not injected until they are needed.
简单地说,我们可以通过改变Bean的连接方式来解决这个问题–使用setter注入(或字段注入),而不是构造函数注入。这样一来,Spring创建了Bean,但依赖关系在需要时才会被注入。
So, let’s change our classes to use setter injections and add another field (message) to CircularDependencyB so we can make a proper unit test:
因此,让我们改变我们的类,使用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;
}
}
Now we have to make some changes to our unit test:
现在我们必须对我们的单元测试做一些修改。
@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());
}
}
Let’s take a closer look at these annotations.
让我们仔细看一下这些注释。
@Bean tells Spring framework that these methods must be used to retrieve an implementation of the beans to inject.
@Bean告诉Spring框架,必须使用这些方法来检索要注入的Bean的实现。
And with @Test annotation, the test will get CircularDependencyA bean from the context and assert that its CircularDependencyB has been injected properly, checking the value of its message property.
通过@Test注解,测试将从上下文中获得CircularDependencyA Bean,并断言其CircularDependencyB已经被正确注入,检查其message属性的值。
4.4. Use @PostConstruct
4.4.使用@PostConstruct
Another way to break the cycle is by injecting a dependency using @Autowired on one of the beans and then using a method annotated with @PostConstruct to set the other dependency.
另一种打破循环的方法是在其中一个Bean上使用@Autowired注入一个依赖,然后使用@PostConstruct注解的方法来设置其他依赖。
Our beans could have this code:
我们的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;
}
}
And we can run the same test we previously had, so we check that the circular dependency exception is still not being thrown and that the dependencies are properly injected.
我们可以运行与之前相同的测试,所以我们检查循环依赖异常是否仍然没有被抛出,以及依赖是否被正确注入。
4.5. Implement ApplicationContextAware and InitializingBean
4.5.实现ApplicationContextAware和InitializingBean
If one of the beans implements ApplicationContextAware, the bean has access to Spring context and can extract the other bean from there.
如果其中一个Bean实现了ApplicationContextAware,该Bean就可以访问Spring上下文,并可以从那里提取另一个Bean。
By implementing InitializingBean, we indicate that this bean has to do some actions after all its properties have been set. In this case, we want to manually set our dependency.
通过实现InitializingBean,我们表明这个Bean在其所有属性被设置后必须做一些动作。在这种情况下,我们想手动设置我们的依赖关系。
Here’s the code for our beans:
下面是我们的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;
}
}
Again, we can run the previous test and see that the exception is not thrown and that the test is working as expected.
再一次,我们可以运行之前的测试,看到异常没有被抛出,测试按预期工作。
5. Conclusion
5.结论
There are many ways to deal with circular dependencies in Spring.
在Spring中,有很多方法来处理循环依赖关系。
We should first consider redesigning our beans so there is no need for circular dependencies. That’s because circular dependencies are usually a symptom of a design that can be improved.
我们首先应该考虑重新设计我们的bean,这样就不需要循环依赖了。这是因为循环依赖通常是一种可以改进的设计的症状。
But if we absolutely need circular dependencies in our project, we can follow some of the workarounds suggested here.
但如果我们在项目中绝对需要循环依赖,我们可以遵循这里建议的一些变通方法。
The preferred method is using setter injections. But there are other alternatives, generally based on stopping Spring from managing the initialization and injection of the beans as well as accomplishing this ourselves using different strategies.
首选的方法是使用setter注入。但也有其他的选择,一般是基于停止Spring管理Bean的初始化和注入,以及使用不同的策略自己完成。
The examples in this article can be found in the GitHub project.
本文的例子可以在GitHub项目中找到。