从Spring的循环引用打破你的认知!!!

假设我现在给出两个类,A、B,并且交由 Spring 容器管理,A、B 内部分别引用了 B、A,那么这样的循环引用是否会成功?
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
我们看结果:
可以发现,A、B 都成功在内部引用了对方,都执行成功了各自的 hello() 方法。
在这里插入图片描述
但是,这说明了什么问题?
很多人认为,循环引用有什么,不就是只要 new 一个 A,new 一个 B,然后 A 注入 B,B 注入 A 不就好了?

这么说确实有那么一丝丝道理,但是,这仅仅只是对一个你认为的对象进行循环引用,但是 Spring 是一个非常复杂,功能繁多,可扩展性极强的一个容器,它是不能够这么做的。
所以 Spring 在创建 bean 的时候,做了非常多非常多的事情,我不可能再开头给你一次性说完,因为太多了。
(而且这里面的方法还可以继续细分)
所以,这篇文章,我会从循环引用的相关处,来一步一步地慢慢分析,所以个位客官请不要急。
在这里插入图片描述

我举一个例子,来说明原因:
Spring 的 bean 要经历实例化对象,属性注入,执行生命周期回调方法,还有 AOP 代理,最后才会放入单例池。
(当然 bean 的创建流程说详细了有很多,这里方便举例,你可以认为大致是这些)
在这里插入图片描述

现在,假设,A 先开始创建,然后 A 要注入 B,就会从单例池去获取;
但是此时此刻还没有 B,于是就要创建 B;
但是 B 创建时又要注入 A,就会从单例池获取;
但是单例池此时还没有 A,因为 A 还没完成,还没有放进单例池,所以又要创建 A;
然后…
在这里插入图片描述

你会发现进入了一个死循环。
但是,有人会说,Spring 的代码写得太蠢,为什么不能创建了对象,就直接能够被其他对象引用,而是非要全部完成了之后,才能让它被引用?

假设只要 A 被 new 了,就能够被 B 引用,那就是下面的流程:
在这里插入图片描述

看起来似乎很完美。
但是,你仔细看看,有没有发现什么问题?
(最好能自己看出问题)

我们可以发现,A 注入的 B,是一个代理 B,但是,B 注入的 A,是一个普通 A。
那么这个属性注入就是失败的,完全不合理!
在这里插入图片描述

所以,不知道到这里你有没有发现,循环引用,其实是一个非常难以解决的问题。
不过 Spring 确实做到了用一个非常精妙的方法,解决了循环引用的问题。

现在我给你们测试一下:
给出一个切面,把它交给 Spring 容器管理。
这样的话,我的所有 bean 的 hello 方法就会被代理,会额外打印一句 “before…”。
在这里插入图片描述
运行同样的测试代码,我们会发现,我们注入的对象,也都是被代理的对象。
在这里插入图片描述
看到这里,如果你没有了解过 Spring 相关源码的话,一定会很疑惑,Spring 到底是如何做到,让两个 bean 能够引用对方,并且都看起来已经走完了完整的创建过程?

还有,值得一提的是,Spring 是默认支持循环引用的。
你可能要疑惑了,支持循环引用又怎么了?

因为,我也可以让 Spring 不支持循环引用,我只要改 Spring 源码的几个字母。
在这里插入图片描述
在这里,我仅仅把 Spring 源码中的一个 true,改成了一个 false;
然后,我在运行原先的测试程序:
在这里插入图片描述
你就会发现,开始报错,循环引用不被允许。

不过,我绝对不支持去修改源码,我们学习 Spring 的源码,其实实际上,主要是为了扩展 Spring
Spring 的代码中其实提供了很多很多的扩展点,我们可以继承 Spring 的很多类,去扩展其中的方法,包括,关闭循环引用。

此外,Spring 支持循环引用还有两个条件,就是必须是单例。

现在,我把 A、B 改成原型,不再是单例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
发现,果然报错。

下面,我来一点一点分析。
由于我之前写了些源码博客,发现虽然把源码分析得无法再详细了,但是能坚持看的人还是很少,所以我这次尝试着少贴一点代码,更多地通过画图描述。当然,必要处,我还是得贴代码,脱离代码的学习源码,一定是不靠谱的。

由于之前给大家演示的 Spring bean 的创建过程比较简略,更多地只是先引入问题。
实际上 bean 的创建过程很复杂,这其中也包含了解决循环引用的办法。
在这里插入图片描述
我们先看一个大致的过程,一个创建 bean 的流程,是先去 get 一个 bean。
你有没有觉得这个逻辑很诡异?

