写作时间: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