什么是循环依赖以及如何避免和解决?

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 协议》,转载必须注明作者和本文链接
MissYou-Coding
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
Coding Peasant @ 互联网
文章
193
粉丝
10
喜欢
60
收藏
63
排名:602
访问:1.3 万
私信
所有博文
博客标签
社区赞助商