为什么创建一个 bean 要去先 get 一个 bean,既然要创建,肯定是还不存在一个 bean 啊,怎么会去 get 一个 bean,这时候肯定为 null 啊。

实际上,这个问题的解释有很多,因为它解决了不止一个问题。

首先,这是一个方法共用
调用 getBean 方法,就会调用 doGetBean 方法,意思就是获取一个 bean。
所以,在这个方法里去获取一个 bean 是没有什么问题的。

如果获取不到 bean,就创建一个 bean 然后返回。
也就是获取 bean 和创建 bean,共用了一个方法:
获取,调这个方法;
创建,也是调用这个方法。

而我们目前所讨论的循环引用,是在 Spring 容器刷新的倒数第二大步,就是创建容器中所有剩下的没有创建的单实例 bean。
而有些其它的 bean,一些后置处理器等等,它们在之前就已经创建完成了,所以此时就不必创建,一个 getSingleton 方法,就能直接获取到,也就不会进行重复创建。
所以,把创建 bean 和获取 bean 用这样同一个方法,就可以很容易地避免单例 bean 被重复创建,这就是 getBean 中创建 bean 的一大原因。

为什么我要在这里提及这个?
因为这只是其中一个原因,
还有一个原因,就是它对解决循环引用 ,也有作用。
(作用现在还无法揭秘,你得跟着我继续去分析)

现在我们先暂且知道,在 doGetBean 的过程中,有两个 getSingleton 方法,
第一次调用的时候,会先返回空;
第二次调用的时候,会真正去创建我们的 bean。

这里,请允许我要贴一小段代码:
在这里插入图片描述
我们可以发现,第二段 getSingleton 方法中,传入了一个 lambda 表达式:
这里面有一个 createBean 方法。
我觉得聪明的大家,看方法名字都能想出来,这肯定就是创建 bean 的方法。
在 createBean 方法里面,在有一处,就又调用了 doCreateBean 方法,然后真正去创建我们的 bean。

那么,这里是怎么跟循环引用扯上关系的呢?
因为,我在之前,故意漏提了一个点,就是 getSingleton:
getSingleton 这个方法,不仅仅只是,去从单例池尝试获取这个对象,它还有一些其它操作。

在第一次 getSingleton 的时候,这里没有传入 lambda 表达式,也就是没有传入对象工厂,在这里不会创建对象,仅仅只是去获取。
第二次 getSingleton 的时候,是一个重载方法,传入了 lambda 表达式作为对象工厂,这个对象工厂会有一个方法,去创建我们的 bean,也就是 lambda 表达式中包含的 createBean 方法。

然后,第一次调用 getSingleton 方法,也不仅仅只尝试获取单例池中的 bean;
如果它获取不到单例池的 bean,它就会判断是否这个 bean 正在创建过程中!!!

什么叫是否这个 bean 正在创建过程中???
这还得从我们第二次调用 getSingleton 方法说起:

第二次调用 getSingleton,会首先去单例池拿一下,如果拿不到,那就会开始创建对象:
在这里插入图片描述
那么,Spring 又是怎样表示一个 bean 正在创建过程中的呢?
我贴一小段代码,你就明白了:
在这里插入图片描述
我们可以发现,Spring 表示一个 bean 正在创建过程中,就是往一个集合中添加了这个 bean 的名字,那么这样以后 getBean 的时候,只要判断这个集合中有没有这个 bean 的名字,就知道这个 bean 是否正在被创建过程之中。
在这里插入图片描述

那么这对于循环引用又有什么作用呢?

假设我们 A 要引用 B,所以就会 getBean,就会在 getBean 方法中创建 B;
B 要引用 A,就会 getBean,
但是在 getSingleton 的时候,会发现正在创建中,就不会重复创建 A。

这就是循环引用解决死循环的办法,主要也是防止重复创建。
在这里插入图片描述

那么既然,解决了 A B 死循环调用的过程。
可是,B 注入 A 的时候,A 也还没有走完创建流程,那么 A 是如何被注入的?

这就得看 createBean 的方法流程:

在 createBean 中调用了 doCreateBean 来进行创建。
在创建的过程中,如果支持循环引用,则会提前暴露单例(在 Spring 中就叫 earlySingletonExposure)。

所以 doCreateBean 的创建流程可以概括为:
在这里插入图片描述
不过,提前暴露单例(earlySingletonExposure),有三个前提

  • 允许循环引用;
  • 这个 bean 是一个单例;
  • 这个 bean 正在创建中。

