写作时间:2019-10-07
Spring Boot: 2.1 ,JDK: 1.8, IDE: IntelliJ IDEA
说明
什么是循环引用?
Bean A --> Bean B --> Bean A
当然如果引用的圈大一点也可以
Bean A --> Bean B --> Bean C --> Bean D --> Bean E --> Bean A
要解决循环引用的问题,要么设计上就禁止出现互相依赖的问题;要么就是把依赖圈中的某个节点设置为弱引用,也就是必须优先设置依赖的对方,如果依赖方已经释放,则弱引用方也被释放。
1. Spring中怎么会出现循环引用
当Spring context加载所有的beans时,它尝试按照顺序创建beans。比如,如果没有循环依赖的情况下,类似下面:
Bean A --> Bean B --> Bean C
Spring 会先创建bean C, 然后创建bean B(同时注入bean C到B),最后创建bean A(同时注入bean B到A)。
但是,如果是循环引用,Spring就决定不了应该先创建哪个bean。这种情况下,Spring在加载context的时候,就会抛出异常BeanCurrentlyInCreationException.
循环依赖只会发生在构造函数注入constructor injection; 如果你用其它方式的注入(比如属性注入),就不会出现循环依赖。因为依赖注入不是在context loading阶段,context加载结束后按需加载。
2. 工程建立
参照教程【SpringBoot 2.1 | 第一篇:构建第一个SpringBoot工程】新建一个Spring Boot项目,名字叫CircularDependency, 在目录src/main/java/resources
下找到配置文件application.properties
,重命名为application.yml
。
3. 循环引用的例子
创建两个通过构造函数注入相互依赖的两个类
CircularDependencyA, CircularDependencyB
@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;
}
}
现在我们一个Configuration class来测试,命名为TestConfig。
这个类扫描指定package成为components。
@Configuration
@ComponentScan(basePackages = { "zgpeace.spring.circulardependency.circular" })
public class TestConfig {
}
最后写个JUnit去测试循环依赖。 测试内容为空,因为循环依赖在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
}
}
运行Unit Test, 将会得到下面的报错信息
BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?
4.1 解决方案:重新设计
如果你有一个循环依赖,说明设计有问题,从根源上重新设计就好。
4.2 解决方案:@Lazy
一个简单的方法就是在loading context的时候打断循环依赖,把其中一个beans声明为lazily。这样子就可以其中一环只有在第一次调用的时候,才会去创建。
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}
4.3 解决方案:用Setter/Field Injeciton
比较流行的解决方案是,用Setter属性的注入,参考 Spring documentation proposes。把原来构造函数注入的方式,改为属性注入的方式,这样子beans只有在第一次调用的时候才会去加载。
两个Beans的实现改为如下
@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;
}
}
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());
}
}
解析:
@Bean: 告诉Spring framework,这些方法用来检索bean注入的初始化。
@Test:从context获取CircularDependencyA bean,断言CircularDependencyB已经被注入为A的属性,检查它的message属性。
4.4 解决方案:用@PostConstruct
另一种方式去打断循环依赖圈,一个bean声明@Autowired,另一个bean的方法声明@PostConstruct
@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 解决方案:实现ApplicationContextAware 和 InitializingBean
如果一个bean实现了接口ApplicationContextAware,bean就可以获取到Spring context,并且能够解析bean。实现接口InitializingBean表明会有一些action,等所有的属性被设置以后。这样子,我们就可以手动设置依赖。
@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;
}
}
总结
恭喜你,学会了Circular Dependency循环依赖的产生原因,已经解决方案。
代码下载:
https://github.com/zgpeace/Spring-Boot2.1/tree/master/basic/CircularDependency
参考
https://www.baeldung.com/circular-dependencies-in-spring
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans