vlambda博客
学习文章列表

从spring源码层面分析循环依赖解决方案的实现原理

A对象中有b属性,B对象中有a属性。

spring对象默认是单例的,在spring容器中,所有对象有且仅有一个。

假设先创建a对象,意味着在创建a的过程中需要去设置属性b,检索一下有没有b属性,如果没有b属性的话,那就需要创建b对象了,而创建b对象的时候,紧跟着就会有一个属性a的设置,又要去看看有没有对象a,所以这样的情况就造成了循环依赖。

而要解决循环依赖问题,需要深刻的认识bean的生命周期。

粗力度的划分bean生命周期4个阶段:

从spring源码层面分析循环依赖解决方案的实现原理
  • • 实例化 在堆中申请开辟一块空间

  • • 初始化 给对象的属性进行赋值,以及某些额外的扩展工作

  • • 使用

  • • 销毁

本文的目标是解决循环依赖问题,所以重点聊下实例化和初始化。

上文说到spring容器中有一级缓存、二级缓存、三级缓存,而循环依赖必须使用三级缓存,为什么不能使用一级缓存和二级缓存?

创建A对象要包含几个环节

从spring源码层面分析循环依赖解决方案的实现原理

假设先创建A对象-->先实例化A对象,b属性为null-->给A对象的b属性赋值-->判断容器中是否有B对象,如果有直接赋值,如果没有则创建B对象-->实例化B对象,a=null-->给B对象的a属性赋值-->判断容器中是否有A对象,如果有直接赋值,如果没有,则创建A对象。

从上图可以看出,这是一个死循环、闭环。

而要解开这个闭环需要思考的维度是在哪个地方是最后一个步骤落成了闭环--就是图中标记1的地方。

思考:如果把这一步去掉,那么就不存在闭环,能否把这一步去掉?

而出现这一步的前提是上一步:判断容器中是否有A对象。在整个对象的创建过程中到底有没有a对象,答案是有的,在刚开始创建的时候其实已经把a对象创建出来了,只是此时的a对象不是完成的对象即图中标记2。

对象有2个状态

  • • 半成品对象

完成实例化未完成初始化

  • • 成品对象

完成实例化且完成初始化

图中标记2处的a对象是有的,只是还处于半成品的状态。

如果持有某一个对象的引用,那么能否在后续步骤的时候对该对象进行赋值操作?

把刚刚的步骤重新梳理一遍:

刚开始先创建a对象,创建完之后,实例化a对象,其中b属性为null, 将标记1处的半成品的a对象放入一个map集合中去。

从spring源码层面分析循环依赖解决方案的实现原理

key为A,value是A半成品对象。

接下来给A对象中的b属性赋值,判断当前缓存中是否有b对象,因此时的map中是不包含b对象的,所以去创建B对象,然后实例化B对象,a属性为null,此时将半成品状态的B对象放到map集合中,对应图标记3处。

从spring源码层面分析循环依赖解决方案的实现原理

然后给B对象中的a属性赋值,判断容器中是否有A对象,如果可以从ma集合中获取到,那么标记1处的那条线就没有必要存在了。

因此时map集合中是有A对象的,所以取出a对象给b中的a属性赋值。

从spring源码层面分析循环依赖解决方案的实现原理

此时b对象就是成品对象了,把它放到map集合中。

创建B对象的原因是为了给a中的b属性赋值,现在b对象创建完了并且变成了成品对象,那么就可以给a对象中的b属性赋值了。

赋值之后,a就变成成品对象了,把它放入map集合中。

从spring源码层面分析循环依赖解决方案的实现原理

此时对象都创建完成了,也没有出现闭环问题。

spring解决循环依赖最本质的点在于实例化和初始化是分开执行的。

再思考一个问题:

上面的过程是将对象的半成品和成品都放在了一个map集合中了,那将不同类的对象放到不同的map集合里面,一个放半成品,一个放成品即分别对应一级缓存和二级缓存,那么是否完全不需要三级缓存了?

这个就是接下来要阐述的为什么一定要使用三级缓存来解决循环依赖问题。

从spring源码层面分析循环依赖解决方案的实现原理

上图两种不同颜色分别代表创建a对象和b对象的过程,执行步骤都是差不多的。

spring容器中的一级、二级、三级缓存对应的是哪个map集合?

从spring源码层面分析循环依赖解决方案的实现原理
  • • singletonObjects 一级缓存

  • • earlySingletonObjects 二级缓存

  • • singletonFactories 三级缓存

一级缓存和二级缓存是线程安全的ConcurrentHashMap,三级缓存是线程不安全的HashMap;一级缓存容量更大一些;一级和二级缓存放的是object对象,三级缓存放的是ObjectFactory。

从spring源码层面分析循环依赖解决方案的实现原理

ObjectFactory是函数式接口,有且仅有一个方法,可以当作方法的数传入进去。当指明此类型参数的方法,可以传入一个lambda表达方式,在执行的时候并不会执行lambda表达式,而在调用getObject方法的时候才会调用lamdba处理的逻辑,这是利用的函数式编程的思想。

从spring源码层面分析循环依赖解决方案的实现原理

最开始的时候这些缓存都是空的。

接下来咱们debug源码。

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

在xml中定义2个bean

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

执行finishBeanFactoryInitialization

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

执行preInstantiateSingletons

preInstantiateSingletons方法中的

List<String> beanNames = new ArrayList<>(this.beanDefinitionNames); 

里面就包含xml定义的a和b这两个bean。

从spring源码层面分析循环依赖解决方案的实现原理

a和b没有明显的创建顺序。

在创建对象的整个过程中有6个重要且标准的方法。

从spring源码层面分析循环依赖解决方案的实现原理
创建bean的关键方法

进入第一个方法getBean

从spring源码层面分析循环依赖解决方案的实现原理

进入第二个方法doGetBean

从spring源码层面分析循环依赖解决方案的实现原理

以do开始的方法基本上是实际干活的方法,包含了非常多的真实的处理逻辑。

创建任何对象之前都要先做一个预判,判断容器里面是否有该对象。

从spring源码层面分析循环依赖解决方案的实现原理

查询容器中是否包含a对象,返回null,说明容器中还没有,所以只能创建。

然后进入第三个方法createBean

从spring源码层面分析循环依赖解决方案的实现原理

这段代码就是ObjectFactory,lambda表达式就是ObjectFactory的调用过程。

createBean并不会真正的执行lambda表达式,而是在调用getObject方法的时候才会执行lambda表达式。

在getSingleton方法中会调用getObject方法

从spring源码层面分析循环依赖解决方案的实现原理

然后执行createBean方法,接着调用doCreateBean方法。

从spring源码层面分析循环依赖解决方案的实现原理

找到第五个方法doCreateBean中调用createBeanInstance

从spring源码层面分析循环依赖解决方案的实现原理

debug可以看到此时已经获取到了A@3006对象,其中的属性b为null

从spring源码层面分析循环依赖解决方案的实现原理

目前的阶段相当于实例化了A即如图标记2处

从spring源码层面分析循环依赖解决方案的实现原理

找到第六个方法doCreateBean中调用populateBean,该方法用于填充对象属性

但在之前有这样一个逻辑

从spring源码层面分析循环依赖解决方案的实现原理

第一个参数是bean名称,第二个参数是lamdba表达式,这个lambda表达式是解决循环依赖的关键

从spring源码层面分析循环依赖解决方案的实现原理

如果一级缓存不存在该对象,则将key=a,value=lambda表达式放入三级缓存。

在对象属性设置之前,已经把K=a,value=lambda表达式放入了三级缓存,注意这里放入的并不是a对象,而是包含a对象的lambda表达式。

接下来就是填充对象属性,给属性赋值。

从spring源码层面分析循环依赖解决方案的实现原理

接着populateBean方法调用applyPropertyValues进行属性赋值操作。

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

pv是属性值的集合,只有一个属性b,value值是RuntimeBeanReference类型的对象,

从spring源码层面分析循环依赖解决方案的实现原理

解析b对象。

从spring源码层面分析循环依赖解决方案的实现原理

这是第二次看到getBean了。

本来是要给a中的b属性赋值的,通过名称判断之后,发现没有b对象,所以要去容器中查找b对象即对应到标红的这一步。

从spring源码层面分析循环依赖解决方案的实现原理

然后又经过上述一系列的步骤,实例化了B对象,属性a为null。

然后接着把key=b,value=lambda表达式放入三级缓存,然后给b的属性值a赋值

从spring源码层面分析循环依赖解决方案的实现原理

这是第三次遇到getBean了,对应到图中标红处。

从spring源码层面分析循环依赖解决方案的实现原理

此时缓存集合情况

从spring源码层面分析循环依赖解决方案的实现原理

三级缓存中包含A和B的2个lambda表达式,A和B对象还没有放入缓存中。

从容器中查询a

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

一级缓存和二级缓存都没有,三级缓存中虽然没有a对象,但是有ObjectFactory。执行 singletonFactory.getObject()实际上调用的是lambda表达式getEarlyBeanReference(beanName, mbd, bean)。

从spring源码层面分析循环依赖解决方案的实现原理

如果有代理对象,则返回代理对象,如果没有代理对象,则返回原始对象。

因为xml中并没有给a对象配置aop代理,所以这里返回的是a的原始对象。

从spring源码层面分析循环依赖解决方案的实现原理

查看getEarlyBeanReference方法可以看到AOP代理的两种实现方式。

从spring源码层面分析循环依赖解决方案的实现原理

接着继续

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

然后将a对象放入二级缓存

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

此时从容器中获取到了a对象,但是b的属性还是null,所以a对象还是半成品。

从spring源码层面分析循环依赖解决方案的实现原理

然后把三级缓存的a的ObjectFactory删除。

从spring源码层面分析循环依赖解决方案的实现原理

为什么删除三级缓存a的objectFactory,因为查找顺序是先查一级缓存,再查二级缓存,最后查三级缓存,二级缓存已经有了a对象,所以三级缓存内容就没有留着的必要了,这也是三级缓存容量比较小的原因。

获取a对象的目的是给b对象中的a属性赋值。

从spring源码层面分析循环依赖解决方案的实现原理

此时就完成了B对象a属性的赋值,此时B对象是成品对象了。

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

将B成品对象放入一级缓存

从spring源码层面分析循环依赖解决方案的实现原理

并在二级缓存和三级缓存中清空B对象的数据。

从spring源码层面分析循环依赖解决方案的实现原理

为什么获取B对象,目标是给A对象的b属性赋值。

从spring源码层面分析循环依赖解决方案的实现原理

此时a和b都是成品对象了。

从spring源码层面分析循环依赖解决方案的实现原理

把a放入一级缓存,清空二级缓存和三级缓存的a相关的数据。

从spring源码层面分析循环依赖解决方案的实现原理

为什么要创建a,因为在刚开始的循环结构中,先处理的a,接下来再处理b。

从spring源码层面分析循环依赖解决方案的实现原理

从一级缓存中是可以找到b的

从spring源码层面分析循环依赖解决方案的实现原理

那么就不需要createBean了,整个逻辑才算结束了。

总结

1、三个map结构中分别存储了什么对象?

一级缓存:成品对象;二级缓存:半成品对象;三级缓存:lambda表达式(getEarlyReference)

2、三个map缓存的查找对象的顺序?

先找一级缓存,查不到,再查二级缓存,再找不到,找三级缓存。

3、如果只有一个map,能否解决循环依赖问题?

不能,如果只有一个map,那么成品对象和半成品对象只能放入一个map中,而半成品对象是不能暴露给外部使用的,所以必须要做区分,否则就有可能暴露半成品对象。

有人可能说添加一个标识,0=半成品、1=成品,每次取的时候都要判断下这个标识别,很麻烦。

4、如果只有两个map,能否解决循环依赖问题?

能,但有前提条件。

修改源码1

把createBean方法中的

this.addSingletonFactory(beanName, () -> {
                return this.getEarlyBeanReference(beanName, mbd, bean);
            });

注释掉,然后添加,earlyearlySingletonObjects.put(beanName,bean)

即不往三级缓存中放入ObjectFactory对应的lambda表达式,而是往二级haunch中放入bean对象。

