前言
在明天,仍然有许多人对循环依赖有着争执,也有许多面试官爱问循环依赖的问题,更甚至是在Spring中只问循环依赖,在国内,这彷佛成了Spring的必学知识点,一大特色,也被泛滥人津津有味。而我认为,这称得上Spring框架里泛滥优良设计中的一点污渍,一个为不良设计而斗争的实现,要晓得,Spring整个我的项目里也没有呈现循环依赖的中央,这是因为Spring我的项目太简略了吗?恰恰相反,Spring比绝大多数我的项目要简单的多。同样,在Spring-Boot 2.6.0 Realease Note中也阐明不再默认反对循环依赖,如要反对需手动开启(以前是默认开启),但强烈建议通过批改我的项目来突破循环依赖。
本篇文章我想来分享一下对于我对循环依赖的思考,当然,在这之前,我会先带大家温故一些对于循环依赖的常识。
依赖注入
因为循环依赖是在依赖注入的过程中产生的,咱们先简略回顾一下依赖注入的过程。
案例:
@Componentpublic class Bar { }
@Componentpublic class Foo { @Autowired private Bar bar;}
@ComponentScan(basePackages = "com.my.demo")public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class); context.getBean("foo"); }}
以上为一个非常简单的Spring入门案例,其中Foo
注入了Bar
, 该注入过程产生于context.getBean("foo")
中。
过程如下:
1、通过传入的"foo", 查找对应的BeanDefinition, 如果你不晓得什么是BeanDefinition,那你能够把它了解成封装了bean对应Class信息的对象,通过它Spring能够失去beanClass以及beanClass标识的一些注解。
2、应用BeanDefinition中的beanClass,通过反射的形式进行实例化,失去咱们所谓的bean(foo)。
3、解析beanClass信息,失去标识了Autowired
注解的属性(bar)
4、应用属性名称(bar),再次调用context.getBean('bar')
,反复以上步骤
5、将失去的bean(bar)设值到foo的属性(bar)中
以上为简略的流程形容
什么是循环依赖
循环依赖其实就是A依赖B, B也依赖A,从而形成了循环,从以上例子来讲,如果bar外面也依赖了foo,那么就产生了循环依赖。
Spring是如何解决循环依赖的
getBean这个过程能够说是一个递归函数,既然是递归函数,那必然要有一个递归终止的条件,在getBean中,很显然这个终止条件就是在填充属性过程中有所返回。那如果是现有的流程呈现Foo依赖Bar,Bar依赖Foo的状况会产生什么呢?
1、创立Foo对象
2、填充属性时发现Foo对象依赖Bar
3、创立Bar对象
4、填充属性时发现Bar对象依赖Foo
5、创立Foo对象
6、填充属性时发现Foo对象依赖Bar....
很显然,此时递归成为了死循环,该如何解决这样的问题呢?
增加缓存
咱们能够给该过程增加一层缓存,在实例化foo对象后将对象放入到缓存中,每次getBean时先从缓存中取,取不到再进行创建对象。
缓存是一个Map,key为beanName, value为Bean,增加缓存后的过程如下:
1、getBean('foo')
2、从缓存中获取foo,未找到,创立foo
3、创立结束,将foo放入缓存
4、填充属性时发现Foo对象依赖Bar
5、getBean('bar')
6、从缓存中获取bar,未找到,创立bar
7、创立结束,将bar放入缓存
8、填充属性时发现Bar对象依赖Foo
9、getBean('foo')
10、从缓存中获取foo,获取到foo, 返回
11、将foo设值到bar属性中,返回bar对象
12、将bar设置到foo属性中,返回
以上流程在增加一层缓存之后咱们发现的确能够解决循环依赖的问题。
多线程呈现空指针
你可能留神到了, 当呈现多线程状况时,这一设计就呈现了问题。
咱们假如有两个线程正在getBean('foo')
1、线程一正在运行的代码为填充属性,也就是刚刚将foo放入缓存之后
2、线程二略微慢一些,正在运行的代码是:从缓存中获取foo
此时,咱们假如线程一挂起,线程二正在运行,那么它将执行从缓存中获取foo这一逻辑,这时你就会发现,线程二失去了foo,因为线程一刚刚将foo放入了缓存,而且此时foo还没有被填充属性!
如果说线程二失去这个还没有设值(bar)的foo对象去应用,并且刚好用了foo对象外面的bar属性,那么就会失去空指针异样,这是不能为容许的!
那么咱们又当如何解决这个新的问题呢?
加锁
解决多线程问题最简略的形式便是加锁。
咱们能够在【从缓存获取】前加锁,在【填充属性】后解锁。
如此,线程二就必须期待线程一实现整个getBean流程之后才在缓存中获取foo对象。
咱们晓得加锁能够解决多线程的问题,但同样也晓得加锁会引起性能问题。
试想,加锁是为了保障缓存里的对象是一个齐备的对象,但如果当缓存里的所有对象都是齐备的了呢?或者说有局部对象曾经是齐备了的呢?
假如咱们有A、B、C三个对象
1、A对象曾经创立结束,缓存中的A对象是齐备的
2、B对象还在创立中,缓存中的B对象有些属性还没填充结束
3、C对象还未创立
此时咱们想要getBean('A'), 那咱们应该冀望什么?咱们是否冀望间接从缓存中获取到A对象返回?或者还是期待获取锁之后能力失去A对象?
很显然咱们更加冀望间接获取到A对象返回就能够了,因为咱们晓得A对象是齐备的,不须要去获取锁。
但以上的设计也很显然无奈达到该要求。
二级缓存
以上问题其实能够简化成如何将齐备对象和不齐备的对象辨别开来?因为只有咱们晓得这个是齐备对象,那么间接返回,如果是不齐备的对象,那么就须要获取锁。
咱们能够这样,再加一级缓存,第一级缓存寄存齐备对象,第二级缓存寄存不齐备的对象,因为此类对象是在Bean刚创立时放入缓存中的,所以咱们这里把它称作晚期对象。
此时,当咱们须要获取A对象时,咱们只需判断第一级缓存有没有A对象,如果有,阐明A对象是齐备的,可间接返回应用,如果没有,阐明A对象可能还没创立或者是创立中,就持续加锁-->从二级缓存获取对象-->创建对象的逻辑
此时流程如下:
1、getBean('foo')
2、从一级缓存中获取foo,未获取到
3、加锁
4、从二级缓存中获取foo,未获取到
5、创立foo对象
6、将foo对象放入二级缓存
7、填充属性
8、将foo对象放入一级缓存,此时foo对象曾经是个齐备对象了
9、删除二级缓存中的foo对象
10、解锁返回
基于现有流程,咱们再来模仿一下循坏依赖时的状况
当初,既能解决对象的齐备性问题,又能满足咱们的性能要求。perfect!
代理对象
要晓得,Java里不仅有一般对象,还有代理对象,那么创立代理对象产生循环依赖时是否可能满足要求呢?
咱们先来理解一下代理对象是什么时候创立的?
在Spring中,创立代理对象逻辑是在最初一步,也就是咱们经常说的【初始化后】
当初,咱们尝试把这部分逻辑退出到之前的流程中
不言而喻,最初的foo对象理论曾经是个代理对象了,但bar依赖的对象仍旧是个一般的foo对象!
所以,当呈现代理对象循环依赖时,之前的流程并不能满足要求!
那么这个问题又该当如何解决呢?
思路
问题呈现的起因就在于bar对象去获取foo对象时,从二级缓存中失去的foo对象是个一般的对象。
那么有没有方法在这里增加一些判断,比如说判断foo对象是不是要进行代理,如果是的话就去创立foo的代理对象,而后将代理对象proxy_foo返回。
咱们先假如这个计划是可行的,再来看有没有其余的问题
依据流程图咱们能够发现出一个问题:创立了两次proxy_foo!
1、getBean('foo')流程中,填充属性之后创立了一次proxy_foo
2、getBean('bar')的填充属性时,从缓存中获取foo时,也创立了一次proxy_foo
而这两个proxy_foo是不雷同的!尽管proxy_foo中援用的foo对象是雷同的,但这也是不可承受的。
这个问题又当如何解决?
三级缓存
咱们晓得这两次创立的proxy_foo是不雷同的,那么程序该当如何晓得呢?也就是说,咱们如果能够加一个标识,标识这个foo对象曾经被代理过了,让程序间接应用这个代理的就能够了,不要再去创立代理了。是不是就解决这个问题了呢?
这个标识可不是什么flag=ture or false之类的,因为就算程序晓得foo曾经被代理过了,那程序还是得把proxy_foo拿到才行,也就是说,咱们还得找个中央把proxy_foo存起来。
这个时候咱们就须要再加一级缓存。
逻辑如下:
1、当从缓存中获取foo时,且foo被代理了之后,就将proxy_foo放入这一级缓存中。
2、在getBean('foo')流程中,创立代理对象时,先在缓存中查看是否有代理对象,如果有则应用该代理对象
这里你可能会有疑难:不是说先判断三级缓存有没有,没有再去创立proxy_foo嘛?怎么不论有没有都去创立?
是的,这里不论如何都去创立了proxy_foo,只是最初判断三级缓存有没有,有的话就应用三级缓存里的,之前创立的proxy_foo就不要了。
起因是这样的,咱们晓得创立代理对象的逻辑是在Bean【初始化后】这一流程当中的某个后置处理器当中实现的,而后置处理器是能够由用户自定义实现的,那么反过来说就示意Spring是无法控制这一部分逻辑的。
咱们能够这样假如,咱们本人也实现了一个后置处理器,这个处理器的作用不是创立代理对象proxy_foo,而是把foo替换成dog, 如果按之前的想法(只判断是否为代理对象)你就会发现这样的问题:getBean('foo')返回的是dog,然而bar对象依赖的是foo。
然而如果咱们将【创立代理对象】这一逻辑看成只是泛滥后置处理器中的一个实现。
1、在从缓存中取foo时,调用一系列的后置处理器,而后将后置处理器返回的最终后果放入三级缓存。
2、在getBean('foo')时,同样调用一系列的后置处理器,而后从三级缓存获取foo对应的对象,失去了就应用它,否则应用后置处理器返回后果。
你就会发现,轻易你怎么折腾,getBean('foo')返回的对象与bar对象依赖的foo永远是同一个对象。
以上即为Spring对于循环依赖的解决方案
我对Spring这部分设计的思考
先总体回顾一下Spring的设计,Spring中采纳了三级缓存
1、第一级缓存寄存齐备的bean对象
2、第二级缓存寄存的是匿名函数
3、第三级缓存寄存的是从第二级缓存中匿名函数返回的对象
是的,Spring将咱们说的[从二级缓存中获取foo, 调用后置处理器]这两个步骤间接做成了一个匿名函数
它的构造如下:
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
@FunctionalInterfacepublic interface ObjectFactory<T> { T getObject() throws BeansException;}
函数内容即为调用一系列后置处理器
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject;}
对于这部分设计,始终存在着一些争议:Spring中到底应用几级缓存能够解决循环依赖?
观点一
一般对象产生循环依赖时二级缓存即能够解决,但代理对象产生循环依赖时须要三级缓存才能够
这也算是一个广泛的观点
这个观点的角度是用二级缓存时,产生循环依赖会不会出bug,认为是一般对象不会,代理对象会。
换句话说:在产生多循环依赖时,屡次从缓存中获取对象,每次失去的对象是否雷同?
举例来说,A对象依赖B对象,B对象依赖A对象和C对象,C对象依赖A对象。
getBean('A')流程如下
在该流程中,A对象从缓存中获取了两次。
当初,咱们联合从缓存中获取对象的过程来思考一下。
当只有二级缓存时的逻辑:
1、调用二级缓存中的匿名函数获取对象
2、返回对象
假如匿名函数中返回原对象,没有创立代理逻辑——这里严格来说是没有后置处理器的逻辑
那么每次【调用二级缓存中的匿名函数获取对象】时返回的A对象都是同一个。
所以得出一般对象在只有二级缓存时没有问题。
假如匿名函数中会触发创立代理的逻辑,匿名函数返回的是代理对象。
那么每次【调用二级缓存中的匿名函数获取对象】是都会创立代理对象。
每次创立的代理对象都是个新对象,故每次返回的A对象都不是同一个。
所以得出代理对象在只有二级缓存时会呈现问题。
那么为什么三级缓存能够呢?
三级缓存时的逻辑:
1、先尝试从三级缓存中获取,未获取到
2、调用二级缓存中的匿名函数获取对象
3、将对象放入三级缓存
4、删除二级缓存中的匿名函数
5、返回对象
所以在第一次从缓存获取时会调用匿名函数创立代理对象,往后每次获取时都是间接从第三级缓存取出返回。
综上所述,该观点是占得住脚的。
但我更心愿这个观点换个更谨严说法:当每次匿名函数返回的对象是统一时,二级缓存足以;当每次匿名函数返回的对象不统一时,须要有第三级缓存
观点二
该观点也是我本人的观点:从设计的角度登程,只有三级缓存能力保障框架的扩展性和健壮性。
当咱们回顾观点一的论断,你就会发现一个非常矛盾的中央:Spring如何能力得悉匿名函数返回的对象是统一的?
匿名函数中的逻辑是调用一系列的后置处理器,而后置处理器是可自定义的。
意思就是匿名函数返回了什么,这件事自身就不受Spring所管制。
这时咱们再借用三级缓存看这个问题,就会发现:无论匿名函数返回的对象是否统一,三级缓存都能无效的解决循环依赖的问题。
从设计来看,三级缓存的设计是能够蕴含二级缓存所达到的需要的。
所以咱们能够得出:应用三级缓存的设计将比二级缓存的设计有更好的扩展性和健壮性。
如果用观点一的认识去设计Spring框架,那得加一大堆逻辑判断,如果用观点二,那只需加一层缓存。
小结
本篇文章的初衷是想写我对Spring循环依赖的思考,但为了可能说分明这件事,还是具体的形容了Spring解决循环依赖的设计。
以至于最初我想表白本人的思考时,只有寥寥几句,因为大部分思考我已写在了【Spring是如何解决循环依赖的】章节。
最初,心愿大家有所播种,如果有疑难可找我询问,或者在评论区留下你的思考。
如果我的文章对你有所帮忙,还请帮忙点赞、关注、转发一下,你的反对就是我更新的能源,非常感谢!
集体博客空间:https://zijiancode.cn