spring循环依赖?三级缓存?

前文:如果你一般都没接触到循环依赖的话,你一般也就不会遇到三级缓存,你甚至可以不用来看这篇文章。(滑稽~)
循环依赖本就是一种不好的行为,而三级缓存则是这种不良行为的解决方案。spring表示“我命苦啊,我还得为开发人员的错误来买单”

  此外,本文尽量跳出源码和以精简的形式来给大家讲解,避免一些人看到源码或长篇大论就润了。
如果你选择了看这篇文章,请静下心来,认真的看。我相信读完这篇文章,你能够从中有所收获!

一、Bean生命周期

上图,发车!

不是吧,一上来就这么顶!我不看了!
别走,这里只是为了知识的完整性才给了那图,毕竟是说bean的生命周期嘛。在本文中你只需要明白:

  • spring首先会去扫描所需要管理的Bean;
  • 其次利用Java反射API实例化对象
  • 然后再填充bean属性

核心就是:实例化对象和填充属性拆分成两步。(至于为什么要这么做,后面会讲,不过你也可以结合标题想想)

image.png

二、循环依赖

当对象之间存在循环引用时,即称为循环依赖。如下图:

image.png

在下文中,将UserServiceImpl 简称为U,EmailServiceImpl简称为E。
如图中所示,U依赖E,而E依赖U。那创建过程是这样子滴:

  • 实例化U后,发现它依赖E,那得去创建E
  • 实例化E后,发现它依赖U,那得去创建U
  • ……
    不知不觉,程序就陷入死循环了。

那怎么解决这问题呢?
通过后面所讲的三级缓存(说实话,我很不喜欢这名字,后面再说说)来解决这问题。
不过,核心思路就是:

  • 实例化U后,将U放入缓存中(其实就是一个Map),然后发现依赖E,就去创建E;
  • 实例化E后,将E放入缓存中,然后发现依赖U,查询缓存,发现缓存中有U,那就将U填充到E属性中;
  • E初始化完成后,就回到U填充属性操作,将U填充到E属性中,从而完成E的初始化。

大概流程如下图:

image.png

你是不是发现了这么一个问题?

好像一层缓存就可以解决循环依赖了,为什么还要搞三层?这不是闲得蛋疼吗?

是的,如果是单纯的循环依赖(不涉及代理对象),确实一层缓存就可以解决了。但如果出现了代理对象的循环依赖(即要注入的对象采用了AOP增强,需要注入的是代理对象),一层缓存可就解决不了了。

三、三级缓存

三级缓存其实就对应了三个Map:

// 一级缓存:存在初始化完毕的实例
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

// 二级缓存:存放已实例化,但还没属性填充的对象
    private final Map<String, Object> earlySingletonObjects = new HashMap(16);

// 三级缓存:存放工厂对象,可以通过getObject方法获取还没完成初始化的对象。二级缓存存放的就是调用该方法后的对象。
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);

来个灵魂拷问啊,为什么叫三级缓存?
鬼知道,大概率是国人在翻译时,看到刚好有3个map,且这些map用来存放对象,直接美名为三级缓存得了。这里无意冒犯命名者啊,只是吐吐槽。

以上面的例子结合三级缓存讲解(不看源码也行,多理解文字的意思):

(1)在实例化U时,会先查询三级缓存中是否存在该对象;

image.png

(2)由于是第一次实例化U,三级缓存中找不到该对象。故进行对象的创建,当类实例化后,会将对象封装为一个ObjectFactory,并将其放到三级缓存中;

image.png

image.png

(3)对U进行属性填充,发现U依赖E,查询三级缓存中是否存在E,由与E是第一次创建,故缓存中不存在E,得创建E对象。

image.png

(4)对E进行属性填充,发现E依赖U,查询三级缓存,发现第三层缓存中存在该对象,则从第三层缓存中获取对象,然后将获取到的对象放到第二层缓存中,最后再删除第三层缓存中相应的数据。

image.png

在这里,得重点说一下 getObject() 这个方法。
getObject()方法最终会调用getEarlyBeanReference()方法。

image.png

