一步一步带你理解 Spring 循环依赖

写在开头

学习 Spring 的过程当中,对 Spring 的循环依赖大致明白了,可是自己再仔细跟踪源码,却又总差点意思,似懂非懂就很烦躁,然后就埋头苦干,一定要自己弄清楚,就肝下了这篇文章,也希望看了这篇文章的朋友能有所收获。

大家也可以关注我的公众号: 浆果捕鼠草,文章也会同步更新,当然,公众号还会有一些资源可以分享给大家~

由于排版的原因,🤩🤩🤩 关注我的公众号:浆果捕鼠草,发送关键字: 循环依赖时序图
即可获得超高清时序图!

☘ Spring 循环依赖

🤔 什么是循环依赖?

举个栗子🌰

/**
 * A 类,引入 B 类的属性 b
 */
public class A {
  private B b;
}
/**
 * B 类,引入 A 类的属性 a
 */
public class B {
  private A a;
}

再看个简单的图:

在这里插入图片描述

像这样,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a,那就创建 a,创建 a 的时候需要依赖 b,那就创建 b,结果创建 b 的时候又需要依赖 a ......

🙂互相依赖何时了,死循环了吧?
👉诺,这就是循环依赖!


循环依赖其实不算个问题或者错误,我们实际在开发的时候,也可能会用到。
再拿最开始的 A 和 B 来说,我们手动使用的时候会用以下方式:

  A a = new A();
  B b = new B();
  b.setA(a);
  a.setB(b);

其实这样就解决了循环依赖,功能上是没有问题的,但是为什么 Spring 要解决循环依赖?

🤔 为什么 Spring 要解决循环依赖?

首先简单了解下,我们用 Spring 框架,它帮我们做了什么事情?总结性来说,六字真言:IoC 和 AOP

由于 Spring 解决循环依赖是考虑到 IoC 和 AOP 相关知识了,所以这里我先提一下。

由于本文主要的核心是 Spring 的循环依赖处理,所以不会对 IoC 和 AOP 做详细的说明,想了解以后有机会再说 😶

IoC,主要是将对象的创建、管理都交给了 Spring 来管理,能够解决对象之间的耦合问题,对开发人员来说也是省时省力的。

AOP,主要是在不改变原有业务逻辑情况下,增强横切逻辑代码,也是解耦合,避免横切逻辑代码重复;也是对 OOP 的延续、补充。

既然类的实例化都交给了 Spring 来管理了,那么循环依赖 Spring 肯定也要考虑到怎么去处理(怎么总觉得有点像是废话 😶)。

🤯 解决循环依赖的方式

参考我们能想到的肯定是手动处理的方式,先将对象都 new 出来,然后进行 set 属性值,而 Spring 也是通过这样的形式来处理的(你说巧不巧?其实一点都不巧 🙃,后面再说为什么),其实 Spring 管理 Bean 的实例化底层其实是由反射实现的。

而我们实例化的方式也有好多种,比如通过构造函数,一次性将属性赋值,像下面这样

// 假设有学生这个类
public class Student {
  private int id;
  private String name;

  public Student(int id, String name) {
    this.id = id;
    this.name = name;
  }
}

// 通过构造器方式实例化并赋值
new Student(1, "Suremotoo");

但是使用构造器这样的方式,是无法解决循环依赖的!为什么不能呢?

我们还是以文中开头的 A 和 B 互相依赖来说, 要通过构造器的方式实现 A 的实例化,如下

new A(b);

😮 Wow,是不是发现问题了?要通过构造器的方式,首先要将属性值实例化出来啊!A 要依赖属性 b,就需要先将 B 实例化,可是 B 的实例化是不是还是需要依赖 A❗️ 这不就是文中开头描述的样子嘛,所以通过构造器的方式,Spring 也没有办法解决循环依赖

我们使用 set 可以解决,那么 Spring 也使用 set 方式呢?答案是可以的。

既然底层是通过反射实现的,我们自己也用反射实现的话,大概思路是这样的(还是以 A 和 B 为例)

  1. 先实例化 A 类

  2. 再实例化 B 类

  3. set B 类中的 a 属性

  4. set A 类中的 b 属性

其实就是通过反射,实现以下代码

      A a = new A();
      B b = new B();
      b.setA(a);
      a.setB(b);

这里可以稍微说明一下,为什么这样可以?

A a = new A(),说明 A 只是实例化,还未初始化

同理,B b = new B(),也只是实例化,并未初始化

a.setB(b);, 对 a 的属性赋值,完成 a 的初始化

b.setA(a);, 对 b 的属性赋值,完成 b 的初始化

现在是不是有点感觉了,先把狗骗进来,再杀 😬

Spring 如何解决循环依赖问题

