Spring 之循环依赖

说一下Spring中是怎么解决循环依赖的

1、实例化A,A添加到三级缓存。
2、A属性赋值,赋值B对象。
3、从一级缓存中获取 B,获取不到,会实例化 B。
4、B添加到三级缓存。
5、B属性赋值,赋值A对象,依次从一级缓存到三级缓存获取B,最终从三级缓存获取到A,然后将A放到二级缓存。
6、B初始化完毕,将B放入一级缓存,并且从二级三级缓存中移除。B创建完毕,赋值给A。
7、A初始化完毕,将A放入一级缓存,并且从二级三级缓存中移除。
8、A创建完毕。

流程图:
在这里插入图片描述

为什么通过构造器无法解决循环依赖?
因为Spring解决循环依赖是通过Bean的中间态完成的,这个中间态是已经实例化但尚未初始化的状态,也就是一个半成品。
如果实例化的过程中通过构造器创建,无法将一个对象提前暴露出来,因为这时还未创建好,所以构造器无法解决循环依赖。

二级缓存能否解决循环依赖?
通过上面的分析,其实把二级缓存拿掉,在 B 尝试获取 A 的时候直接返回 A 的实例,其实也是可以的。

但是为什么还是用三级缓存呢?
主要为了解决动态代理场景下的循环依赖。
如果去掉三级缓存:在创建代理对象的时候对Bean生命周期有影响。
如果去掉二级缓存:如果A依赖B和C,切B和C也依赖A,三级缓存的getObject()返回的是代理对象,会导致B和C依赖了不同的A。
如果只保留一级缓存呢:如果A依赖B,在实例化B的时候,从一级缓存是获取不到A的,因为这时A还未初始化完毕。

什么是循环依赖

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。
在这里插入图片描述

Spring怎么解决循环依赖

Spring的单例对象的初始化主要分为三步:
1、createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象;
2、populateBean:填充属性,这一步主要是对bean的依赖属性进行填充;
3、initializeBean:调用spring xml中的init 方法;

Spring的循环依赖的理论依据其实是基于Java的引用传递,当我们获取到对象的引用时,对象的field或则属性是可以延后设置的(但是构造器必须是在获取引用之前)。

构造注入解决不了: 因为构造方法创建实例,每次都要new一个要构造的实例bean,而A创建时,依赖B,就去创建B,B又依赖了A,继续构造A,如此循环下去 A(B) B(A) A(B)->…,就会陷入一个死循环。

设值注入: 使用三级缓存来解决循环依赖问题;构造的对象使用无参构造方法,会把A先放入二级缓存(前置也会把A的工厂方法放到三级缓存),后面B,会从二级缓存中获取A(中间态)完成属性设置,B就直接进入了一级缓存中,递归回A,最后也进入一级缓存。

Spring为了解决单例的循环依赖问题,使用了三级缓存:

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

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

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

singletonObjects:一级放成品,单例对象的cache
earlySingletonObjects :二级是中间态,提前曝光的单例对象的Cache
singletonFactories : 三级是单例对象工厂的cache

初始化A对象实例流程:
首先Spring尝试通过ApplicationContext.getBean()方法获取A对象的实例,由于Spring容器中还没有A对象实例,因而其会创建一个A对象。

然后发现其依赖了B对象,因而会尝试递归的通过ApplicationContext.getBean()方法获取B对象的实例。

但是Spring容器中此时也没有B对象的实例,因而其还是会先创建一个B对象的实例。

需要注意这个时间点,此时A对象和B对象都已经创建了,并且保存在Spring容器中了,只不过A对象的属性b和B对象的属性a都还没有设置进去。

在前面Spring创建B对象之后,Spring发现B对象依赖了属性A,因而还是会尝试递归的调用ApplicationContext.getBean()方法获取A对象的实例。

因为Spring中已经有一个A对象的实例,虽然只是半成品(其属性b还未初始化),但其也还是目标bean,因而会将该A对象的实例返回。

此时,B对象的属性a就设置进去了,然后还是ApplicationContext.getBean()方法递归的返回,也就是将B对象的实例返回,此时就会将该实例设置到A对象的属性b中。

这个时候,注意A对象的属性b和B对象的属性a都已经设置了目标对象的实例了

可能会比较疑惑的是,前面在为对象B设置属性a的时候,这个A类型属性还是个半成品。但是需要注意的是,这个A是一个引用,其本质上还是最开始就实例化的A对象。

而在上面这个递归过程的最后,Spring将获取到的B对象实例设置到了A对象的属性b中了

这里的A对象其实和前面设置到实例B中的半成品A对象是同一个对象,其引用地址是同一个,这里为A对象的b属性设置了值,其实也就是为那个半成品的a属性设置了值。

图解解决循环依赖

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

参考:
高频面试题:Spring 如何解决循环依赖?
Spring-bean的循环依赖以及解决方式

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值