《系列二》-- 4、循环依赖及其处理方式
阅读之前要注意的东西:本文就是主打流水账式的源码阅读,主导的是一个参考,主要内容需要看官自己去源码中验证。全系列文章基于 spring 源码 5.x 版本。
写在开始前的话:
阅读spring 源码实在是一件庞大的工作,不说全部内容,单就最基本核心部分包含的东西就需要很长时间去消化了:
- beans
- core
- context
实际上我在博客里贴出来的还只是一部分内容,更多的内容,我放在了个人,fork自 spring 官方源码仓了; 而且对源码的学习,必须是要跟着实际代码层层递进的,不然只是干巴巴的文字味同嚼蜡。
这个仓设置的公共仓,可以直接拉取。
1 什么是循环依赖
简单来说就是依赖成环了, 看如下的伪代码:
2 Spring 中的循环依赖类型
- 构造函数依赖: Bean 依赖的其它bean 通过 "构造函数" 注入
- Setter 循环依赖: Bean 依赖的其它bean 通过, "set函数" 注入
2.1 Setter 循环依赖
- Worker.class --> Car.class
- Car.class --> Factory.class
- Factory.class --> Worker.class
“工人”需要驾驶“汽车”,“汽车”需要被"工厂"修理、生产,而"工厂" 需要依靠“工人” 运作;这里是不是就是闭环了呢?
@Component
public class Worker {
private Car car;
@Autowired
public void setCar(Car car) {
this.car =car;
}
public void drive() {
// 工人驾驶汽车
car.run();
}
public void work() {
// 工人根据自己的职业完成工作
}
}
@Component
public class Car {
private Factory factory;
@Autowired
public void setFactory(Factory factory) {
this.factory =factory;
}
public void fix() {
// 汽车去工厂修理自身
this.factory.fix(car);
}
public void run() {
// 汽车可以运行
}
}
@Component
public class Factory {
private Worker workers;
@Autowired
public void setWorker(Worker workers) {
this.workers = workers;
}
public void build() {
// 工人,为工厂工作
worker.work();
}
public void fix(Car car) {
// 工厂修理汽车
}
}
综上所述,虽然逻辑没有那么严密,这里的三个单例 bean 简单的构成了循环依赖.
- 当容器注入 Worker 时发现它依赖 Car;
- 然后去加载Car 并实例化,接下来容器发现 Car 依赖 Factory;
- 接着又去加载并实例化 Factory ,但是问题来了,容器发现 Factory 依赖 Worker, 这不首尾串联了么?
无限套娃了呀,有木有? 要是这么一直套娃下去,解决就是内存溢出了。。。
按理说spring 启动到了这里,就该报错了,但是并不会,Setter 循环依赖是明确可以解决的循环依赖。
2.2 构造函数循环依赖
所谓构造函数循环依赖,就是几个 bean 之间成环状依赖,且是通过构造函数注入的依赖。
不同于 setter 注入可以解决,构造函数注入的循环依赖是无法处理的,只能抛出:BeanCurrentlyInCreationException。
接下来给出演示的伪代码:还是工人、汽车、工厂,但是我们把注入的方式改变一下
@Component
public class Worker {
private Car car;
@Autowired
public Worker(Car car) {
this.car =car;
}
public void drive() {
// 工人驾驶汽车
car.run();
}
public void work() {
// 工人根据自己的职业完成工作
}
}
@Component
public class Car {
private Factory factory;
@Autowired
public Car(Factory factory) {
this.factory =factory;
}
public void fix() {
// 汽车去工厂修理自身
this.factory.fix(car);
}
public void run() {
// 汽车可以运行
}
}
@Component
public class Factory {
private Worker workers;
@Autowired
public Factory(Worker workers) {
this.workers = workers;
}
public void build() {
// 工人,为工厂工作
worker.work();
}
public void fix(Car car) {
// 工厂修理汽车
}
}
要说区别吧,跟上边的 setter 循环依赖的去呗还真不大,就是把 setter 函数改成了构造函数而已。
2.3 总结
这里主要的区别是,构造函数注入依赖时,必须要保证 【所依赖的bean 对象】 已经正确加载;因为他们仨的构造函数,成环依赖注定无法创建成功;
这不就是蛋生只因,只因生蛋的问题了?
而 setter 注入的方式中,可以先使用 "无参构造函数" new 出来相关的几个对象;当三个对象都创建之后,在后期按照依赖顺序设置对象地址引用即可。
实际上 spring 中也只支持单例的 Setter 循环依赖的消解,试想一下:
- 若上述案例的 Worker Car Factory 三者全是【原型模式】作用域的bean, 我们为无参构造函数创建出来的bean 注入循环依赖时,
必定会再次陷入,只因生蛋,蛋生只因的死循环中。因为 【原型模式】 作用域bean被当作依赖时,必须创建一个新的 bean,
这样势必导致无限创建依赖环中的bean,内存会被快速消耗殆尽。
这里留下一个思考问题, "spring只能消解单例的 Setter 注入的循环依赖",这个说法来源说得不甚明了。
- 理论上来说,不论是3个bean,抑或是更多的 bean 成环状依赖,只要这个环状依赖中,
存在至少一个单例bean 时,那么这个无限循环就可以被这个单例bean 通过无参构造函数创建的提前暴露的bean 所消解。
3 Spring 对bean 及其依赖bean 的加载顺序
以Worker、Car、Factory 为例,当程序启动可能会以如下顺序进行bean 的加载:
-
Spring 容器加载 Worker_Bean, 发现它依赖于 Car_Bean, 于是在 "当前正在创建bean池" 中记录,Worker_Bean 转而去加载 Car_bean
-
同理加载 Car_bean 时发现它依赖于 Factory_Bean, 重复上述操作:在 "当前正在创建bean池" 记录 Car_bean, 转而加载 Factory_bean
-
然后 Spring 容器加载 Factory_bean,并向 "当前正在创建bean池" 记录Factory_bean;
接着spring容器解析 Factory_bean 的依赖时,发现它依赖于:Worker_bean;
此时 Worker_bean 已经存在于 "当前正在创建bean池" 中了;
一般情况下这时候应该抛出 "循环依赖" 异常了。
不过这并不是没有转机的,前边提到过,spring 可以消解单例 bean 的 Setter 循环依赖,接下来的第四节将详细介绍具体的冲突消解原理。
4 Spring 对 Setter 依赖的消解
4.1 ObjectFactory 接口介绍
在讲 spring 消解单例 Setter 循环依赖之前,我们引入一个接口
package org.springframework.beans.factory;
import org.springframework.beans.BeansException;
/**
* <p>This interface is similar to {@link FactoryBean}, but implementations
* of the latter are normally meant to be defined as SPI instances in a
* {@link BeanFactory}, while implementations of this class are normally meant
* to be fed as an API to other beans (through injection). As such, the
* {@code getObject()} method has different exception handling behavior.
*/
@FunctionalInterface
public interface ObjectFactory<T> {
/**
* Return an instance (possibly shared or independent)
* of the object managed by this factory.
*/
T getObject() throws BeansException;
}
看类注释,讲得很清楚了:
- ObjectFactory接口,相当类似前边提到过的, FactoryBean 接口。但是区别是 ObjectFactory 中管理的bean 更像是一个中间结果,
它一般会被当作 "API" 提供给别的bean. 【英文比较好的伙计可以看上边的-类注释】
实际上在 spring 官方给出的解释中: ObjectFactory 用于, 提前暴露一个创建中的 bean。
重点关注关键词: "提前暴露"
4.2 循环依赖的消解
我们结合最新引入的 ObjectFactory,重新梳理下 Spring 加载bean 的过程。
还是以 Worker、Car、Factory 为例,当程序启动可能会以如下顺序进行bean 的加载:
-
Spring 容器加载 Worker_Bean, 首先利用Worker类的无参构造函数创建bean,使用 ObjectFactory 管理它,并提前暴露该创建中的bean;
然后spring 解析依赖时,发现Worker_bean 依赖于 Car_Bean, 于是在 "当前正在创建bean池" 中记录Worker_Bean, 转而去加载 Car_bean。 -
同理加载 Car_bean,先根据无参构造函数创建Car类的 bean,然后用ObjectFactory 提前暴露该创建中的bean;
然后spring 解析依赖时,发现Car_bean 依赖于 Factory_bean, 同样的在 "当前正在创建bean池" 记录 Car_bean, 转而加载 Factory_bean。 -
然后 Spring 容器加载Car_bean 依赖的 Factory_bean ,重复上述流程:
- 无参构造函数创建bean
- ObjectFactory 管理提前暴露的 Factory_bean
- "当前正在创建bean池" 中记录 Factory_bean
-
接下来 spring 解析发现, Factory_bean 依赖通过 setter 注入的 Worker_bean;
这时候由于,Worker_bean 已经存在于 "当前正在创建bean池" 中了,那么就可以去获取 ObjectFactory 管理的,提前暴露的 Worker_bean了。 -
最后同理:提前暴露的 Worker_bean 被加载后,继续去加载关联的提前暴露bean: Car_bean、 Factory_bean ...
直至最终加载完,循环依赖中的所有bean。
5 实验
为了证明 2.3 小节遗留的问题,引入本节的实验:
可以通过调整如下的变量进行对比,从而观察,spring对 循环依赖的消解。
Bean 的注入方式:
- Setter 注入
- 构造函数注入
- @LookUp 注入
Bean 作用域:
- 单例作用域(单例)
- 原型作用域(“多例”)
5.1 实验场景 && 结论
结论:
当多个bean 成环状依赖时:只需要保证这个环中有一个bean 是单列, 且它所需要依赖的其它 bean 都是被延迟注入的 (Setter注入、@LookUp注入等方式),那么该循环依赖可以被消解。
关于这个依赖环中的其它bean:
可以是任意方式注入
- 构造函数注入 [依赖的Bean初始化时,必须通过构造函数传入]
- Setter注入[可延迟注入,类似 @LookUp]
可以是任意作用域
- singleton
- prototype
5.3 测试代码
参考下边的实际测试代码
有5个产品制造相关的 bean(Service):
- 【prototype】 Manager: 管理员,为工厂工作,负责 NiceCar
- 【prototype】 Worker: 普通工作人员,为工厂工作,负责 Car
- 【prototype】 Car: 普通汽车
- 【多例】 NiceCar: 精致汽车
- 【singleton】 CarFactory: 工厂,负责所有的汽车生产
1个对外接口bean(Controller),它负责对外暴露工厂的访问入口:访问工厂获取 (Car / NiceCar), 它不在循环依赖环中,故此不讨论它的作用域:
- ExecuteController: 测试web程序入口,我们可以通过它得到 Car 和 NiceCar 产品
有如下依赖关系的类图
参考下图:
图中 CarFactory 把 Setter 注入的代码删除了; 其实是可以保留的,Setter 和 @LookUp 的工作互不干扰
-
Setter 保证 CarFactory_Bean 创建并初始化之后:car 和 niceCar 有初始值 【Setter 注入后不再被更新】
-
而 LookUp 保证 produceCar() 和 produceNiceCar() 每次从 Java 堆得到一个全新的 Car_Bean 或者 NiceCar_Bean
图中绿色线条标记的是,"多例" bean, 且都是以最致命的 "构造函数注入"。
实际上,多例 bean想要生效,需要使用 @LookUp 注解实时读取,但是这里为了便于演示功能的复杂性,特意使用了最致命的场景。
所谓致命,就是程序无法启动,或者 bean 访问时报错。
演示代码地址: