关于我对Spring循环依赖的思考(二)

简介: 关于我对Spring循环依赖的思考

二级缓存

以上问题其实可以简化成如何将完备对象和不完备的对象区分开来?因为只要我们知道这个是完备对象,那么直接返回,如果是不完备的对象,那么就需要获取锁。

我们可以这样,再加一级缓存,第一级缓存存放完备对象,第二级缓存存放不完备的对象,由于此类对象是在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是如何解决循环依赖的】章节。

最后,希望大家有所收获,如果有疑问可找我询问,或者在评论区留下你的思考。

目录
相关文章
|
8天前
|
人工智能 Java Spring
Spring Boot循环依赖的症状和解决方案
Spring Boot循环依赖的症状和解决方案
|
8天前
|
缓存 Java 开发工具
【spring】如何解决循环依赖
【spring】如何解决循环依赖
15 0
|
8天前
|
存储 缓存 Java
【Spring系列笔记】依赖注入,循环依赖以及三级缓存
依赖注入: 是指通过外部配置,将依赖关系注入到对象中。依赖注入有四种主要方式:构造器注入、setter方法注入、接口注入以及注解注入。其中注解注入在开发中最为常见,因为其使用便捷以及可维护性强;构造器注入为官方推荐,可注入不可变对象以及解决循环依赖问题。本文基于依赖注入方式引出循环依赖以及三层缓存的底层原理,以及代码的实现方式。
24 0
|
8天前
|
存储 缓存 Java
【spring】06 循环依赖的分析与解决
【spring】06 循环依赖的分析与解决
10 1
|
8天前
|
存储 缓存 Java
Spring解决循环依赖
Spring解决循环依赖
|
8天前
|
缓存 算法 Java
开发必懂的Spring循环依赖图解 Spring 循环依赖
开发必懂的Spring循环依赖图解 Spring 循环依赖
22 1
|
8天前
|
缓存 算法 Java
Spring解决循环依赖
Spring解决循环依赖
21 1
|
存储 缓存 Java
Spring 动态代理时是如何解决循环依赖的?为什么要使用三级缓存?
在研究 『 Spring 是如何解决循环依赖的 』 的时候,了解到 Spring 是借助三级缓存来解决循环依赖的。
409 0
|
8月前
|
存储 缓存 Java
Spring为何需要三级缓存解决循环依赖,而不是二级缓存?
今天给大家分享一道大厂面试真题,Spring为何需要三级缓存解决循环依赖,而不是二级缓存?我一共分为五个部分来给大家介绍: 1、什么是循环依赖? 循环依赖就是指循环引用,是两个或多个Bean相互之间的持有对方的引用。在代码中,如果将两个或多个Bean互相之间持有对方的引用,因为Spring中加入了依赖注入机制,也就是自动给属性赋值。Spring给属性赋值时,将会导致死循环。那么,哪些情况会出现循环依赖呢?
158 0
|
9月前
|
缓存 Java Spring
最通俗的方式理解Spring循环依赖三级缓存
有位粉丝找我,说要耽误我5分钟时间,想让我帮助它理解一下Spring循环依赖的三级缓存,绕晕了一个星期,没有想明白。我想今天,用最通俗易懂的方式给大家重新梳理一下,保证让你听懂了。
81 0