1.什么是循环依赖?
它发生在bean A依赖于另一个bean B时,bean B依赖于bean A:
豆A→豆B→豆A
当然,我们可以有更多的暗示:
豆A→豆B→豆C→豆D→豆E→豆A
2.春天会发生什么
当Spring上下文加载所有bean时,它会尝试按照它们完全工作所需的顺序创建bean。例如,如果我们没有循环依赖,如下例所示:
豆A→豆B→豆C.
Spring将创建bean C,然后创建bean B(并将bean注入其中),然后创建bean A(并将bean B注入其中)。
但是,当具有循环依赖时,Spring无法决定应该首先创建哪个bean,因为它们彼此依赖。在这些情况下,Spring将在加载上下文时引发BeanCurrentlyInCreationException。
使用构造函数注入时,它可能发生在Spring中; 如果您使用其他类型的注入,则不应该发现此问题,因为依赖项将在需要时注入,而不是在上下文加载时注入。
3.一个快速示例
让我们定义两个相互依赖的bean(通过构造函数注入):
1 2 3 4 5 6 7 8 9 10 |
@Component public class CircularDependencyA { private CircularDependencyB circB; @Autowired public CircularDependencyA(CircularDependencyB circB) { this .circB = circB; } } |
1 2 3 4 5 6 7 8 9 10 |
@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 } } |
如果您尝试运行此测试,则会出现以下异常:
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更改为以下内容:
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; } } |
如果您现在运行测试,您将看到此次错误不会发生。
4.3。使用Setter / Field Injection
最流行的解决方法之一,也是Spring文档提出的,是使用setter注入。
简单地说,如果你改变你的bean的连接方式,使用setter注入(或现场注入)而不是构造函数注入 - 这确实解决了这个问题。这样Spring就会创建bean,但是在需要之前不会注入依赖项。
让我们这样做 - 让我们改变我们的类以使用setter注入,并将另一个字段(消息)添加到CircularDependencyB,以便我们可以进行适当的单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Component public class CircularDependencyA { private CircularDependencyB circB; @Autowired public void setCircB(CircularDependencyB circB) { this .circB = circB; } public CircularDependencyB getCircB() { return circB; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@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属性的值。
4.4。使用@PostConstruct
打破循环的另一种方法是在其中一个bean上使用@Autowired注入依赖项,然后使用@PostConstruct注释的方法来设置其他依赖项。
我们的bean可以有以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Component public class CircularDependencyA { @Autowired private CircularDependencyB circB; @PostConstruct public void init() { circB.setCircA( this ); } public CircularDependencyB getCircB() { return circB; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@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必须在设置了所有属性后执行一些操作; 在这种情况下,我们想手动设置我们的依赖项。
我们的bean的代码是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@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; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@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,这样就不需要循环依赖:它们通常是可以改进的设计的症状。
但是如果你的项目中绝对需要循环依赖,那么你可以遵循这里建议的一些解决方法。
优选的方法是使用二次注射。但是还有其他选择,通常基于阻止Spring管理bean的初始化和注入,以及使用一种策略或另一种策略自己做。
你可以找到上面显示的豆这里,和单元测试在这里。