注意:不要硬钻这方法里每一行代码是什么意思,没必要,记住下面的话就行,这里主要是拿出来给大家看看这个方法是长什么样的。不要钻牛角尖!

getEarlyBeanReference方法逻辑如下:
如果要获取的bean被AOP代理的话则返回代理对象,如果未被代理则返回原bean实例。

这里也说明了为什么一开始要把bena封装到objectFactory中,主要是在获取bean时(即调用objectFactory.getObject()方法),如果bean是进行了AOP增强,则返回代理对象,否者返回员对象。

(5)E获取到U对象后,完成属性填充。将E对象放到一级缓存中,然后删除在其它层中的缓存。

image.png

(6)最后,回到U对象的属性填充过程,在一级缓存中获取到E对象并将其填充到U对象属性中。然后将U对象放到一级缓存中,删除在其它层中的缓存。

至此,整个循环过程结束!

四、灵魂拷问

欢迎来到这个扯皮正经的环节!

(1)在上面的讲述中,好像只用第一层缓存和第三层缓存便可以解决问题了,那为什么还要设计第二层缓存呢?

快扯,两分钟内扯不出来打PP

【答】:在涉及AOP代理对象的循环依赖中,第二层缓存是必不可少的。第二层缓存的作用是避免对一个被代理类生成多个不同的代理对象,保证始终只有一个代理对象(别忘了,我们创建的对象默认是单例的)。当我们从第三层缓存中获取到代理对象后,便放到了第二层缓存,即后续不会再调用singleFactory.getObject()方法获取新的代理对象。若没有第二层缓存,则可能发生调用多次singleFactory.getObject()方法,从而导致生成了多个代理对象。

(2)若没有第二层缓存的情况下,什么情况下会发生调用多次singleFactory.getObject()方法,从而导致生成了多个代理对象?

就硬钻牛角尖了是吧,打你哦(⊙o⊙)

【答】:在多层循环依赖的情况下会发生。
如A依赖B,B依赖A、C,C依赖A。其中,A是被AOP代理的。
过程:A完成实例化后放到缓存中,然后跑去实例化B,B从缓存中获取A的代理对象。此时,A由于还没完成属性填充,所以不会放到一级缓存中。此后跑去实例化C,然后问题就来了,C从缓存中获取到了新的A的代理对象。
上述情况的核心就是由于对象还没完成初始化,存放在三级缓存中,那此时就可能多个对象从三级缓存中获取多个不同的代理对象。

(3)换一个角度想想,采用两层缓存可不可以,从spring设计原则出发?

说实在的,我觉得这问题远没有第一个问题实在,这感觉就是纯纯的扯淡,为了回答而问,为了问而回答。我更倾向你去回答第一个问题,回答少掉第二层缓存的情况。也可能是我现在还太菜,还无法领会到这真谛

【答】:三级缓存和二级缓存的区别在于:为被代理对象创建代理的时机不同。若采用三级缓存,其流程如下:(A对应上面所说的U,B对应E,A采用了AOP增强)
image.png
在使用了三级缓存的情况下为A创建代理的时机是在B中需要注入A的时候。

若采用二级缓存,其流程如下:

image.png

可见,A实例化后就需要马上为A创建代理然后放入到二级缓存中去。
在二级缓存下,实例化完对象后,就得创建代理对象放到二级缓存中。这违背Spring的设计原则,它的设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。

总结:

由于一些不可控的情况下出现了循环引用,spring将实例化对象和属性填充划分为了两步,并通过缓存来解决了循环引用。
在循环引用的基础上,又出现了AOP代理对象的循环依赖问题,我们在填入某些对象时,需要注入代理对象。spring采用三级缓存来解决了这个问题。其中,第三层缓存是用来判断返回原型对象还是代理对象;第二层缓存则是为了避免针对某一个被代理对象生成多个不同的代理对象。


本文到此结束!

参考资料:
spring为什么使用三级缓存而不是两级?
跳出源码陷阱,Spring是怎样巧用三级缓存解决循环依赖的?
三级缓存和循环依赖(不看点源码都看不懂)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值