共计 6499 个字符,预计需要花费 17 分钟才能阅读完成。
前言
在明天,仍然有许多人对循环依赖有着争执,也有许多面试官爱问循环依赖的问题,更甚至是在 Spring 中只问循环依赖,在国内,这彷佛成了 Spring 的必学知识点,一大特色,也被泛滥人津津有味。而我认为,这称得上 Spring 框架里泛滥优良设计中的一点污渍,一个为不良设计而斗争的实现,要晓得,Spring 整个我的项目里也没有呈现循环依赖的中央,这是因为 Spring 我的项目太简略了吗?恰恰相反,Spring 比绝大多数我的项目要简单的多。同样,在 Spring-Boot 2.6.0 Realease Note 中也阐明不再默认反对循环依赖,如要反对需手动开启(以前是默认开启),但强烈建议通过批改我的项目来突破循环依赖。
本篇文章我想来分享一下对于我对循环依赖的思考,当然,在这之前,我会先带大家温故一些对于循环依赖的常识。
依赖注入
因为循环依赖是在依赖注入的过程中产生的,咱们先简略回顾一下依赖注入的过程。
案例:
@Component
public class Bar {}
@Component
public 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);
@FunctionalInterface
public 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