修改源码2

把getSingleton方法中的

从spring源码层面分析循环依赖解决方案的实现原理

这一部分,换成

singletonObject=this.earlySingletonObjects.get(beanName);
return singletonObject;

即不从三级缓存中获取,而是直接从二级缓存获取。

运行下发现并没有报错

从spring源码层面分析循环依赖解决方案的实现原理

给a对象添加一个aop代理

从spring源码层面分析循环依赖解决方案的实现原理

再来运行代码,就会报错:other beans do not use the final version of the bean即其他bean不能使用最终版本的bean。

所以只要不包含aop就可以使用二级缓存解决循环依赖问题,但是出现aop之后,就必须要使用三级缓存了。

5、为什么三级缓存就可以解决循环依赖中包含的代理对象问题呢?

(1)创建代理对象的时候是否需要创建出原始对象?

在没有生成代理对象之前就可以生成原始对象了

从spring源码层面分析循环依赖解决方案的实现原理

在doCreateBean方法中

从spring源码层面分析循环依赖解决方案的实现原理

接着往后看

从spring源码层面分析循环依赖解决方案的实现原理
从spring源码层面分析循环依赖解决方案的实现原理

这个方法是创建代理对象的。

(2)同一个容器中能否出现两个不同的对象?不能,对象名都叫a,要么是原始对象要么是代理对象,不能同时出现。

(3)如果一个对象被代理,那么原始对象和和代理对象应该这么去存储?

如果需要代理对象,那么代理对象创建完之后应该覆盖原始对象。

在getEarlyBeanReference方法中,会判断是否需要代理对象,如果创建出了代理对象,就需要覆盖原始对象。

(4)在对象对外暴露的时候,容器怎么知道什么时候需要被暴露呢?或者说在对象对外暴露的时候,如何准确的给出原始对象或代理对象?

因为正常的代理对象的创建是在BeanPostProcessor的后置方法中,在解决循环依赖问题的时候还没有执行到那个地方,所以此时就需要lambda表达式了,类似于一种回调机制,在确定要对外暴露的时候,就唯一性确定到底是代理对象还是原始对象,这也是什么不把对象直接放入二级缓存中,而通过三级缓存lambda表达式的方式来执行的原因。

从spring源码层面分析循环依赖解决方案的实现原理

所以这里放一个lambda表达式,如果这个对象在整体的调用链处理逻辑中,需要变成一个属性,给它引用出去的时候,就去执行lambda表达式。

从spring源码层面分析循环依赖解决方案的实现原理

在lambda表达式里面预先做一个处理逻辑的判断,判断一下是否需要进行代理对象的创建,如果需要则创建代理对象。

从spring源码层面分析循环依赖解决方案的实现原理

addSingletonFactory这里把lambda表达式放入三级缓存,该表达式的内容是判断是否需要代理对象,若需要则创建,这里并没有真正的去执行该表达式。

initializeBean这个方法中的applyBeanPostProcessorsBeforeInitialization方法才是真正的通过后置方法beanPostProcessor创建代理对象,而在处理循环依赖的时候并没有执行到这一步呢。

程序没有办法去确定你写的这个对象什么时候被其他对象调用,什么时候需要变成某一个对象的属性,所以把他换成一个lambda表达式,在确定需要对外暴露的时候才执行对象的确定(原始还是代理)。

先创建a对象,再创建b对象,需要给b中的a属性赋值,从三级缓存中找到lambda表达式,然后判断是否需要代理对象。lambda表达式相当于回调机制,并不会立刻执行,当你需要给这个属性赋值的时候,你才会去执行。

刚开始是原始对象,如果需要被代理,则返回被代理之后的对象。

某一个对象的代理只能是一个,如果是多个代理的话,就需要看代理对象的创建顺序了。

(5)spring提供了循环依赖的解决方案,那日常工作中是否也会有遇到循环依赖的问题?

spring是一个跟业务无关的技术框架,它只能预防一些问题,而不是解决所有问题,就跟我们写代码的时候的异常处理一样,你能判断到一些问题,但不是所有的异常情况你都能全部解决掉的。