浅谈 Spring 如何解决 Bean 的循环依赖问题(转载)

1. 什么是循环依赖?

通俗来讲,循环依赖指的是一个实例或多个实例存在相互依赖的关系(类之间循环嵌套引用)。

举个例子

public class AService {
    private BService bService;
}

public class BService {
    private AService aService;
}

上述例子中 AService 依赖了 BServiceBService 也依赖了 AService,这就是两个对象之间的相互依赖。当然循环依赖还包括 自身依赖、多个实例之间相互依赖

在这里插入图片描述

正常运行上面的代码调用 AService 对象并不会出现问题,也就是说普通对象就算出现循环依赖也不会存在问题,因为对象之间存在依赖关系是很常见的,那么为什么被 Spring 容器管理后的对象会出现循环依赖问题呢?

2. Spring Bean 的循环依赖问题

被 Spring 容器管理的对象叫做 Bean,为什么 Bean 会存在循环依赖问题呢?

想要了解 Bean 的循环依赖问题,首先需要了解 Bean 是如何创建的。

2.1 Bean 的创建步骤

为了能更好的展示出现循环依赖问题的环节,所以这里的 Bean 创建步骤做了简化:

1、在创建 Bean 之前,Spring 会通过扫描获取 BeanDefinition。

2、BeanDefinition就绪后会读取 BeanDefinition 中所对应的 class 来加载类。

