关于我对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是如何解决循环依赖的】章节。

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

目录
相关文章
|
1月前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
24天前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
44 2
|
3月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
232 24
|
2月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
72 1
|
3月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
48 4
|
4月前
|
存储 缓存 Java
面试问Spring循环依赖?今天通过代码调试让你记住
该文章讨论了Spring框架中循环依赖的概念,并通过代码示例帮助读者理解这一概念。
面试问Spring循环依赖?今天通过代码调试让你记住
|
4月前
|
缓存 Java Spring
spring如何解决循环依赖
Spring框架处理循环依赖分为构造器循环依赖与setter循环依赖两种情况。构造器循环依赖不可解决,Spring会在检测到此类依赖时抛出`BeanCurrentlyInCreationException`异常。setter循环依赖则通过缓存机制解决:利用三级缓存系统,其中一级缓存`singletonObjects`存放已完成的单例Bean;二级缓存`earlySingletonObjects`存放实例化但未完成属性注入的Bean;三级缓存`singletonFactories`存放创建这些半成品Bean的工厂。
|
4月前
|
Java Spring 容器
循环依赖难破解?Spring Boot神秘武器@RequiredArgsConstructor与@Lazy大显神通!
【8月更文挑战第29天】在Spring Boot应用中,循环依赖是一个常见问题。当两个或多个Bean相互依赖形成闭环时,Spring容器会陷入死循环。本文通过对比@RequiredArgsConstructor和@Lazy注解,探讨它们如何解决循环依赖问题。**@RequiredArgsConstructor**:通过Lombok生成包含final字段的构造函数,优先通过构造函数注入依赖,简化代码但可能导致构造函数复杂。**@Lazy**:延迟Bean的初始化,直到首次使用,打破创建顺序依赖,增加灵活性但可能影响性能。根据具体场景选择合适方案可有效解决循环依赖问题。
175 0
|
5月前
|
缓存 Java 开发者
Spring循环依赖问题之Spring循环依赖如何解决
Spring循环依赖问题之Spring循环依赖如何解决
|
4月前
|
前端开发 Java 测试技术
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作