什么是循环依赖以及如何避免和解决?
1. 什么是循环依赖
当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复制代码
|
2 循环依赖在spring中是如何发生的
当Spring的context开始加载所有beans的时候,它尝试按照某种顺序去创建这些beans,从而使得他们能完全工作。例如,如果我们没有循环依赖的话,可能会有如下的案例:
| Bean A → Bean B → Bean C复制代码
|
这样Spring会先创建bean C,然后Spring再创建Bean B(同时把C注入到B中),然后再创建Bean A(同时把bean B注入到A中)。
但是,如果发生循环依赖,他们彼此依赖,导致Spring无法决定哪一个bean最先被创建。在这样的情况下,Spring将在加载context时产生一个
BeanCurrentlyInCreationException .
循环依赖只会在Spring使用 构造器注入(constructor injection)的时候才会产生;如果你使用其他类型的注入方式,这些bean只会在被调用的时候才加载到context中,这样你就不会遇到循环依赖的问题。
3 循环依赖的简单案例
让我们定义2个通过构造器注入的互相依赖的bean
| @Componentpublic class CircularDependencyA { private CircularDependencyB circB; @Autowired public CircularDependencyA(CircularDependencyB circB) { this.circB = circB; }}复制代码
|
| @Componentpublic class CircularDependencyB { private CircularDependencyA circA; @Autowired public CircularDependencyB(CircularDependencyA circA) { this.circA = circA; }}复制代码
|
现在写一个用于测试的Configuration,就叫他TestConfig,同时指定扫描的包路径,假定我们的bean定义在“com.baeldung.circulardependency”中:
| @Configuration@ComponentScan(basePackages = { "com.baeldung.circulardependency" })public class TestConfig {}复制代码
|
最后,我们再写一个JUnit 测试用例,来验证循环依赖,由于循环依赖只有在bean加载的时候才会被检测到,导致这个测试可能是返回空。
| @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 方法一: 重新设计
如果你有一个循环引用,很可能你的代码结构有设计问题,且职责未做到很好的分离。你应该重新设计你工程的各个组件,使得他们的继承和依赖关系能有一个合理的设计,从而达到不需要循环依赖的目的。
如果你无法重新设计这些组件(很可能是如下原因: 1. 陈年老代码、2. 已经完全测试过且无法修改的代码),仍有如下几种解决方案可以解决:
4.2 方法二: 使用@Lazy
这里有一个简单的方法来打破这种循环,就是告诉Spring 在加载context之后再初始化Beans,而不是完全初始化这个bean,使用@Lazy时,Spring将创建一个代理bean注入到其他bean中,这种注入只有在bean第一次调用时才会被完全生效。
为了测试@Lazy,可以修改CircularDependencyA如下:
| @Componentpublic class CircularDependencyA { private CircularDependencyB circB; @Autowired public CircularDependencyA(@Lazy CircularDependencyB circB) { this.circB = circB; }}复制代码
|
如果你再次运行,你会发现这次没有产生error
4.3 方法三: 使用Setter/Field注入
使用Setter注入是Spring官方文档推荐的,也是最受欢迎的。
你只需要把原来使用构造器注入(@Resource @AutoWired)的方式. 替换成setter注入(或field注入),就会解决问题,这样bean只会在被调用时才会被注入。
如下是使用Setter注入的例子:
| @Componentpublic class CircularDependencyA { private CircularDependencyB circB; @Autowired public void setCircB(CircularDependencyB circB) { this.circB = circB; } public CircularDependencyB getCircB() { return circB; }}复制代码
|
| @Componentpublic 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
4.4. 方法四: 使用@PostConstruct
另外一种打破循环依赖的解决办法是:在其中一个循环依赖的bean中使用@Autowired ,同时使用一个带有@PostConstruct的方法来指定依赖关系
| @Componentpublic class CircularDependencyA { @Autowired private CircularDependencyB circB; @PostConstruct public void init() { circB.setCircA(this); } public CircularDependencyB getCircB() { return circB; }}复制代码
|
| @Componentpublic 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 and InitializingBean
如果这些循环依赖的beans的其中一个实现了ApplicationContextAware,那么这个bean就能获取到Spring context ,从而提前出其他的bean,通过实现InitializingBean,我们可以指定这个bean在索引的属性set之后再干点其他的事情,这里呢,我们就可以实现afterPropertiesSet来达到手动的set我们的依赖关系的目的。
| @Componentpublic 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; }}复制代码
|
| @Componentpublic 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的循环依赖。首先应该考虑的是,好好的设计你的beans之间的依赖关系,最好不要彼此依赖,循环依赖的产生说明你的代码设计有待提高。
但是,如果在你的项目中你确实需要循环依赖,你可以使用上面的一些解决方案。其中最好的方案是使用setter注入,其他的方案都是让Spring停止管理初始化和注入这些beans,你可以选择使用任何一种策略来实现。
以上代码可以在这个gitHub中找到GitHub project.
作者:someecho
链接:juejin.cn/post/6844904039231012878
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
本作品采用《CC 协议》,转载必须注明作者和本文链接