3、实例化阶段:根据构造函数来完成实例化 (未属性注入以及初始化的对象 这里简称为 原始对象

4、属性注入阶段:对 Bean 的属性进行依赖注入 (这里就是发生循环依赖问题的环节

5、如果 Bean 的某个方法有AOP操作,则需要根据原始对象生成代理对象

6、最后把代理对象放入单例池(一级缓存singletonObjects)中。

两点说明:

上面的 Bean 创建步骤是对于 单例(singleton) 作用域的 Bean。

Spring 的 AOP 代理就是作为 BeanPostProcessor 实现的,而 BeanPostProcessor 是发生在属性注入阶段后的,所以 AOP 是在 属性注入 后执行的。

2.2 为什么 Spring Bean 会产生循环依赖问题?

通过上面的 Bean 创建步骤可知:实例化 Bean 后会进行 属性注入(依赖注入)

如上面的 AServiceBService 的依赖关系,当 AService 创建时,会先对 AService 进行实例化生成一个原始对象,然后在进行属性注入时发现了需要 BService 对应的 Bean,此时就会去为 BService 进行创建,在 BService 实例化后生成一个原始对象后进行属性注入,此时会发现也需要 AService 对应的 Bean。
在这里插入图片描述

这样就会造成 AServiceBService 的 Bean 都无法创建,就会产生 循环依赖 问题。

2.3 三大循环依赖问题场景

Spring 并不能解决所有 Bean 的循环依赖问题,接下来通过例子来看看哪些场景下的循环依赖问题是不能被解决的。循环依赖是指在应用程序中存在相互引用的对象,导致无法正确地初始化这些对象。以下是三个常见的循环依赖问题场景:

  1. 构造函数循环依赖: 当两个或多个类在它们的构造函数中相互依赖时,可能会引发构造函数循环依赖问题。例如,类A依赖于类B的实例,而同时类B也依赖于类A的实例。这种情况下,如果使用传统的依赖注入方式,很可能会导致无限循环,无法正确完成实例化。
  2. 属性循环依赖: 当两个或多个类之间存在相互依赖的属性关系时,可能会引发属性循环依赖问题。例如,类A有一个属性引用类型为类B的实例,而类B又有一个属性引用类型为类A的实例。如果没有正确处理循环依赖,可能会导致无法正确初始化属性。
  3. 静态成员变量循环依赖: 当两个或多个类之间存在相互依赖的静态成员变量时,可能会引发静态成员变量循环依赖问题。例如,类A的静态成员变量引用类型为类B的实例,而类B的静态成员变量又引用类型为类A的实例。这种情况下,无法解决循环依赖,可能导致应用程序加载和初始化时出现问题。

以上是三个常见的循环依赖问题场景。在实际开发中,为了避免循环依赖问题,可以采取一些解决方案,例如使用延迟初始化、Setter注入替代构造函数注入、引入第三方容器管理依赖关系等。具体的解决方法可以根据具体的情况和框架选择。

对应的解决方法:

  1. 构造函数循环依赖:
    • 通过构造函数注入改为使用Setter注入:将类的依赖关系改为通过Setter方法来注入,从而打破循环依赖。
    • 使用延迟初始化:延迟初始化一个或多个循环依赖的对象,可以通过懒加载或者使用代理对象实现。
  2. 属性循环依赖:
    • 使用懒加载(@Lazy):将循环依赖的属性设置为延迟加载,只有在需要的时候才进行初始化。
    • 使用中间对象:引入一个中间对象,它的责任是处理循环依赖的属性,并且在合适的时候进行初始化。
  3. 静态成员变量循环依赖:
    • 使用静态工厂方法:将实例的创建和初始化放在静态工厂方法中,由工厂方法控制依赖关系的初始化顺序。
    • 使用第三方容器:使用像Spring这样的依赖注入容器,容器可以解决循环依赖问题并正确地管理对象的生命周期。

3. Spring 如何解决循环依赖问题?

通过上文的内容能了解到 Spring 为什么会产生循环依赖问题 以及 Spring 能解决什么场景下的循环依赖问题

上文中也有提到过 Spring 是靠 三级缓存 来解决循环依赖问题的,接下来了解一下 什么是三级缓存 以及 解决循环依赖问题的具体流程

3.1 三级缓存是什么?

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

三级缓存分为:

  • 一级缓存(singletonObjects):缓存的是已经实例化、属性注入、初始化后的 Bean 对象。
  • 二级缓存(earlySingletonObjects):缓存的是实例化后,但未属性注入、初始化的 Bean对象(用于提前暴露 Bean)。
  • 三级缓存(singletonFactories):缓存的是一个 ObjectFactory,主要作用是生成原始对象进行 AOP 操作后的代理对象(这一级缓存主要用于解决 AOP 问题,后续文章中讲解)。

3.2 为什么缓存可以解决循环依赖问题?

(注意这里只是为了说明缓存可以解决循环依赖问题,但是 Spring 实际上并不是这样做的)

上文中可以看到 AServiceBService 的循环依赖问题是因为 AService的创建 需要 BService的注入BService的注入 需要 BService的创建BService的创建 需要 AService的注入AService的注入 需要 AService的创建,从而形成的环形调用。

想要打破这一环形,只需要增加一个 缓存 来存放 原始对象 即可。

在创建 AService 时,实例化后将 原始对象 存放到缓存中(提早暴露),然后依赖注入时发现需要 BService,便会去创建 BService,实例化后同样将 原始对象 存放到缓存中,然后依赖注入时发现需要 AService 便会从缓存中取出并注入,这样 BService 就完成了创建,随后 AService 也就能完成属性注入,最后也完成创建。这样就打破了环形调用,避免循环依赖问题。
在这里插入图片描述

3.3 为什么还需要第三级缓存?

通过上面的分析可以发现只需要一个存放 原始对象 的缓存就可以解决循环依赖问题,也就是说只要二级缓存(earlySingletonObjects)就够了,那么为什么 Spring 还设置了三级缓存(singletonFactories)呢?

其实 第三级缓存(singletonFactories 是为了处理 Spring 的 AOP的。

如上面的例子如果 AService 中方法没有使用 AOP 操作,会发现 BService 注入的 原始对象 与最后 AService 完成创建后的最终对象是同一个对象

如果 AService 方法中有 AOP 操作,Bean 的创建会如下图:
在这里插入图片描述

所以如果 AService 方法中有 AOP 操作时,当 AService 的原始对象赋值(注入)给 BServiceAService 会进行 AOP 操作产生一个 代理对象,这个代理对象最后会被放入单例池(一级缓存)中,也就是说此时 BService 中注入的对象是原始对象,而 AService 最终创建的完成后是代理对象,这样就会导致 BService 依赖的 AService 和 最终的 AService 不是同一个对象

出现这个问题主要是上文提到过的 AOP 是通过 BeanPostProcessor 实现的,而 BeanPostProcessor 是在 属性注入阶段后 才执行的,所以会导致注入的对象有可能和最终的对象不一致


3.4 Spring 是如何通过第三级缓存来避免 AOP 问题的?

三级缓存中存放的是 ObjectFactory 对象,那 ObjectFactory 是什么呢?

ObjectFactory 是什么?

深入源码会发现 Spring 在 doCreateBean() 方法中的 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)) 加入缓存。
在这里插入图片描述

深入 addSingletonFactory 方法:可以看到方法中的第二个参数就是 ObjectFactory 类型,并且将其添加进 三级缓存(singletonFactories 中。
在这里插入图片描述

这里放一下 ObjectFactory 类:

在这里插入图片描述

也就是说 Spring 在加入缓存时,会将 实例化后生成的原始对象 通过 lambda 表达式调用 getObject() 方法,getObject() 方法里调用 getEarlyBeanReference() 方法 来封装成 ObjectFactory 对象。

getEarlyBeanReference() 方法的作用

进入 getEarlyBeanReference() 中,会发现调用了 SmartInstantiationAwareBeanPostProcessorgetEarlyBeanReference() 方法。
在这里插入图片描述

找到 SmartInstantiationAwareBeanPostProcessor 的实现类 AbstractAutoProxyCreator 实现的 getEarlyBeanReference() 方法就可以看到其作用了。

在这里插入图片描述

  • earlyProxyReferences 存储的是 (beanName, bean) 键值对,这里的 bean 指的是原始对象(刚实例化后的对象)。
  • wrapIfNecessary() 方法用于执行 AOP 操作,生成一个代理对象(也就是说如果有 AOP 操作最后返回的是代理对象,否则返回的还是原始对象)。

Spring 真正意义上地创建 Bean 的流程

先放具体流程图:
在这里插入图片描述

要点说明:

  1. 并不是马上就执行 ObjectFactorygetEarlyBeanReference() 方法(有循环依赖时才执行)。
    • 实例化后的 Bean 会生成原始对象,然后经过 lambda 表达式封装为 ObjectFactory 对象,并且通过 addSingletonFactory() 方法将其放入 三级缓存(singletonFactories)中。
    • 但是这里执不执行 lambda 表达式中的 getEarlyBeanReference() 方法是看程序有没有调用 singletonFactories.get(beanName),只有调用了该方法(其实也就是看是否存在循环依赖需要提前获得该 Bean),才会触发执行 getEarlyBeanReference() 方法。
    • getEarlyBeanReference() 方法会根据 Bean 中是否有 AOP 操作来决定返回的是 原始对象 还是 代理对象,并且会将其上移到二级缓存中(也就是提前暴露出来让别的 Bean 使用)。
  2. 如果 Bean 中有 AOP 操作,而 AOP 操作又是在属性注入之后执行的,那么之前的 getEarlyBeanReference() 方法中执行的 AOP 操作会不会重复
    • 答案是不会,还记得 getEarlyBeanReference() 方法中的 earlyProxyReferences 吗,这个就是用来记录当前 Bean 是否已经执行 AOP 操作。
    • 当属性注入后需要执行 AOP 操作时,会先判断当前的 Bean 是否在 earlyProxyReferences 中,如果在则说明已经提前执行了 AOP 了,不用再执行了,否则就执行当前 AOP 操作。
  3. 二级缓存中的对象什么时候会上移到一级缓存?
    • 二级缓存是为了提前暴露 Bean 来解决循环依赖问题,此时的 Bean 可能还没有进行属性注入,只有等完成了属性注入、初始化后的 Bean 才会上移到一级缓存(单例池)中。
  4. 为什么可以解决 AOP 的问题?
    • 三级缓存通过利用 ObjectFactorygetEarlyBeanReference() 做到了提前执行 AOP 操作从而生成代理对象。
    • 这样在上移到二级缓存时,可以做到如果 Bean 中有 AOP 操作,那么提前暴露的对象会是 AOP 操作后返回的代理对象;如果没有 AOP 操作,那么提前暴露的对象会是原始对象。
    • 这样就能做到出现循环依赖问题时,注入依赖的对象和最终生成的对象是同一个对象。(相当于 AOP 提前在属性注入前完成,这样就不会导致后面生成的代理对象与属性注入时的对象的不一致)

所以 Spring 利用 三级缓存 巧妙地将出现 循环依赖 时的 AOP 操作 提前到了 属性注入 之前,避免了对象不一致问题。

4. 梳理 Spring 解决 Bean 的循环依赖的整个流程

还是以 AServiceBService 的循环依赖为例,完整地看看 Spring 是如何解决 Bean 的循环依赖问题。

源码分析整个流程

由于前面的内容过于繁琐,这里就以文字概括,只关注几个主要的方法:

AbstractApplicationContextrefresh() 方法出发,进入 finishBeanFactoryInitialization() 方法再进入 preInstantiateSingletons() 方法再进入 getBean() 方法再进入 doGetBean() 方法。

看看 doGetBean() 方法:
在这里插入图片描述

其中的第一个 getSingleton(beanName) 是判断 三级缓存 中是否有创建好的 Bean 对象,看看源码:
在这里插入图片描述

可以看到这里分别去每一级的缓存中取数据,依次从第一级开始取数据,如果取得到则直接返回,取不到则往下一级查找。

可以看到在第三级缓存中调用了 singletonFactories.get(beanName) 按照上文所说的会触发执行有 AOP 操作返回代理对象,没有返回原始对象,并且在这里会判断取出的数据是否存在,存在则上移到二级缓存中并删除三级缓存的数据。

如果都没有的话就会执行第二个 getSingleton() 也就是去执行 createBean() 创建一个 Bean 对象出来。

会执行 createBean() 方法中的 doCreateBean() 方法,看看源码:
在这里插入图片描述

到这里应该就一目了然了。

梳理整个流程

  1. 首先会获取 AService 对应的 Bean 对象。
  2. 先是调用 doGetBean() 中的第一个 getSingleton(beanName) 判断是否有该 Bean 的实例,有就直接返回了。(显然这里没有)
  3. 然后调用 doGetBean() 中的第二个 getSingleton() 方法来执行 doCreateBean() 方法。
  4. 先进行实例化操作(也就是利用构造函数实例化),此时实例化后生成的是原始对象。
  5. 将原始对象通过 lambda表达式 进行封装成 ObjectFactory 对象,通过 addSingletonFactory 加入三级缓存中。
  6. 然后再进行属性注入,此时发现需要注入 BService 的 Bean,会通过 doGetBean() 去获取 BService 对应的 Bean。
  7. 同样调用 doGetBean() 中的第一个 getSingleton(beanName) 判断是否有该 Bean 的实例,显然这里也是不会有 BService 的 Bean 的。
  8. 然后只能调用 doGetBean() 中的第二个 getSingleton() 方法来执行 doCreateBean() 方法来创建一个 BService 的 Bean。
  9. 同样地先进行实例化操作,生成原始对象后封装成 ObjectFactory 对象放入三级缓存中。
  10. 然后进行属性注入,此时发现需要注入 AService 的 Bean,此时调用调用 doGetBean() 中的第一个 getSingleton(beanName) 查找是否有 AService 的 Bean。此时会触发三级缓存,也就是调用 singletonFactories.get(beanName)
  11. 因为三级缓存中有 AService 的原始对象封装的 ObjectFactory 对象,所以可以获取到的代理对象或原始对象,并且上移到二级缓存中,提前暴露给 BService 调用。
  12. 所以 BService 可以完成属性注入,然后进行初始化后,将 Bean 放入一级缓存,这样 AService 也可以完成创建。

以上就是 Spring 解决 Bean 的循环依赖问题的整个流程了。

5.疑问

为啥不只用两层缓存解决问题,从第二层缓存取对象时,如果该对象需要aop,则进行aop处理

解答:

这个问题太经典了,这里我说一下我的看法:首先呢,如果使用二级缓存的话,那么我可以理解为相当于提前判断Bean是否需要AOP,需要的话直接生成代理对象放入二级缓存,等Bean创建完成后再升到一级缓存中。大概是这个思路吧,但是为什么Spring不这么设计呢,我认为有两点原因:① 由于Spring对AOP的设计思想是AOP的创建时机是在初始化后为Bean生成代理对象的,如果采用上述的设计,那么不管Bean是否存在循环依赖,都会提早生成代理对象,这样其实已经违背了Bean的设计理念。② 使用ObjectFactory对象每次触发调用lambda表达式中的 getEarlyBeanReference()生成的代理对象其实都是新创建的,也就是说如果存在两个以上的Bean循环依赖,例如 A 依赖 B 和 C,而 B 也依赖 A,C 也依赖A,并且A需要AOP,那么此时在创建 B 时触发 A 的ObjectFactory对象生成的代理对象和创建 C 时触发 A 的ObjectFactory对象生成的代理对象是不一样的,因为两次触发生成的都是新对象。这也就是为什么需要在中间设置一个二级缓存存放第一次调用创建的代理对象,这样还需要属性注入 A 的Bean就可以直接在二级缓存中拿,而不需要再次触发ObjectFactory对象。

作者:单程车票
链接:https://juejin.cn/post/7218080360403615804
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值