手撕Spring源码,详细理解Spring循环依赖及三级缓存
Posted 走进Java
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手撕Spring源码,详细理解Spring循环依赖及三级缓存相关的知识,希望对你有一定的参考价值。
前言:
首先说一下我为什么选择去读Spring源码,以及写下这篇文章的原因,其实Spring循环依赖一直都是Spring知识的热点。就在前几天,我在一个群里吹牛逼的时候,有众多兄弟对Spring的这个循环依赖和三级缓存产生了热火朝天的讨论。所以我就准备花几天时间,好好看看Spring的源码,去详细的理解一下Spring的这个循环依赖。希望能够帮助到更多的兄弟们。让兄弟们能够在以后的面试当中,当面试官问:Spring循环依赖是什么东西?为什么使用三级缓存去解决它的时候,我们能够做到对答如流。
1.什么是循环依赖?
其实简单总结来说,就是X依赖了Y,而Y呢,又依赖了X。大致的Java代码如下:
//X里面用到了Y,我们就说X依赖了Y
class X{
public Y y;
}
//同理,Y依赖了X
class Y{
public X x;
}
其实单单看以上这些代码,他完全没有任何问题。我们如果单单的使用Java去进行这样的依赖关系下,其实是没有任何问题的。对象之间相互依赖,在正常不过了。
那为什么在我们Spring当中,这种循环依赖就有问题?
因为在Spring当中,对象的产生并不是简单的new。而是经过了我们整个Bean的生命周期,就是因为这个生命周期,循环依赖问题就出现了。
要理解Spring的循环依赖,得先了解Spring Bean的生命周期。
在之前的文章中我们已经讲述了,SpringBean整个的生命周期。此文不在详细讲述,只做大概介绍。
我们说,被Spring所管理的对象就做Bean,产生的步骤如下:
1.实例化得到对象。
2.给对象中的属性赋值。
3.如果对象中某个方法被AOP了,那么需要对这个对象生成一个代理对象。
4.将对象/代理对象(如果有)放入到单例池,后续从单例池中获取。
(步骤非常多,我们只说对本文讲解有用的步骤,详细请寻找以往的文章《【Spring篇】深入浅出的去理解Spring Bean的生命周期及作用域》)
如上图,我们最后一步的放入单例池,就是把创建好的Bean放入到一个HashMap当中。K就是我们的Bean名称,V就是我们Bean对象。他与我们常常所说的单例模式是不一样的。我们单例模式是在JVM中,某一个类实例有且只能存在一个。而我们单例池,指的是,我们的Bean名称是不重复的,但这并不能代表我们某一个类对应的Bean就只有一个。
就像下面这样定义:
"xService1") (
public XService xService1(){
return new XService();
}
"xService2") (
public XService xService2(){
return new XService();
}
如上图,我们定义两个Bean,类是同一个类。只是Bean的名称不一样。这就是与我们单例模式的区别。
我们用下面的例子来举例说明一下这过程中Bean的产生已经Bean的依赖关系:
public class XService {
private YService yService;
}
public class YService {
private XService xService;
}
从以上代码可以看出,XService与YService形成了循环依赖。那么他有什么问题呢?
按照我们Bean的生命周期来看,我们假设Spring在加载的时候,先加载了XService,那么XService创建的过程是下面这个样子的(我们暂时假设没有AOP现象)。
1.实例化XService(去单例池寻找XService,找不到,开始创建。new)
2.给其中的YService进行赋值(去单例池寻找,找不到,开始创建,new)
2.1: 实例化YService
2.2: 给YService中的XService进行赋值--------(去单例池寻找XService,还是找不到,又去创建XService,是不是又回到了第一步。)
.....(后续操作永远无法进行)
是不是死循环了,我们这两个Bean永远创建不了,也就永远执行不了后续的流程,即永远放入不了单例池,发现了吗,循环以来就是这么产生的,如下图:
2.三级缓存、解决循环依赖问题
2.1 二级缓存
上面我们说到了单例池,单例池其实就是三级缓存中的一级缓存。就是把我们的Bean给缓存到了某一个地方,后续直接拿到使用。但是现在如果存在循环依赖的问题,我们Bean永远放入不了单例池,就没有办法进行使用。于是Spring就加了一个二级缓存。二级缓存也是一个HashMap,那么他是怎么解决的呢?就是在我们实例化XService的第一步,我们把创建中的XService对象,暂时放入二级缓存。这样在XService给YService属性赋值的时候,YService就能在二级缓存中找到XService的引用了。是不是就没有问题了。我们来看一下流程:
1.实例化XService( 去单例池寻找XService,找不到,开始创建。new,放入二级缓存)
2.给其中的YService进行赋值(去单例池寻找,找不到,开始创建,new,放入二级缓存)
2.1: 实例化YService
2.2: 给YService中的XService进行赋值--------(去单例池寻找XService,还是找不到,去二级缓存中寻找,是不是找到了。拿出来,给YService中的XService完成赋值)
2.3:YService完成创建。YService放入单例池
3.XService完成创建,放入单例池。
在这里,你可能有这么一个问题:
为什么不在对象创建之后,直接放入到单例池呢?这样在Y又需要X的时候,不就可以获取到了吗?
其实这个很简单,肯定是不能放的,因为在XService实例化之后,还是一个不完整的对象,因为里面的属性并没有进行赋值,其他地方如果直接从单例池中取到去用,肯定会出现问题。
大致流程如下:
2.2 三级缓存
我们都知道 Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说 Spring 最终给我们放进容器里面的是一个代理对象,而非原始对象。
假设我们现在是二级缓存架构,创建 X 的时候,我们不知道有没有循环依赖,所以放入二级缓存提前暴露,接着创建 Y,也是放入二级缓存,这时候发现又循环依赖了 X,就去二级缓存找,是有,但是如果此时还有 AOP 代理呢,我们要的是代理对象可不是原始对象,这怎么办,难道所有 Bean 统统去完成 AOP 代理吗,如果是这样的话,就不需要三级缓存了,但是这样不仅没有必要,而且违背了 Spring 在结合 AOP
跟 Bean 的生命周期的设计。
所以 Spring “多此一举”的将实例先封装到 ObjectFactory 中(三级缓存),只有当 Spring 中存在该后置处理器,所有的单例 bean 在第一步实例化后都会被进行提前曝光到三级缓存中,但是并不是所有的 bean 都存在循环依赖,也就是三级缓存不一定都会被执行,有可能曝光后直接创建完成,没被提前引用过,就直接被加入到一级缓存中。因此可以确保只有提前曝光且被引用的 bean 才会进行该后置处理。
经典面试题:
1.Y 中提前注入了一个没有经过初始化的 X 类型对象不会有问题吗?
虽然在创建 Y 时会提前给 X 注入了一个还未初始化的 X 对象,但是在创建 X 的流程中一直使用的是注入到 Y 中的 X 对象的引用,之后会根据这个引用对 X 进行初始化,所以这是没有问题的。
2.Spring 是如何解决的循环依赖?
Spring 为了解决单例的循环依赖问题,使用了三级缓存。其中一级缓存为单例池(singletonObjects
),二级缓存为提前曝光对象(earlySingletonObjects
),三级缓存为提前曝光对象工厂(singletonFactories
)。
假设X、Y循环引用,实例化 X 的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了 Y,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖 X,这时候从缓存中查找到早期暴露的 X,没有 AOP 代理的话,直接将 X 的原始对象注入Y,完成 Y 的初始化后,进行属性填充和初始化,这时候 X 完成后,就去完成剩下的 X 的步骤,如果有 AOP 代理,就进行 AOP 处理获取代理后的对象 X,注入 Y,走剩下的流程。
3.为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?
如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过 AnnotationAwareAspectJAutoProxyCreator
这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。
回复【30G】,获取30G容量的Java全方位视频课程资料
回复【面试冲刺】,获取5G容量的Java全方位面试资料
回复【java规范】,获取阿里巴巴Java开发规范手册
分享面试经验及宝典,提供技术资料。传播编程经验,挖掘优秀资源。有爆料,有咨询,有趣,有能量。
以上是关于手撕Spring源码,详细理解Spring循环依赖及三级缓存的主要内容,如果未能解决你的问题,请参考以下文章
Spring 循环依赖原理源码的探究和总结以及三级缓存的详解一万字