先上个通俗的答案解释,三级缓存

    /**
     * 单例对象的缓存:bean 名称——bean 实例,即:所谓的单例池。
     * 表示已经经历了完整生命周期的 Bean 对象
     * <b>第一级缓存</b>
     */
    Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /**
     * 早期的单例对象的高速缓存:bean 名称——bean 实例。
     * 表示 Bean 的生命周期还没走完(Bean 的属性还未填充)就把这个 Bean 存入该缓存中
     * 也就是实例化但未初始化的 bean 放入该缓存里
     * <b>第二级缓存</b>
     */
    Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    
    /**
     * 单例工厂的高速缓存:bean 名称——ObjectFactory。
     * 表示存放生成 bean 的工厂
     * <b>第三级缓存</b>
     */
    Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

代码中注释可能不清晰,我再给贴一下三级缓存:

第一级缓存(也叫单例池)Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象

第二级缓存Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完)

第三级缓存Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂

Spring 管理的 Bean 其实默认都是单例的,也就是说 Spring 将最终可以使用的 Bean 统一放入第一级缓存中,也就是 singletonObjects(单例池)里,以后凡是用到某个 Bean 了都从这里获取就行了。

仅使用一级缓存可以吗?

既然都从 singletonObjects 里获取,那么<u>仅仅使用这一个 singletonObjects</u>,可以吗?肯定不可以的
首先 singletonObjects 存入的是完全初始化好的 Bean,可以拿来直接用的。
如果我们直接将未初始化完的 Bean 放在 singletonObjects 里面,注意,这个未初始化完的 Bean 极有可能会被其他的类拿去用,它都没完事呢,就被拿去造了,肯定要出事啊!

我们以 A 、 B、C 举栗子🌰

  1. 先实例化 A 类,叫 a
  2. 将 a 放入 singletonObjects 中(此时 a 中的 b 属性还是空的呢)
  3. C 类需要使用 A 类,去 singletonObjects 获取,且获取到了 a
  4. C 类使用 a,拿出 a 类的 b 属性,然后 NPE了.

诺,出事了吧,这下就不是解决循环依赖的问题了,反而设计就不对了。

NPE 就是 NullPointerException

使用二级缓存可以吗?

再来回顾下循环依赖的问题:A→B→A→B......

说到底就是怎么打破这个循环,一级缓存不行,我们就再加一级,可以吗?
我们看个图

在这里插入图片描述

图中的缓存就是二级缓存

看完图,可能还会有疑惑,A 没初始化完成放入了缓存,那么 B 用的岂不是就是未完成的 A,是这样的没错!
在整个过程当中,A 是只有1 个,而 B 那里的 A 只是 A 的引用,所以后面 A 完成了初始化,B 中的 A 自然也就完成了。这里就是文中前面提到的手动 setA,setB那里,我再贴一下代码:

  A a = new A();
  B b = new B();
  b.setA(a); // 这里设置 b 的属性 a,其实就是 a 的引用
  a.setB(b); // 这里设置 a 的属性 b,此时的 b 已经完成了初始化,设置完 a 的属性, a 也就完成了初始化,那么对应的 b 也就完成了初始化

分析到这里呢,我们就会发现二级缓存就解决了循环依赖的问题了,可是为什么还要三级缓存呢?
这里就要说说 Spring 中 Bean 的生命周期。

Spring 中 Bean 的管理

在这里插入图片描述

要明白 Spring 中的循环依赖,首先得了解下 Spring 中 Bean 的生命周期。

被 Spring 管理的对象叫 Bean
这里不会对 Bean 的生命周期进行详细的描述,只是描述一下大概的过程,方便大家去理解循环依赖。

Spring 中 Bean 的生命周期,指的就是 Bean 从创建到销毁的一系列生命活动。

那么由 Spring 来管理 Bean,要经过的主要步骤有:

  1. Spring 根据开发人员的配置,扫描哪些类由 Spring 来管理,并为每个类生成一个 BeanDefintion,里面封装了类的一些信息,如全限定类名、哪些属性、是否单例等等

  2. 根据 BeanDefintion 的信息,通过反射,去实例化 Bean(此时就是实例化但未初始化 的 Bean)

  3. 填充上述未初始化对象中的属性(依赖注入)

  4. 如果上述未初始化对象中的方法被 AOP 了,那么就需要生成代理类(也叫包装类)

  5. 最后将完成初始化的对象存入缓存中(此处缓存 Spring 里叫: singletonObjects),下次用从缓存获取 ok 了

如果没有涉及到 AOP,那么第四步就没有生成代理类,将第三步完成属性填充的对象存入缓存中。

二级缓存有什么问题?

如果 Bean 没有 AOP,那么用二级缓存其实没有什么问题的,一旦有上述生命周期中第四步,就会导致的一个问题。因为 AOP 处理后,往往是需要生成代理对象的,代理对象和原来的对象根本就不是 1 个对象

以二级缓存的场景来说,假设 A 类的某个方法会被 AOP,过程就是这样的:

