Spring循环依赖问题

解决思想

提供一个另外思考的视角,Spring 解决循环依赖的做法是 【图论环检测】 的经典实践。 依赖关系可以表示为一个有向图,Cat -> Dog -> Cat 的依赖关系可以表示为一个【有向图】 ,Bean是怕【顶点】, 依赖关系是【边】。Spring在createBean时会递归构建依赖图 (深度优先遍历DFS) , 如果发现当前路径中某个Bean正在创建中(即存在“正在创建”的顶点被重复访问),则判定存在环 【环检测】 。简化做法是采用二级缓存来获取 Cat 的引用 【在图中引入一个虚拟顶点】, 将环拆解为链式依赖 ,从而避免了无限递归。当所有依赖注入完成,再将真实顶点替换虚拟顶点。故视频中的loadingIoc缓存是用于存放虚拟顶点进行环检测的集合容器,ioc是存放真实顶点的集合容器。

Spring的落地实践解决循环依赖

Spring中存在三层缓存,就对于单例对象来说

  • 第一层缓存:单例对象缓存池,已经实例化并且属性赋值,这里的对象是成熟对象
  • 第二层缓存:单例对象缓存池,已经实例化但还没进行属性set,这里的对象是半成品对象
  • 第三层缓存:单例工厂的缓存,里面存放的是工厂对象,实际上是一串lambda表达式,每个工厂都可以创建对应的bean,eg:private Map<String, Supplier> singletonFactories = new HashMap<>();

    Spring生成Bean的流程

    假设A和B 发生了循环依赖

    起点:先初始化A

    1. 先往creatingSet集合(一个set集合,用于存放正在被创建的bean,便于后续判断是否发生了循环依赖)里放入A
    2. 一级缓存找不到A,那么就通过无参构造化实例化A,将和A有关的一串lambda表达式放入三级缓存(此时A的实例化和属性注入就已经分离开了),set属性注入 B
    3. creatingSet里找不到B,此时还不知道出现了循环依赖,就去一级缓存找B,找不到,就往creatingSet里找B。通过无参构造实例化B,将和B有关的一串lambda表达式放入三级缓存,进行set属性注入A
    4. 通过creatingSet发现了A,就知道发生了循环依赖,然后这时候就会去二级缓存寻找,没找到,去三级缓存找,找到有关A的lambda表达式,执行lambda
      • 如果A需要被AOP,则此时提前对其进行AOP(正常的bean周期应该是初始化后才AOP的),将AOP的代理对象放入二级缓存,并从三级缓存中删除(这样能保证你AOP出来的代理对象是单例的,也就是说你这个lambda表达式是一次性的,同时是会加锁的保证线程安全),并将代理对象放入earlyProxyReference(用于存储哪些对象提前AOP了,后续有用)
      • 如果A不需要AOP,则生成A的普通对象并放入二级缓存,并从三级缓存中删除
    5. OK此时B是已经拿到对象A了(可能是proxy的,也可能是普通的)进行set注入,进行B的其他Bean的声明周期,当Bean成熟之后放入一级缓存,移除二、三级缓存(实际上此时二级缓存中没有B,三级缓存中有B)
    6. 此时返回A的初始化过程,A就拿到了B的成熟对象,set注入A中。然后进行A的其他Bean的声明周期
    7. A的其他生命周期包括AOP(是在BeanPostProcessor的after方法执行的),但这时候A可能在lambda表达式那里就提前AOP了,这就需要通过earlyProxyReference判断A是否提前AOP了,即A存不存在。防止重复AOP
    8. 将成熟的A放入1缓存,同时删除2,3缓存(实际此时三级缓存已经没有A了,在第4步已经被移除了)

    如果A和B发生了循环依赖,然后发现A和C又发生了循环依赖

    A和B走完流程要注入C,此时二级缓存其实是有A了,C就直接从二级缓存里拿A set就好了,重复A和B的流

    各级缓存的作用:

    1. 一级缓存就是用来保存Bean单例的,也就是我们getBean的源头
    2. 二级缓存用来保证Bean是单例的。因为你三级缓存lambda是一次性消耗品,因此只会生成一份Bean。那么这个生成的半成品Bean就先放入二级缓存。用于存放提前曝光的不成熟的bean
    3. 三级缓存则是用来打破循环依赖并出现AOP的情况下的循环依赖的。当发生循环依赖时,可以保证你一定是能从三级缓存获取到Bean的(因为三级缓存都是会在Bean实例化同时存入lambda表达式的)。通过三级缓存,我们可以生成半成品的普通Bean或者proxybean。然后再放入二级缓存,然后把三级缓存删掉,这样能保证Bean是单例的

    一级缓存能解决循环依赖吗?

    可以解决普通的循环依赖。但把半成品和完全品都放入一个集合里不是很优雅。AOP的循环依赖没法解决

    可不可以使用一、二级缓存解决循环依赖?

    可以,但这样如果出现AOP的话,在对象实例化时就要完成AOP代理,生成代理对象,再进行属性注入,即先创建代理对象,再对代理对象进行属性注入,这与Spring设计理念中“将Bean的初始化和代理解耦”是不符的

    那么就可以通过三级缓存中的lambda判断,只有当出现AOP + 循环依赖的时候,才提前创建代理对象,然后进行依赖注入初始化,否则创建普通对象,走正常的Bean生命周期

    可不可以使用一、三级缓存解决循环依赖?

    不可以。因为你用三级缓存这套东西的话就说明你的bean是单例的,也就是每次产出半成品都会删除。那这

    样的话,你每次都会重新去放入lambda表达式,每次出来的半成品都不是同一个。当出现A与B发生循环依

    赖,A与C也发生循环依赖的这种情况,B中的A和C中的A就不是同一个了。必须让三级缓存是单例的,且半

    成品对象必须得找个地方存,这个地方就是二级缓存

    Spring如何解决循环依赖?

    creatingSet(判断是否存在循环依赖)+三级缓存(解决循环依赖问题)+

    earlyProxyReference(防止解决循环依赖的过程中重复AOP)

    哪些循环依赖默认情况下无法解决?

    1. 构造注入下的循环依赖,因为构造注入是要求强制注入的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Component
    class A {
    private final B b;

    @Autowired
    A(B b) { // 构造器注入
    this.b = b;
    }
    }

    如果没有 B,A a = new A(null); 都过不去(final 成员必须赋值)。

    👉 在 对象创建的一瞬间 就要保证依赖存在。

    没有任何缓冲空间,所以是 强制注入。
    1. 两个都是多例对象,因为多例对象根本就没三级缓存这一说,每次getbean都会执行一次构造方法并给属性赋值

    其它循环依赖如何解决:

    @Lazy: