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


文章目录

  • Spring如何解决循环依赖问题?
  • 代码模拟
  • 图示
  • 结合Bean的生命周期分析
  • 问题解决的关键
  • 解决方案【三级缓存】
  • 详细步骤
  • 如下图所示:
  • 为什么需要三级缓存?
  • 一个场景:
  • 现在我们再来走一遍这个场景:
  • 总结:


代码模拟
@Component
class A{
    
    @Autowired
	private B b;
}

@Component
class B{
    
    @Autowired
	private A a;
}
图示

springboot单元测试启动报循环依赖 spring如何解决循环依赖问题_面试

结合Bean的生命周期分析

springboot单元测试启动报循环依赖 spring如何解决循环依赖问题_spring_02

详细的bean生命周期可参考

  1. Spring扫描class得到BeanDefinition
  2. 根据bean的definition生成bean
  3. 推断构造方法(优先选无参)
  4. 根据构造方法通过反射创建对象(放入三级缓存lamda表达式后面详细讨论)
  5. 填充对象属性(依赖注入)
  6. 如果对象的某个方法被AOP了,那么则需要根据原对象生成代理对象
  7. 把最终生成的对象放进单例池中

在Spring中,bean的生命周期导致循环依赖问题

springboot单元测试启动报循环依赖 spring如何解决循环依赖问题_面试_03

问题解决的关键

在Spring中,如果通过Set方式注入属性,【有参构造无法提前暴露对象,

不能解决循环依赖问题】那么对象的实例化和初始化就是分开的,实例化

完成的对象是不完整的对象,但是却可以之间给其他对象引用的,所以可

以在此增加一层缓存,把完成实例化但未初始化的对象提前暴露出去,让

其他对象能够进行引用,就解开了上图中的闭环问题。

springboot单元测试启动报循环依赖 spring如何解决循环依赖问题_面试_04

解决方案【三级缓存】

  • 一级缓存:singletonObjects 拥有完整生命周期的bean对象
  • 二级缓存:earlySingletonObjects 半成品对象,如果提前进行了AOP,存放的就是半成品代理对象
  • 三级缓存:singletonFactories 对象工厂,一个lamda表达式,用于回调是否生成代理对象还是原始对象

springboot单元测试启动报循环依赖 spring如何解决循环依赖问题_java_05

详细步骤

springboot单元测试启动报循环依赖 spring如何解决循环依赖问题_源码_06

第1步,反射构造A对象的时候将对象放入三级缓存,存入的是一个未执行的lambda表达式,

之后填充属性需要B,就会从三级缓存找(没有找到)------>二级缓存(没有找到)------>一级缓存(没有找到)

最终都没有找到就去创建B,开始了B的生命周期,同样B填充属性需要依赖A,就会找三级缓存(找到)

执行三级缓存中A的lambda表达式获取一个对象。

并检测如果需要进行AOP就返回一个代理对象【不完整】,反之就返回一个普通对象。

之后lambda表达式执行完三级缓存删除,放入二级缓存【没有经过完整生命周期的对象A(或代理对象A)的引用。

最后当B的生命周期结束之后返回A的生命周期,A填充属性B,判断是否已经提前进行AOP并决定是否

需要进行AOP,发现二级缓存中含有A对象(或代理对象A)就将二级缓存删除,放入一级缓存中。

如下图所示:

springboot单元测试启动报循环依赖 spring如何解决循环依赖问题_spring_07

为什么需要三级缓存?

只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean没被AOP进行切面代理,如果这个bean被AOP进行了切面代理,那么只使用两个缓存是无法解决问题,下面来看一下bean被AOP进行了切面代理的场景

三级缓存实现aop(代理对象实例化的时候,实例化对象是原始对象,若没有三级缓存,此时若根据

类名直接获取对象的话,获取的是原始对象,而我们想要的肯定是通过类名直接获取代理对象,所以

Spring在类加载过程中,直接将实例化的对象放入三级缓存中,从三级缓存中获取类对象的时候,判

断类是否被代理,若被代理则返回代理对象)

一个场景:

a对象依赖b,c对象,同时b,c依赖a对象;那么我们想一下在b对象注入a对象时没有在一级缓存里找到我

们的a的bean对象也没有在二级缓存找到我们的代理对象a,此时spring会在三级缓存找到存放着我们

的原始对象a的lambda表达式,spring执行该lambda表达式生成的代理对象a,此时b注入a完成其他步

骤正常执行,这里注意我们并没有一个完整的a的bean对象在我们的SingletonObjects里面,所以我们

的c对象在注入a对象时又会生成一个代理对象a,这个时候问题来了,代理对象a出现不一致的情况,

那么如何解决呢?这时候二级缓存的重要性体现出来了,我们将这个代理对象放earlySingletonObjects

二级缓存,key存放我们的beanName,value存放我们的代理对象。

现在我们再来走一遍这个场景:

a对象实例化好,在进行注入时发现依赖a,c两个对象,此时spring会去先创建b,c对象,b对象会去注入

a对象,会先去SingletonObjects一级缓存和earlySingletonObjects二级缓存找,发现没找到就去

SingletonFactories三级缓存,找到带有原始对象a的lambda表达式并执行该lambda表达式获得一个代

理对象a并放入二级缓存将该lambda在三级缓存删除,b对象的流程正常执行,此时c对象也需要注入

一个a对象,先去一级缓存找,没找到,再去二级缓存找找到了代理对象a,此时c对象也正常执行现

在a对象也能成功完成注入,spring的循环依赖已经解决。

总结:

  • 只有一级三级缓存,加入二级缓存可避免lambda表达式重复生成代理对象,保证单例的Spring的bean。
  • 如果只有一级二级缓存,三级缓存通过lambda表达式可灵活决定是否是需要提前进行aop并生成代理对象。
  • 如果没有循环依赖,三级缓存的lambda表达式就不会执行,二级缓存也不会使用,不影响Spring的正常bean生命周期。
  • 循环依赖两个核心
  • 单例bean
  • Set方式注入属性
  • 具体实现细节可参考Spring源码