二级缓存问题示例图
  1. 生成 a 的实例,然后放入缓存,a 需要 b
  2. 再生成 b ,填充 b 的时候,需要 a,从缓存中取到了 a,完成 b 的初始化;
  3. 紧接着 a 把初始化好的 b 拿过来用,完成 a 的属性填充和初始化
  4. 由于 A 类涉及到了 AOP,再然后 a 要生成一个代理类,这里就叫:代理 a 吧

结果就是:a 最终的产物是代理 a,那 b 中其实也应该用代理 a,而现在 b 中用的却是原始的 a
代理 a 和原始的 a 不是一个对象,现在这就有问题了。

使用三级缓存如何解决?

二级缓存还是有问题,那就再加一层缓存,也就是第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,在 bean 的生命周期中,创建完对象之后,就会构造一个这个对象对应的 ObjectFactory 存入 singletonFactories 中。

singletonFactories 中存的是某个 beanName 及对应的 ObjectFactory,这个 ObjectFactory 其实就是生成这个 Bean 的工厂。实际中,这个 ObjectFactory 是个 Lambda 表达式:() -> getEarlyBeanReference(beanName, mbd, bean)而且,这个表达式<u>并没有</u>执行

那么 getEarlyBeanReference 具体做了什么事情?

核心就是两步:

第一步:根据 beanName 将它对应的实例化后且未初始化完的 Bean,存入 java Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);

第二步:生成该 Bean 对应的代理类返回

这个 earlyProxyReferences 其实就是用于记录哪些 Bean 执行过 AOP,防止后期再次对 Bean 进行 AOP

那么 getEarlyBeanReference 什么时候被触发,什么时候执行?

在二级缓存示例中,填充 B 的属性时候,需要 A,然后去缓存中拿 A,此时先去第三级缓存中去取 A,如果存在,此时就执行 getEarlyBeanReference 函数,然后该函数就会返回 A 对应的代理对象。

后续再将该代理对象放入第二级缓存中,也就是 java Map<String, Object> earlySingletonObjects里。

为什么不放入第一级缓存呢?

此时就拿到的代理对象,也是未填充属性的,也就是仍然是未初始化完的对象。

如果直接放入第一级缓存,此时被其他类拿去使用,肯定有问题了。

那么什么时候放入第一级缓存?

这里需要再简单说下第二级缓存的作用,假如 A 经过第三级缓存,获得代理对象,这个代理对象仍然是未初始化完的!那么就暂时把这个代理对象放入第二级缓存,然后删除该代理对象原本在第三级缓存中的数据(确保后期不会每次都生成新的代理对象),后面其他类要用了 A,就去第二级缓存中找,就获取到了 A 的代理对象,而且都用的是同一个 A 的代理对象,这样后面只需要对这一个代理对象进行完善,其他引入该代理对象的类就都完善了。

再往后面,继续完成 A 的初始化,那么先判断 A 是否存在于 earlyProxyReferences 中, 存在就说明 A 已经经历过 AOP 了,就无须再次 AOP。那 A 的操作就转换从二级缓存中获取,把 A 的代理类拿出来,填充代理类的属性。

完成后再将 A 的代理对象加入到第一级缓存,再把它原本在第二级缓存中的数据删掉,确保后面还用到 A 的类,直接从第一级缓存中获取。

看个图理解下

在这里插入图片描述

总结

说了这么多,总结下三级缓存:

第一级缓存(也叫单例池)Map<String, Object> singletonObjects,存放已经经历了完整生命周期的 Bean 对象

第二级缓存Map<String, Object> earlySingletonObjects,存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未填充完),可能是代理对象,也可能是原始对象

第三级缓存Map<String, ObjectFactory<?>> singletonFactories,存放可以生成 Bean 的工厂,工厂主要用来生成 Bean 的代理对象

🎉 附: 一个完整的 Spring 循环依赖时序图

时序图并不标准,但是方便大家去理解 🙃
在理解循环依赖的时候,整体是个递归,你要有种套中套、梦中梦的感觉

Spring 循环依赖时序图

由于排版的原因,🤩🤩🤩 关注我的公众号: 浆果捕鼠草,发送关键字: 循环依赖时序图
即可获得超高清时序图!

🎉🎉🎉🎉另外特殊福利 🤩🤩🤩 关注我的公众号: 浆果捕鼠草,发送关键字: 循环依赖精美
即可获得本文的精美 PDF 版本哦!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容

  • 最近面试的时候发现很多人会问Spring是如何解决循环依赖的,虽然知道是通过三级缓存去解决的,但是也仅仅只是知其然...
    凯凯雄雄阅读 683评论 0 6
  • 一、Spring bean生命周期 可以简化为以下5步。 1、构建BeanDefinition 2、实例化 Ins...
    胡峻峥阅读 923评论 0 0
  • 网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图、流程图帮助读者理解,...
    CoderBear阅读 673评论 1 8
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,532评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,187评论 4 8