源码深度解析spring的循环引用(三)——终章

前言

大家好,我是子路,一个靠Java吃饭的男人

前两篇文章:

源码深度解析spring的循环引用(一)——生命周期

源码深度解析spring的循环引用(二)——逐条解读代码

已经介绍完了循环依赖在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吃饭的男人。如果觉得还不错,请点个关注~

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