type
status
date
slug
summary
tags
category
icon
password

循环依赖是什么

既然要解决循环依赖,那么就要知道循环依赖是什么。如下图所示:
notion image
通过上图,我们可以看出:
  • A 依赖于 B
  • B 依赖于 C
  • C 依赖于 A
这种依赖关系形成了一种闭环,从而造成了循环依赖的局面。
下面是未解决循环依赖的常规步骤:
  1. 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行。
  1. A 对象发现需要注入 B 对象,但是容器中并没有 B 对象(如果对象创建完成并且属性注入完成和执行完初始化方法就会放入容器中)。
  1. 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行。
  1. B 对象发现需要注入 C 对象,但是容器中并没有 C 对象。
  1. 实例化 C,此时 C 还未完成属性填充和初始化方法(@PostConstruct)的执行。
  1. C 对象发现需要注入 A 对象,但是容器中并没有 A 对象。

Spring如何解决循环依赖

先说结论:Spring 解决循环依赖有两种方式
  • 使用 Autowired 注解。
  • 使用 @Lazy 注解。
首先我们要了解,在 Spring 中依赖注入主要有三种方式:
  • 构造器注入
  • field 注入
  • setter 注入
而循环依赖问题就发生在依赖注入的时候。

使用 Autowired 注解

使用 Autowired 注解可以解决 field 注入或者 setter 注入的单例对象的循环依赖问题 。对注入的属性值或者 setter 方法加上 Autowired 注解,Spring 会自动解决循环依赖的问题。原理参考下面要介绍的三级缓存的原理
注意三级缓存并不能解决以下三种情况的循环依赖。这也是面试常问的重点。
  1. 构造器注入循环依赖。
结果:项目启动失败抛出异常BeanCurrentlyInCreationException
原因分析:Spring 使用三级缓存解决循环依赖依靠的是 Bean 的“中间态”这个概念,而这个中间态指的是已经实例化,但还没初始化的状态。而构造器是完成实例化的东东,所以构造器的循环依赖无法解决。
 
  1. prototype 类型对象的循环依赖(无论是 field 注入还是 setter 注入)
循环依赖双方 scope 都是 prototype 的话,也会循环依赖失败。prototype在平时使用情况较少,但是也并不是不会使用到,因此此种方式也需要引起重视。
结果:需要注意的是本例中启动时是不会报错的(因为非单例Bean默认不会初始化,而是使用时才会初始化),所以很简单咱们只需要手动getBean()或者在一个单例Bean内@Autowired一下它即可
这样子启动就报错:
原因分析:scope 为 prototype 意思就是说这个 Bean 每次需要的时候都现场创建,不用三级缓存里的。每次创建的时候必然就会发生互相依赖的问题。
 
  1. @Async 产生的循环依赖。
启动报错
原因分析:在 BService 中注入了 AService 的原始对象,但是 AService 在后续的处理流程中被 AOP 代理了,产生了新的对象,导致 BService 中的 AService 并不是最终的 AService,所以就出错了!详细原因参考
@Async循环依赖的根本原因
如下两个前置知识大家先理解一下:
第一:
其实大部分的 AOP 循环依赖是没有问题的,这个 @Async 只是一个特例,特别在哪里呢?一般的 AOP 都是由 AbstractAutoProxyCreator 这个后置处理器来处理的,通过这个后置处理器生成代理对象,AbstractAutoProxyCreator 后置处理器是 SmartInstantiationAwareBeanPostProcessor 接口的子类,并且 AbstractAutoProxyCreator 后置处理器重写了 SmartInstantiationAwareBeanPostProcessor 接口的 getEarlyBeanReference 方法;而 @Async 是由 AsyncAnnotationBeanPostProcessor 来生成代理对象的,AsyncAnnotationBeanPostProcessor 也是 SmartInstantiationAwareBeanPostProcessor 的子类,但是却没有重写 getEarlyBeanReference 方法,默认情况下,getEarlyBeanReference 方法就是将传进来的 Bean 原封不动的返回去。
第二:
在 Bean 初始化的时候,Bean 创建完成后,后面会执行两个方法:
  • populateBean:这个方法是用来做属性填充的。
  • initializeBean:这个方法是用来初始化 Bean 的实例,执行工厂回调、init 方法以及各种 BeanPostProcessor。
 