就是这段代码:
在这里插入图片描述
首先,bean 确实是单例的;
在这里,我们可以保证,这个 bean 是正在创建中的,上文已经分析过了;
还有,允许循环引用是默认支持的,因为开头我也演示了,Spring 默认传入的参数是 true,我改成了 false 就会报错。

所以,提前暴露单例(earlySingletonExposure)在这里是一定成立的,
我们只需要研究,如果支持循环引用,那么提前暴露单例(earlySingletonExposure)是什么意思。

容许我贴一小段代码:
在这里插入图片描述
这里你看不懂还没关系,我会继续分析。
不过首先你得知道,在这里,Spring 的提前暴露单例,就是调用了 addSingletonFactory 方法,
在这个方法中:
往 singletonFactories 这么一个 Map 中 put 了一个对象。

其它的代码不用管,我给你理思路。
在这些相关的代码中,都会出现三个 Map:

  • singletonObjects;// 单例池
  • singletonFactories;
  • earlySingletonObjects;

singletonObjects 就是我们平时所熟悉的单例池,所有完整的 bean 都会存放在单例池里面。
其它两个的作用你们很快也能知道,先别急。

我要提的是,所有和这三个 Map 有关的代码,都会保证,在这三个 Map 中,一个 bean 的有关对象,只会存在其中的一个 Map 里,另两个一定是空的。
所以,所有出现的有关代码,只要一个 Map 进行了 put,另两个,要么是判断是否 contains,要么就是 remove,不管是否存在,都会 remove。

所以,在这里,earlySingletonObjects 里是没有相关对象的,但是,仍然会去 remove,这是 Spring 做的保证;
singletonObjects 在前面也做了一个 if,保证确实没有先关对象。

所以,在这里,我们看到的 Spring 所谓的提前暴露单例,就是往 singletonFactories 中,put 进了一个对象。

为什么往这个 singletonFactories 中存放了一个对象,就能解决循环引用?
因为我在之前,还是故意漏提了一个东西:

就是第一次调用 getSingleton 方法的时候:

  • 首先会从单例池取;
  • 取不到会判断是否在创建过程中;
    实际上,后面还有事情:
  • 就会继续从 earlySingletonObjects 中取;
  • 这时候,我们发现,这个 Map 中不包含有关对象,所以还是空;
  • 于是又会判断:是否允许早期引用,也就是是否允许循环依赖;
  • 现在我们讨论的都是允许循环依赖的场景,所以就会从 singletonFactories 中取出一个工厂对象;
  • 这时,就不为空了吧!
    所以,就会把取出的工厂对象调用 getObject 获取一个对象,这个对象就能返回;
  • 不过,注意三个 Map:
    这时,会把这个工厂取出的对象 put 进 earlySingletonObjects;
    singletonFactories 会 remove 掉那个工厂对象,保证只有一个 Map 存在有关对象。

所以,你就会发现,当 B 去获取 A 的时候,这个 getSingleton 方法就能获取到这个 A 对象,所以就能完成注入!
循环引用就似乎被解决了!
(这个图画了好久。。至少你应该能看明白循环引用的大致流程了)
在这里插入图片描述
所以,简单点概括,就是,A 注入 B 的时候创建 B;
B 注入 A 的时候,注入一个早期的 A;
A 注入完 B 继续走完创建流程。

但是,上面我提到了三个 Map,但是才用到两个啊?
还有一个 earlySingletonObjects 虽然有 put 进去什么东西,但是没见取出来过啊?
它有什么作用?

还有,为什么提早暴露单例,为什么要放一个工厂进去啊?
它为什么不直接把 A 放进去,而是放进去一个工厂啊?
直接放一个 A,到时候 B 就只要直接注入 A 就好了啊,那就不用再从工厂里面在取出一个 A 来了?

但是!!!!!!!!!!!!!!!!!!!!
没有结束啊!
记得我在开头给你们演示的,循环引用的时候,注入的是代理对象啊?
可是这里 Spring 怎么在还没有代理的时候,就注入了?

这不是自相矛盾??

这些个问题,如果我在这里不提出来,你可能就不一定全部发现,
要是没有发现这些问题,你就搞不定这个循环引用,
所以,当把这些点,都串起来之后,然后去互相联系着,去研究 Spring 的做法,
你就能搞清楚,循环引用是怎么回事。

首先,我们要弄明白,这个 B 注入的 A,是怎么不明不白就被代理了的?
那就得从这三个 Map 说起。

首先,我们很明确,singletonObjects 是单例池,只有完成了 bean 的创建的对象,才会被放进去,所以,在 B 注入 A 的期间,A 是不存在单例池中的。
而且之前也已经分析过了,B 是从 singletonFactories 中获取到一个工厂对象,得到 A 的,
所以注入的 A,一定是在工厂中,被完成了代理。
所以,这个秘密一定隐藏在这个工厂对象中。

