《系列二》-- 4、循环依赖及其处理方式

阅读之前要注意的东西:本文就是主打流水账式的源码阅读,主导的是一个参考,主要内容需要看官自己去源码中验证。全系列文章基于 spring 源码 5.x 版本。

写在开始前的话:

阅读spring 源码实在是一件庞大的工作,不说全部内容,单就最基本核心部分包含的东西就需要很长时间去消化了:

  • beans
  • core
  • context

实际上我在博客里贴出来的还只是一部分内容,更多的内容,我放在了个人,fork自 spring 官方源码仓了; 而且对源码的学习,必须是要跟着实际代码层层递进的,不然只是干巴巴的文字味同嚼蜡。

https://gitee.com/bokerr/spring-framework-5.0.x-study

这个仓设置的公共仓,可以直接拉取。



Spring源码阅读系列--全局目录.md



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 的加载:

  1. Spring 容器加载 Worker_Bean, 发现它依赖于 Car_Bean, 于是在 "当前正在创建bean池" 中记录,Worker_Bean 转而去加载 Car_bean

  2. 同理加载 Car_bean 时发现它依赖于 Factory_Bean, 重复上述操作:在 "当前正在创建bean池" 记录 Car_bean, 转而加载 Factory_bean

  3. 然后 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 的加载:

  1. Spring 容器加载 Worker_Bean, 首先利用Worker类的无参构造函数创建bean,使用 ObjectFactory 管理它,并提前暴露该创建中的bean;
    然后spring 解析依赖时,发现Worker_bean 依赖于 Car_Bean, 于是在 "当前正在创建bean池" 中记录Worker_Bean, 转而去加载 Car_bean。

  2. 同理加载 Car_bean,先根据无参构造函数创建Car类的 bean,然后用ObjectFactory 提前暴露该创建中的bean;
    然后spring 解析依赖时,发现Car_bean 依赖于 Factory_bean, 同样的在 "当前正在创建bean池" 记录 Car_bean, 转而加载 Factory_bean。

  3. 然后 Spring 容器加载Car_bean 依赖的 Factory_bean ,重复上述流程:

    • 无参构造函数创建bean
    • ObjectFactory 管理提前暴露的 Factory_bean
    • "当前正在创建bean池" 中记录 Factory_bean
  4. 接下来 spring 解析发现, Factory_bean 依赖通过 setter 注入的 Worker_bean;
    这时候由于,Worker_bean 已经存在于 "当前正在创建bean池" 中了,那么就可以去获取 ObjectFactory 管理的,提前暴露的 Worker_bean了。

  5. 最后同理:提前暴露的 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 产品

有如下依赖关系的类图

graph LR D(ExecuteManager) --> B(CarFactory) --> C(Car) C(Car) --> A(Worker) A(Worker) --> B(CarFactory)
graph LR D(ExecuteManager) --> B(CarFactory) --> C(NiceCar) C(NiceCar) --> A(Manager) A(Manager) --> B(CarFactory)

参考下图:

img.png

图中 CarFactory 把 Setter 注入的代码删除了; 其实是可以保留的,Setter 和 @LookUp 的工作互不干扰

  • Setter 保证 CarFactory_Bean 创建并初始化之后:car 和 niceCar 有初始值 【Setter 注入后不再被更新】

  • 而 LookUp 保证 produceCar() 和 produceNiceCar() 每次从 Java 堆得到一个全新的 Car_Bean 或者 NiceCar_Bean

图中绿色线条标记的是,"多例" bean, 且都是以最致命的 "构造函数注入"。

实际上,多例 bean想要生效,需要使用 @LookUp 注解实时读取,但是这里为了便于演示功能的复杂性,特意使用了最致命的场景。

所谓致命,就是程序无法启动,或者 bean 访问时报错。

演示代码地址:

https://gitee.com/bokerr/spring-cycle-example.git

posted @ 2023-06-24 10:04  bokerr  阅读(41)  评论(0编辑  收藏  举报