前言
大家好,我是子路,一个靠Java吃饭的男人
前两篇文章:
已经介绍完了循环依赖在Bean生命周期中的体现,以及对每一条代码做出了诠释。这篇文章将接着上面两篇文章,完成Spring当中的循环应用的讲解。
正文
我先给出这个方法的源码全貌;重点我用红色标记了,并且会在进行代码解析;黄色线下面的读者可以不用管,和本文内容没多大关系;
读者可以好好看看下图:方便你阅读下面的代码解析
//doCreateBean -1
instanceWrapper = createBeanInstance(beanName, mbd, args);
createBeanInstance 顾名思义就是创建一个实例,注意这里仅仅是创建一个实例对象,还不能称为bean;因为我文章一开头就解释了什么是bean,什么是对象;好吧再啰嗦一下吧,文章比较长,不方便翻阅;
1、spring bean——受spring容器管理的对象,可能经过了完整的spring bean生命周期(为什么是可能?难道还有bean是没有经过bean生命周期的?答案是有的,具体我们后面文章分析),最终存在spring容器当中;一个bean一定是个对象
2、对象——任何符合java语法规则实例化出来的对象,但是一个对象并不一定是spring bean;
同样用dubug来说明一下:
注意这是张gif,如果你看着不动请参考我上面说的方法
运行完createBeanInstance之后控制打印了X构造方法的内容,说明X对象已经被创建了,但是这个时候的x不是bean,因为bean的生命周期才刚刚开始;这就好比你跑到天上人间,问了各种你想问的问题之后交了1000块钱,但是这个时候你仅仅是个消费者,还不是渣男,因为一条龙的服务是从交钱开始,接下来的各种服务完成你才是一个名副其实的渣男,不知道这么解释有没有偏差;为了把前面知识串起来,照例画一下当前代码的语境吧:
这个createBeanInstance方法是如何把对象创建出来的呢?对应文章开头说的bean的生命周期一共17步,其中的第8步(推断构造方法)和第9步(利用构造方法反射来实例化对象);具体如何推断构造方法我会在后面的博客分析;这里截个图看看代码就行,不做分析;
推断构造方法的代码运行结果分析——注意这张图比较长,读者可以多看几遍;因为推断构造方法笔者以为是属于spring源码中特别重要和特别难的一块知识;后面会有单独博客来分析,所以读者可以先多看看这张图;
注意这是张gif,如果你看着不动请参考我上面说的方法
至此x对象已经实例化出来,代码往下执行到合并beanDefinition,看图吧:
但是其实合并beanDefinition和本文讨论的循环依赖无关,故而先跳过;
//doCreateBean-2 开始解析
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
这段代码其实比较简单,就是给earlySingletonExposure
这个布尔类型的变量赋值;这个变量的意义是——是否支持(开启了)循环依赖;如果返回true则spring会做一些特殊的操作来完成循环依赖;如果返回false,则不会有特殊操作;
回到天上人间那个问题,好比你去一条龙的时候;人家会分析你是否是雏,如果你是雏则随便给你安排一个技师;当然如果你是笔者这样的资深玩家,可能会安排新亘结衣也说不定;
那么这个布尔变量的赋值逻辑是怎样的呢?上面代码可知三个条件做&&运算,同时成立才会返回true;
1、mbd.isSingleton();判断当前实例化的bean是否为单例;再一次说明原型是不支持循环依赖的;因为如果是原型这里就会返回false,由于是&&运算,整个结果都为false;相当于人家判断你是雏;那么新亘结衣什么的就别想了;在本文环境里X是默认单例的,故而整个条件是true;
2、this.allowCircularReferences
;整个全局变量spring 默认为true;当然spring提供了api供程序员修改,这个在本文开头笔者解释过(笔者是通过修改spring源码来改变这个值为false),在没有修改的情况下这里也返回true;
3、isSingletonCurrentlyInCreation(beanName);判断当前正在创建的bean是否在正在创建bean的集合当中;还记得前文笔者已经解释过singletonsCurrentlyInCreation这个集合现在里面存在且只有一个x;故而也会返回true。
其实这三种情况需要关心的只有第二种;因为第一种是否单例一般都是成立的,因为如果是原型的循环依赖前面代码已经报错了;压根不会执行到这里;第三种情况也一般是成立,因为这个集合是spring操作的,没有提供api给程序员去操作;而正常流程下代码执行到这里,当前正在创建的bean是一定在那个集合里面的;换句话说这三个条件1和3基本恒成立;唯有第二种情况可能会不成立,因为程序员可以通过api来修改第二个条件的结果;
总结:spring的循环依赖,不支持原型,不支持构造方法注入的bean;默认情况下单例bean是支持循环依赖的,但是也支持关闭,关闭的原理就是设置allowCircularReferences=false;spring提供了api来设置这个值;
至此我们知道boolean earlySingletonExposure=true
,那么代码接着往下执行;判断这个变量;
if
成立,进入分支;
//doCreateBean-3 开始分析
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
这段代码又用了lamda表达式;笔者为了初学者看懂,还是改成传统代码
ObjectFactory<?> singletonFactory = new ObjectFactory<?>(){
public T getObject(){
//至于这个getEarlyBeanReference方法的代码,后面再来说
// 现在可以理解为就是返回 bean
getEarlyBeanReference(beanName, mbd, bean);
//getEarlyBeanReference 的代码稍微复杂一点,可以简单理解为下面这样
getEarlyBeanReference(beanName, mbd, bean){
return bean;
}
}
}
也就是singletonFactory.getObject();其实就是返回当前正在实例化的bean
改完之后的代码可以理解成这样:
addSingletonFactory(beanName,singletonFactory);
addSingletonFactory(beanName,singletonFactory);
顾名思义添加一个单例工厂;其实这里要非常注意,因为大部分资料里面在说到spring循环依赖的时候都说是提前暴露一个半成品bean;笔者觉得这个不严格;甚至算错误了,所谓的提前暴露就是这里的add,但是我们看到源码并不是add一个bean的,而是add一个工厂对象——singletonFactory;两种说法有什么区别呢?区别可大了,简直天壤之别;我们慢慢分析;这里bean和工厂有什么区别呢?在当前的语境下面bean就是x对象经历完spring生命周期之后;所谓的半成品bean,可能还没有经历完整的生命周期;而工厂对象呢?如果你去ObjectFactory的源码或者直接顾名思义他是一个能够产生对象的工厂,或者叫能够产生bean的工厂;换句话说bean是一个产品,而工厂是产生这些产品的公司;如果还不能理解换成天上人间可能好理解——冰火和全套的区别,冰火是全套里面的一个项目,除了冰火还有其他项目;
那么spring在这里add的是singletonFactory这个工厂对象(这个工厂可以产生半成品对象),而不是一个半成品对象;相当于这里add的是全套,而不是冰火;将来拿出来的时候是得到工厂,继而通过工厂得到半成品bean;将来拿出来的是全套,你可以在全套里面肆意选择一个项目;不知道我又没有解释清楚这个问题;
当然说了这么多可能你还是没明白为什么需要在这里add这个工厂对象呢?还有add到哪里去呢?
我们首先分析bean工厂对象到底add到哪里去了,查看源码:
读者可以好好看看上图,笔者在spring源码当中把注释写上了(注释的信息很重要,认真看看),整个方法其实就是对三个map操作,至于这三个map的意义,参考下图:
通过代码可以得知singletonFactory主要被add到二级缓存中;至于为什么要add到这个map?主要了循环依赖,提前暴露这个工厂;当然如果你不理解为什么要提前暴露,没关系往下看,看完文章一定会知道的;
保持好习惯照例画个图,让读者知道现在的情况吧:
当然这里还是用一幅图来秒杀一下这个三个map的各种情况吧:
一级缓存:可能存在很多bean,比如spring各种内置bean,比如你项目里面其他的已经创建好的bean,但是在X的创建过程中,一级缓存中绝对是没有xbean的,也没用y;因为spring创建bean默认的顺序是根据字母顺序的;
二级缓存:里面现在仅仅存在一个工厂对象,对应的key为x的beanName,并且这个bean工厂对象的getObect方法能返回现在的这个时候的x(半成品的xbean)
put完成之后,代码接着往下执行;
三级缓存:姑且认为里面什么都没有吧!
//doCreateBean-4 开始解析
populateBean(beanName, mbd, instanceWrapper);
populateBean这个方法可谓大名鼎鼎,主要就是完成属性注入,也就是大家常常说的自动注入;假设本文环境中的代码运行完这行代码那么则会注入y,而y又引用了x,所以注入进来的y对象,也完成了x的注入;什么意思呢?首先看一下没有执行populateBean之前的情况。
没有执行populateBean之前只实例化了X,Y并没实例化,那么Y也不能注入了;接下来看看执行完这行代码之后的情况:
populateBean里面的代码以后我更新文章来说明,本文先来猜测一下这个方法里面究竟干了什么事;
x 填充 y (简称 xpy)首先肯定需要获取y,调用getBean(y),getBean的本质上文已经分析过货进入到第一次调用getSingleton,读者可以回顾一下上文我对doGetBean方法名字的解释里说了这个方法是创建bean和获取共用的;
第一次getSingleton会从单例池获取一下y,如果y没有存在单例池则开始创建y;
创建y的流程和创建x一模一样,都会走bean的生命周期;比如把y添加到正在创建的bean的集合当中,推断构造方法,实例化y,提前暴露工厂对象(二级缓存里面现在有两个工厂了,分别是x和y)等等。。。。重复x的步骤;
直到y的生命周期走到填充x的时候ypx,第一次调用getSingletion获取x?这里问个问题,能否获取到x呢?
在回答这个问题之前我们先把该画的图贴出来,首先那个正在被创建bean的集合已经不在是只有一个x了;(读者可以对比一下上文的图)
然后我们再把xpy到ypx的流程图贴出来,请读者仔细看看
是否能够获取到x呢?首先我们想如果获取失败则又要创建x—>实例化x—填充属性----获取y--------。。。。。。。就无限循环了;所以结果是完成了循环依赖,那么这里肯定能够获取到x;那么获取到x后流程是怎样呢?
那么为什么能够获取到x呢?讲道理联系上文第一次调用getSingleton是无法获取到x的?因为我们上面说过第一次调用getSingleton是从单例池当中获取一个bean,但是x显然没有完成生命周期(x只走到了填充y,还有很多生命周期没走完),所以应该是获取不到的?为了搞清楚这个原因得去查看第一次getSingleton的源码;如果读者有留意的话笔者前面只是凭只管告诉你第一次getSingleton是从单例池当中获取一个bean,并没有去证明,也就是没有去分析第一次getSingleton的源码;而且我在总结第一次getSingleton的时候用了目前这个词;证据如下(图是本文前面的内容,为了翻阅方便我直接贴这里了)
显然这是笔者前面故意挖的坑,所以各位读者在阅读别人的文章或者书籍的时候一定要小心验证;包括笔者的文章如果有错误一定记得告诉我;
下面来开始对第一次getSIngleton源码做深入分析;首先把源码以及我写的注释贴出来,分为图片和源代码,建议大家看图片,可读性好。
源码:如果你仔细看了上面的图片可以跳过这里的源码展示。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//从单例池当(一级缓存)中直接拿,也就是文章里面'目前'的解释
//这也是为什么getBean("xx")能获取一个初始化好bean的根本代码
Object singletonObject = this.singletonObjects.get(beanName);
//如果这个时候是x注入y,创建y,y注入x,获取x的时候那么x不在容器
//第一个singletonObject == null成立
//第二个条件判断是否存在正在创建bean的集合当中,前面我们分析过,成立
//进入if分支
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//先从三级缓存那x?为什么先从三级缓存拿?下文解释
singletonObject = this.earlySingletonObjects.get(beanName);
//讲道理是拿不到的,因为这三个map现在只有二级缓存中存了一个工厂对象
//回顾一下文章上面的流程讲工厂对象那里,把他存到了二级缓存
//所以三级缓存拿到的singletonObject==null 第一个条件成立
//第二个条件allowEarlyReference=true,这个前文有解释
//就是spring循环依赖的开关,默认为true 进入if分支
if (singletonObject == null && allowEarlyReference) {
//从二级缓存中获取一个 singletonFactory,回顾前文,能获取到
//由于这里的beanName=x,故而获取出来的工厂对象,能产生一个x半成品bean
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
//由于获取到了,进入if分支
if (singletonFactory != null) {
//调用工厂对象的getObject()方法,产生一个x的半成品bean
//怎么产生的?下文解释,比较复杂
singletonObject = singletonFactory.getObject();
//拿到了半成品的xbean之后,把他放到三级缓存;为什么?下文解释
this.earlySingletonObjects.put(beanName, singletonObject);
//然后从二级缓存清除掉x的工厂对象;?为什么,下文解释
this.singletonFactories.remove(beanName);
}
}
}
}
针对上面的源码我做一个简单的总结:首先spring从单例池当中获取x,前面说过获取不到,然后判断是否在正在创建bean的集合当中,前面分析过这个集合现在存在x,和y;所以if成立进入分支;进入分支spring直接从三级缓存中获取x,根据前面的分析三级缓存当中现在什么都没有,故而返回nll;进入下一个if分支,从二级缓存中获取一个ObjectFactory工厂对象;根据前面分析,二级缓存中存在x,故而可以获取到;跟着调用singletonFactory.getObject();拿到一个半成品的x bean对象;然后把这个x对象放到三级缓存,同时把二级缓存中x清除(此时二级缓存中只存在一个y了,而三级缓存中多了一个x);
问题1:
为什么首先是从三级缓存中取呢?
主要是为了性能,因为三级缓存中存的是一个x对象,如果能取到则不去二级找了;
哪有人会问二级有什么用呢?为什么一开始要存工厂呢?为什么一开始不直接存三级缓存?
这里稍微有点复杂,如果直接存到三级缓存,只能存一个对象,假设以前存这个对象的时候这对象的状态为xa,但是我们这里y要注入的x为xc状态,那么则无法满足;但是如果存一个工厂,工厂根据情况产生任意xa或者xb或者xc等等情况;比如说aop的情况下x注入y,y也注入x;而y中注入的x需要加代理(aop),但是加代理的逻辑在注入属性之后,也就是x的生命周期周到注入属性的时候x还不是一个代理对象,那么这个时候把x存起来,然后注入y,获取、创建y,y注入x,获取x;拿出来的x是一个没有代理的对象;但是如果存的是个工厂就不一样;首先把一个能产生x的工厂存起来,然后注入y,注入y的时候获取、创建y,y注入x,获取x,先从三级缓存获取,为null,然后从二级缓存拿到一个工厂,调用工厂的getObject();spring在getObject方法中判断这个时候x被aop配置了故而需要返回一个代理的x出来注入给y。
当然有的读者会问你不是前面说过getObject会返回一个当前状态的xbean嘛?
我说这个的前提是不去计较getObject的具体源码,因为这块东西比较复杂,需要去了解spring的后置处理器功能,这里先不讨论,总之getObject会根据情况返回一个x,但是这个x是什么状态,spring会自己根据情况返回;
问题2:为什么要从二级缓存remove?
因为如果存在比较复杂的循环依赖可以提高性能;比如x,y,z相互循环依赖,那么第一次y注入x的时候从二级缓存通过工厂返回了一个x,放到了三级缓存,而第二次z注入x的时候便不需要再通过工厂去获得x对象了。因为if分支里面首先是访问三级缓存;至于remove则是为了gc吧;
至此循环依赖的内容讲完,有错误欢迎指正,欢迎留言提问;
另外,欢迎关注笔者的B站:子路玩Java,里面有更加详细的视频讲解,后期我也会将视频传至西瓜视频,敬请期待!
我是子路,一个靠Java吃饭的男人。如果觉得还不错,请点个关注~