(虽然,现在我还没有掏出强大的源码给你看,但是,就算不看工厂到底做了什么,我们也能推断出,一定是在工厂中,完成了 A 的代理。不然你上哪代理去!)

所以,我们就要看,在提前暴露单例(earlySingletonExposure)中,到底传入了一个什么工厂。
容许我贴一小段代码:
在这里插入图片描述
我们可以看到,spring 就是在这里传入了一个 lambda 表达式作为一个工厂,所以,等到 B 获取到 A 的工厂,然后 get 出 A 的时候,就会调用这个 lambda 表达式的方法,然后返回一个 A。
而这个 lambda 表达式里面,则只是调用了一个 getEarlyBeanReference 方法,所以,我们就只需要去研究这个方法即可。

一小段代码:
(其实代码现在看不看都无所谓,看不看得懂也无所谓,你只要把我的讲解看明白了,这些重点记住了,你就能理解循环引用是怎么做的,代码我只是贴出来辅助理解。)
在这里插入图片描述
像这段代码,也不复杂,其实就是把所有的 SmartInstantiationAwareBeanPostProcessor 这一种后置处理器的 getEarlyBeanReference 后置处理方法都执行一遍。

而其中有一个 SmartInstantiationAwareBeanPostProcessor 后置处理器,就会执行我们的代理逻辑,把 A 变成一个代理的 A。
所以就会有如下的流程:
在这里插入图片描述
看起来好像没什么问题。
但是好像又有什么问题。

B 注入 A 的时候,工厂调用方法代理了 A,返回给 B 注入,而 A 注入 B 完成后,又继续走创建流程,去进行代理,从而又被代理了一次。
也就是,看起来,按照这样的流程,A 就会产生了两个代理对象!!!
在这里插入图片描述

所以,这时,就要验证,这个 A 的代理到底是什么样的逻辑?
到底会不会产生两个代理对象?

想要搞清楚,有两个办法:
1、看源码;
2、测试

我先给大家看一下测试代码,然后再从源码继续分析,Spring 到底是怎么做的。
在这里插入图片描述
在这里插入图片描述
然后我们会惊奇地发现,竟然引用的对象,和容器中管理的对象,是同一个!

也就是,A 在执行完了属性注入之后,不管后面的代理是怎么做的,总之最后一定往单例池放入了之前 B 注入 A 时就代理了的 A 对象。

那么,A 又是怎么获取到自己提前代理的对象,把它存入单例池中的呢?
我们就得去看后面的代码:

首先,我们知道,spring 创建 bean 的并存入容器的过程就是在 getSingleton 方法中,把 createBean 方法创建出的 bean,put 到单例池中;
而 createBean 又是调用了 doCreateBean 方法,创建 bean;
所以,doCreateBean 方法的返回值,就是最终被 put 进单例池中的 bean。

所以,我们只要找出,最后 A 在 doCreateBean 方法中,返回的对象是什么!

贴一小段代码,这里我特地给你们写了注释:
在这里插入图片描述

首先,我们在之前已经搞明白过了:

  • A 在创建的时候,会先往 singletonFactories 里放入一个工厂;
  • 然后 B 要注入 A 的时候,A 还没完成,就会从工厂中,取出一个 A 注入;
    工厂返回 A 的时候,会完成一系列工作,包括代理工作;
    然后,这个被工厂处理过的 A,会被放入 earlySingletonObjects 这个 Map 中;
  • A 在创建的最后,判断初始化返回的对象,如果是原对象,那就使用 earlySingletonObjects 取出的对象,也就是之前工厂处理过的那个对象;

所以,我们最终发现,注入的对象,就是容器中的对象,也就是这两个对象是相等的。
在这里插入图片描述

到这里,我们就弄清楚了循环引用的流程,也就是,如果循环引用,导致对象被提前代理了,那么这个代理对象就会被缓存起来;
那么 A 在注入 B 之后,就不会再代理处新的对象,而是使用这个缓存了的代理对象,这样就不会出现,产生两个代理对象,使得注入的,和容器中的不是同一个对象了。

但是,你们现在虽然听我说,不会再返回第二次代理的新对象,
但是,虽然返回的原对象,但是我们不能判断出,在 A 到了初始化的过程中,是否进行了代理。

可能我说的有点绕,但是,你仔细想:
返回的是 A 缓存的早期代理对象;
但是,A 现在也执行到了初始化的阶段,不管这个初始化阶段有没有被代理,首先,初始化阶段是会执行的。