大家先把这两点搞清楚,然后我来跟大家说上面代码的执行流程。
  1. 首先 AService 初始化,初始化完成之后,存入到三级缓存中。
  1. 执行 populateBean 方法进行 AService 的属性填充,填充时发现需要用到 BService,于是就去初始化 BService。
  1. 初始化 BService 发现需要用到 AService,于是就去缓存池中找,找到之后拿来用,但是!!!这里找到的 AService 不是代理对象,而是原始对象。因为在三级缓存中保存的 AService 的那个 ObjectFactory 工厂,在对 AService 进行提前 AOP 的时候,执行的是 SmartInstantiationAwareBeanPostProcessor 类型的后置处理器 中的 getEarlyBeanReference 方法,如果是普通的 AOP,调用 getEarlyBeanReference 方法最终会触发提前 AOP,但是,这里执行的是 AsyncAnnotationBeanPostProcessor 中的 getEarlyBeanReference 方法,该方法只是返回了原始的 Bean,并未做任何额外处理。
  1. 当 BService 创建完成后,AService 继续初始化,继续执行 initializeBean 方法。
  1. 在 initializeBean 方法中,执行其他的各种后置处理器,包括 AsyncAnnotationBeanPostProcessor,此时调用的是 AsyncAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法,在该方法中为 AService 生成了代理对象。
  1. 在 initializeBean 方法执行完成之后,AService 会继续去检查最终的 Bean 是不是还是一开始的 Bean,如果不是,就去检查当前 Bean 有没有被其他 Bean 引用过,如果被引用过,就会抛出来异常,也就是上图大家看到的异常信息。

使用 @Lazy 注解

在 Spring Boot 2.6.x版本以上默认禁用循环依赖,在不开启循环依赖的情况下,可以使用 @Lazy 注解搭配 @Autowired 解决循环依赖的问题。
Spring 官方不推荐开发者使用循环依赖。如果一定要使用循环依赖,需要手动设置配置参数 spring.main.allow-circular-references=true
这里 @Lazy 只要一个其实就能解决问题,也可以两个都添加。使用 @Lazy 注解可以解决上面三级缓存无法解决的三个循环依赖问题。具体原理参考下面的@Lazy 注解的原理
解决 prototype 类型对象的循环依赖问题
解决 @Async 的循环依赖问题

三级缓存的原理

一句话总结:三级缓存解决循环依赖的核心就是提前暴露对象,而提前暴露的对象就是放置于第二级缓存中。三级缓存是放在 DefaultSingletonBeanRegistry 里面的。
名称
描述
singletonObjects
一级缓存,存放完整的 Bean。所有被 Spring 管理的 Bean,最终都会存放在 singletonObjects 中,这里面存放的 Bean 是经历了所有生命周期的(除了销毁的生命周期),完整的,可以给用户使用的。
earlySingletonObjects
二级缓存,存放提前暴露的Bean,Bean 是不完整的,未完成属性注入和执行 init 方法。
singletonFactories
三级缓存,存放的是 Bean 工厂,主要是生产 Bean,存放到二级缓存中,然后并且删除三级缓存中的工厂对象。
Bean 都已经实例化了,为什么还需要一个生产 Bean 的工厂呢?
这里实际上是跟 AOP 有关,如果项目中不需要为 Bean 进行代理,那么这个 Bean 工厂就会直接返回一开始实例化的对象,如果需要使用 AOP 进行代理,那么这个工厂就会发挥重要的作用了,这也是本文需要重点关注的问题之一。

三级缓存如何解决循环依赖

Spring 是如何通过上面介绍的三级缓存来解决循环依赖的呢?这里只用 A,B 形成的循环依赖来举例:
  1. 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行,A 只是一个半成品。
  1. 为 A 创建一个 Bean 工厂,并放入到 singletonFactories 中。
  1. 发现 A 需要注入 B 对象,但是一级、二级、三级缓存均为发现对象 B。
  1. 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行,B 只是一个半成品。
  1. 为 B 创建一个 Bean 工厂,并放入到 singletonFactories 中。
  1. 发现 B 需要注入 A 对象,此时在一级、二级未发现对象 A,但是在三级缓存中发现了对象 A,从三级缓存中得到对象 A,并将对象 A 放入二级缓存中,同时删除三级缓存中的对象 A。(注意,此时的 A 还是一个半成品,并没有完成属性填充和执行初始化方法)
  1. 将对象 A 注入到对象 B 中。
  1. 对象 B 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 B。(此时对象 B 已经是一个成品)
  1. 对象 A 得到对象 B,将对象 B 注入到对象 A 中。(对象 A 得到的是一个完整的对象 B)
  1. 对象 A 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 A。
我们从源码中来分析整个过程:
创建 Bean 的方法在 AbstractAutowireCapableBeanFactory::doCreateBean()
添加三级缓存的方法如下:
通过这段代码,我们可以知道 Spring 在实例化对象的之后,就会为其创建一个 Bean 工厂,并将此工厂加入到三级缓存中。
因此,Spring 一开始提前暴露的并不是实例化的 Bean,而是将 Bean 包装起来的 ObjectFactory。为什么要这么做呢?
这实际上涉及到 AOP,如果创建的 Bean 是有代理的,那么注入的就应该是代理 Bean,而不是原始的 Bean。但是 Spring 一开始并不知道 Bean 是否会有循环依赖,通常情况下(没有循环依赖的情况下),Spring 都会在完成填充属性,并且执行完初始化方法之后再为其创建代理。但是,如果出现了循环依赖的话,Spring 就不得不为其提前创建代理对象,否则注入的就是一个原始对象,而不是代理对象。因此,这里就涉及到应该在哪里提前创建代理对象?
Spring 的做法就是在 ObjectFactory 中去提前创建代理对象。它会执行 getObject() 方法来获取到 Bean。实际上,它真正执行的方法如下:
因为提前进行了代理,避免对后面重复创建代理对象,会在 earlyProxyReferences 中记录已被代理的对象。
通过上面的解析,我们可以知道 Spring 需要三级缓存的目的是为了在没有循环依赖的情况下,延迟代理对象的创建,使 Bean 的创建符合 Spring 的设计原则。
我们目前已经知道了 Spring 的三级依赖的作用,但是 Spring 在注入属性的时候是如何去获取依赖的呢?他是通过一个 getSingleton() 方法去获取所需要的 Bean 的。
当 Spring 为某个 Bean 填充属性的时候,它首先会寻找需要注入对象的名称,然后依次执行 getSingleton() 方法得到所需注入的对象,而获取对象的过程就是先从一级缓存中获取,一级缓存中没有就从二级缓存中获取,二级缓存中没有就从三级缓存中获取,如果三级缓存中也没有,那么就会去执行 doCreateBean() 方法创建这个 Bean。

思考:一定需要三个级别的缓存才能解决循环依赖吗?

一级缓存是保存成品 Bean 供容器调用,肯定是要保留的,那么单独去掉二级或者三级缓存可以解决循环依赖吗。

只留一级和二级缓存 ✅

在没有 AOP 的情况下,只留一二级缓存可以解决循环依赖
我们已经知道,第三级缓存的目的是为了延迟代理对象的创建,因为如果没有依赖循环的话,那么就不需要为其提前创建代理,可以将它延迟到初始化完成之后再创建。
既然目的只是延迟的话,那么我们是不是可以不延迟创建,而是在实例化完成之后,就为其创建代理对象,这样我们就不需要第三级缓存了。因此,我们可以将addSingletonFactory() 方法进行改造。
这样的话,每次实例化完 Bean 之后就直接去创建代理对象,并添加到二级缓存中。测试结果是完全正常的Spring 的初始化时间应该也是不会有太大的影响,因为如果 Bean 本身不需要代理的话,是直接返回原始 Bean 的,并不需要走复杂的创建代理 Bean 的流程。

只留一级和三级缓存❌

只留一三级缓存,会导致注入的 Bean 和保存到一级缓存中的 Bean 不是同一个对象的问题
例如 Aoo 和 Boo 两个类循环依赖。
  1. Spring 启动
  1. 从一级缓存和三级缓存中获取 Aoo,缓存中没有,则创建
  1. 创建 Aoo 的空壳对象,并把它和工厂对象放入三级缓存中。
  1. 对 Aoo 进行属性注入,发现 Boo 即不在一级缓存,也不在三级缓存。只能创建了
  1. 创建 Boo 对象
  1. 对 Boo 进行属性注入,发现三级缓存中有 Aoo 对象,直接从三级缓存中获取。
  1. Boo 对象属性装配完成,把它从三级缓存移到一级缓存。
  1. Aoo 对象属性装配完成,此时从三级缓存中移到一级缓存。
乍一看没啥问题是不是,其实不是的。问题出在第 6 步,和第 8 步。通过前面的讲解,一定要了解到,三级缓存中每次返回的对象都不一样。所以第 6 步和第 8 步如果都从三级缓存中获取 Aoo 对象, 这两步中的 Aoo 对象不是同一个,Spring 中的 Aoo 对象和 Boo 对象就会使这个样子.
notion image

结论

  1. 第二级缓存不能去掉。因为三级缓存中的工厂每次 getObject 方法返回的实例不是同一个对象,所以需要二级缓存来缓存一下三级缓存生成的 bean,这样就保证了两个类的属性是环形依赖,不会破坏循环依赖。
  1. 第三级缓存在没有 AOP 的情况下可以去掉。如果在有 AOP 的情况还坚持选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。为了不违背设计原则 Spring 选择了三级缓存。

@Lazy 注解的原理

一句话总结:@Lazy 注解是通过建立一个中间代理层,来破解循环依赖的。
// todo

参考

Spring源码:ApplicationContext启动流程Jackson使用详解
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