手撕Spring源码,详细理解Spring循环依赖及三级缓存

450 阅读8分钟

前言:

首先说一下我为什么选择去读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就只有一个。

就像下面这样定义:

    @Bean("xService1")
    public XService xService1(){
        return new XService();
    }

    @Bean("xService2")
    public XService xService2(){
        return new XService();
    }

如上图,我们定义两个Bean,类是同一个类。只是Bean的名称不一样。这就是与我们单例模式的区别。

我们用下面的例子来举例说明一下这过程中Bean的产生已经Bean的依赖关系:

@Component("xService")
public class XService {

    @Autowired
    private YService yService;
}

@Component("yService")
public class YService {

    @Autowired
    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 代理。