这是第一点,
其二,初始化阶段执行的过程中,可能也再一次代理了 A 对象,因为为了生命周期的完整;
只不过,这个代理对象最终没有返回而已;
这也是可能的对吧。

所以,我们虽然知道了,会通过返回的对象是不是原对象,来判断是否用缓存中的对象;
但是,我们不能保证,这个初始化过程没有执行代理;
我们只保证了,没有返回代理对象;
因为可能执行了代理但是没有返回!!!

所以,我们还是得继续验证,到底,有没有对 A 执行了两次代理。

所以,我们要找到给 A 执行代理的后置处理器,查看它的逻辑。

(幸亏你们遇到我,都已经帮你们找好了)
初始化逻辑有很多,这里我只展示代理相关的代码:
在这里插入图片描述
我们可以看到,实际上,就是在 bean 初始化的最后,调用了所有 BeanPostProcessor 的 PostProcessAfterInitialization 方法,执行逻辑;
而其中一个后置处理器,就完成了 AOP 代理。

(好心的我已经帮你们找出来了)
在这里插入图片描述
这段代码逻辑很简单,我们可以发现,一共一个 if,两种操作:

  • 如果里面不存在这个 bean 对象,那么就执行代理操作;
  • 如果存在,那么 remove 操作就会成功,返回的 bean 就会相等,所以 if 判断就不成立,就会不代理,直接返回原来的 bean 对象。

所以我们可以大胆地猜测,在早期代理的时候,一定会执行一步操作,把这个早期 bean 添加到了 earlyProxyReferences 这个 Map 中;
所以此时就会发现存在这个 bean,所以就把它移除,不再进行代理,所以返回的就是原对象。

所以答案已经很明了了,现在只差一步,找到那段代码,验证我们的猜想!!!

既然早期代理是在工厂中进行,所以,我们就要找到那个工厂。
上文我们分析过了,那个工厂是一个 lambda 表达式传入的,我们没法直接找到那个类,
所以我们只要去看那段 lambda 表达式的代码。

(这段代码之前贴过了)
在这里插入图片描述

所以,我们现在要做的,就是找到那个后置处理器。

我真是太好了,又帮你们找到了(好吧,其实就是刚才那个后置处理器)
在这里插入图片描述

现在终于证明完成,可见确实是早期代理的时候,事先往 earlyProxyReferences 存放了这个 bean 对象,
所以等到这个 bean 真正要走到初始化,要被代理的时候,发现 earlyProxyReferences 已经存放过了这个对象,就不再会去代理了。
所以就会返回原对象。

spring 发现,返回了原对象,所以知道该 bean 一定被早期代理了,就会返回缓存中的那个对象,作为最终产生的 bean,把它放入 singletonObjects 这个单例池中。

致此,大功告成,循环引用成功。

那么,致此,关于循环引用的知识点,就应该全部理解了。
(当然,如果你一目十行你肯定也还是不知道我在讲什么东西。。。)

我下面举一个例子,来描述一遍整个循环引用的流程,同时也算是一个总结和巩固,也帮助大家查漏补缺。

例子:
A 引用 B、C;
B 引用 A;
C 引用 A;
bean 的创建顺序是 A、B、C。

(因为实在是有点多,所以最好鼠标点击放大后进行观看)
在这里插入图片描述

上面的这个例子就是来帮助大家巩固的。
但是,我要提一点就是,bean 的创建过程不仅仅只有这些流程,还有很多的操作与循环引用无关的,我都没有列举。
不过,等到掌握了这些之后,就能对 Spring 的 bean 的生命周期有一个大体的认识,那么,你也就算是入门了 Spring 的源码,你之后的阅读源码和学习,就可以轻松很多。

还有,由于 Spring 的源码中,有各种各样的方法,包含各种各样的功能,所以,时间有限的情况,我们是没有办法全部阅读研究完的。
但是,至少,关键的,总体上的代码,阅读研究之后,就能对 Spring 的流程、思想有自己的见解认识,而其它细枝末节之处,即使没有阅读,也不成大碍。

这也是我第一次尝试,尽量不用代码来给大家讲解源码
虽然光听我的演示,和描述流程,可以明白和理解这么一个循环引用的过程。

但是,笔者认为,脱离代码,不去阅读,那么你学习的知识仍然是很浮,不扎实的,你最终还是无法精通 Spring 源码的。

所以,笔者还是建议,你能够自己对着源码去 debug,去尝试着,自己找到那些关键的代码,去理清 Spring 运作的流程,相信你会获益匪浅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值