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

前言

我是子路,一个把Java当饭吃的人。

笔者之前在华南谷歌搬砖,在系统架构设计、分布式、微服务、高并发、高可用等技术架构具有丰富的实战经验。对市面上主流的开源框架源码——spring、nacos,springboot、JDK并发工具等等都有深入的研究。

Spring是Java语言里面一个非常重要的框架,可以说任何一个学Java的人都必须要接触到Spring。

这里笔者先给大家好好从源码的角度来讲讲Spring。

正文

众所周知spring在默认单例的情况下是支持循环引用的。

为了节省图片大小我把那些可以动得gif图片做成了只循环一次,如果看到图片不动了请右键选择在新标签打开,那么图片就会动,手机用户则更简单,直接手指点击图片便能看到动图,每张gif我都标识了,如果没有标识则为静态图片;

Appconfig.java类的代码

@Configurable
@ComponentScan("com.shadow")
public class Appconfig {
}

X.java类的代码

package com.shadow.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class X {

    @Autowired
    Y y;

    public X(){
        System.out.println("X create");
    }
}

Y.java类的代码

package com.shadow.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Y {
    @Autowired
    X x;

    public Y(){
        System.out.println("Y create");
    }
}

这两个类非常简单,就是相互引用了对方,也就是我们常常的说的循环依赖,spring是允许这样的循环依赖(前提是单例的情况下的,非构造方法注入的情况下)

运行这段代码的结果下图

注意这是张gif,如果你看着不动请参考我上面说的方法

上面代码从容器中能正常获取到Xbean,说明循环依赖成功。但是spring的循环依赖其实是可以关闭的,spring提供了api来关闭循环依赖的功能。当然你也可以修改spring源码来关闭这个功能,这里笔者为了提高逼格,就修改一下spring的源码来关闭这个功能,老话说:要想高明就得装逼。

下图是我修改spring源码运行的结果

我在AnnotationConfigApplicationContext的构造方法中加了一行setAllowCircularReferences(false);结果代码异常,循环依赖失败:

那么为什么setAllowCircularReferences(false);会关闭循环依赖呢?首要明白spring的循环依赖是怎么做到的呢?spring源码当中是如何处理循环依赖的? 分析一下所谓的循环依赖其实无非就是属性注入,或者就是大家常常说的自动注入, 故而搞明白循环依赖就需要去研究spring自动注入的源码;spring的属性注入属于spring bean的生命周期一部分;怎么理解spring bean的生命周期呢?注意笔者这里并不打算对bean的生命周期大书特书,只是需要读者理解生命周期的概念,细节以后在计较;

要理解bean的生命周期首先记住两个概念——spring bean(一下简称bean)和对象:

1、spring bean——受spring容器管理的对象,可能经过了完整的spring bean生命周期(为什么是可能?难道还有bean是没有经过bean生命周期的?答案是有的,具体我们后面文章分析),最终存在spring容器当中;一个bean一定是个对象。

2、对象——任何符合java语法规则实例化出来的对象,但是一个对象并不一定是spring bean。

所谓的bean的生命周期就是磁盘上的类通过spring扫描,然后实例化,跟着初始化,继而放到容器当中的过程;

我画了一张简单图来阐述一下spring bean的生命周期大概有哪些步骤:

上图就是spring容器初始化bean的大概过程(至于详细的过程,后面文章再来介绍);

文字总结一下:

  1. 实例化一个ApplicationContext的对象;
  2. 调用bean工厂后置处理器完成扫描;
  3. 循环解析扫描出来的类信息;
  4. 实例化一个BeanDefinition对象来存储解析出来的信息;
  5. 把实例化好的beanDefinition对象put到beanDefinitionMap当中缓存起来,以便后面实例化bean;
  6. 再次调用bean工厂后置处理器;
  7. 当然spring还会干很多事情,比如国际化,比如注册BeanPostProcessor等等,如果我们只关心如何实例化一个bean的话那么这一步就是spring调用finishBeanFactoryInitialization方法来实例化单例的bean,实例化之前spring要做验证,需要遍历所有扫描出来的类,依次判断这个bean是否Lazy,是否prototype,是否abstract等等;
  8. 如果验证完成spring在实例化一个bean之前需要推断构造方法,因为spring实例化对象是通过构造方法反射,故而需要知道用哪个构造方法;
  9. 推断完构造方法之后spring调用构造方法反射实例化一个对象;注意我这里说的是对象、对象、对象;这个时候对象已经实例化出来了,但是并不是一个完整的bean,最简单的体现是这个时候实例化出来的对象属性是没有注入,所以不是一个完整的bean;
  10. spring处理合并后的beanDefinition(合并?是spring当中非常重要的一块内容,后面的文章我会分析);
  11. 判断是否支持循环依赖,如果支持则提前把一个工厂存入singletonFactories——map;
  12. 判断是否需要完成属性注入
  13. 如果需要完成属性注入,则开始注入属性
  14. 判断bean的类型回调Aware接口
  15. 调用生命周期回调方法
  16. 如果需要代理则完成代理
  17. put到单例池——bean完成——存在spring容器当中

用一个例子来证明上面的步骤,结合一些运行时期的动态图片

为了节省图片大小我把那些可以动得gif图片做成了只循环一次,如果看到图片不动了请右键选择在新标签打开,那么图片就会动,手机用户则更简单,直接手指点击图片便能看到动图,每张gif我都标识了,如果没有标识则为静态图片;

Z.java的源码

package com.shadow.service;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class Z implements ApplicationContextAware {
    @Autowired
    X x;//注入X

    //构造方法
    public Z(){
        System.out.println("Z create");
    }

    //生命周期初始化回调方法
    @PostConstruct
    public void zinit(){
        System.out.println("call z lifecycle init callback");
    }

    //ApplicationContextAware 回调方法
    @Override
    public void setApplicationContext(ApplicationContext ac) {
        System.out.println("call aware callback");
    }
}

来看看Z的生命周期,注意下图当中的字幕,会和上面的17个步骤一一对应。

下图是第一步到第六步,请自行对应

接下来我们通过各种图片分析一下springbean的生命周期,读者只需要看图搞明白流程,至于图中涉及的源码,分析完流程之后再来解释;

图① 注意这是张gif,如果你看着不动请参考我上面说的方法

在研究其他步骤之前,首先了解spring大概在什么时候实例化bean的

图② 注意这是张gif,如果你看着不动请参考我上面说的方法

上图可以知道spring在AbstractApplicationContext#finishBeanFactoryInitialization方法中完成了bean的实例化。这点需要记住

然后通过图片来说明一下第7步

图③ 注意这是张gif,如果你看着不动请参考我上面说的方法

接下来spring需要推断构造方法,然后通过推断出来的构造方法反射实例化对象,也就是上面说的8步和第9

当然有可能推断不出来构造方法;关于这块知识博主后面更新文章
图④ 注意这是张gif,如果你看着不动请参考我上面说的方法

上图说明spring是通过createBeanInstance(beanName, mbd, args);完成了推断构造方法和实例化的事情那么接下来便要执行第10步处理合并后的beanDefinition对象,这一块内容特别多,读者可以先不必要理解,后面文章会解释;

图⑤ 注意这是张gif,如果你看着不动请参考我上面说的方法

仔细看上图,其实这个时候虽然Z被实例化出来了,但是并没有完成属性的注入;其中的X属性为null,而且里面的Aware接口的方法也没有调用,再就是@PostConstruct方法也没有调用,再一次说明他不是一个完整的bean,这里我们只能说z是个对象;

继而applyMergedBeanDefinitionPostProcessors方法就是用来处理合并后的beanDefinition对象;

跟着第11,判断是否支持循环依赖,如果支持则提前暴露一个工厂对象,注意是工厂对象

图⑥ 注意这是张gif,如果你看着不动请参考我上面说的方法

12步,spring会判断是否需要完成属性注入(spring默认是需要的,但是程序员可以扩展spring,根据情况是否需要完成属性注入);如果需要则spring完成13步——属性注入,也就是所谓的自动注入;

图⑦ 注意这是张gif,如果你看着不动请参考我上面说的方法

14、15、16

图⑧ 注意这是张gif,如果你看着不动请参考我上面说的方法

默认情况 至此一个bean完成初始化,被put到单例池,也是对上文说的17个步骤的一个证明;这说明一个bean在spring容器当中被创建出来是有一个过程的,这个过程就是所谓的bean的生命周期,我们的循环依赖也是在这个生命周内完成的。下面我们具体来分析这些步骤

由于bean的生命周期特别复杂本文只对涉及到循环依赖的步骤做分析,其他生命周期的步骤我会在后续博客中分析,可以继续关注博主

回顾上面的图②图③ 我们知道spring的bean是在AbstractApplicationContext#finishBeanFactoryInitialization()方法完成的初始化,即循环依赖也在这个方法里面完成的。该方法里面调用了一个非常重要的方法 doGetBean的方法

照例用图片来说明一下吧

图⑨ 注意这是张gif,如果你看着不动请参考我上面说的方法

doGetBean方法内容有点多,这个方法非常重要,不仅仅针对循环依赖,甚至整个spring bean生命周期中这个方法也有着举足轻重的地位,读者可以认真看看笔者的分析。需要说明的是我为了更好的说清楚这个方法,我把代码放到文章里面进行分析;但是删除了一些无用的代码;比如日志的记录这些无关紧要的代码。下面重点说这个doGetBean方法。

首先笔者把精简后的代码贴出来方便大家阅读:

protected <T> T doGetBean(final String name, 
                    @Nullable final Class<T> requiredType,
                    @Nullable final Object[] args, 
                    boolean typeCheckOnly)
                    throws BeansException {
    //读者可以简单的认为就是对beanName做一个校验特殊字符串的功能
    //我会在下次更新博客的时候重点讨论这个方法
    //transformedBeanName(name)这里的name就是bean的名字
   final String beanName = transformedBeanName(name);

   //定义了一个对象,用来存将来返回出来的bean
   Object bean;

    //deGetBean-1
   Object sharedInstance = getSingleton(beanName);

    //deGetBean-2
    if (sharedInstance != null && args == null) {
      bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
   }else{
        deGetBean-3
        if (isPrototypeCurrentlyInCreation(beanName)) {
            throw new BeanCurrentlyInCreationException(beanName);
      }else{
        //doGetBean-4
        if (mbd.isSingleton()) {
            sharedInstance = getSingleton(beanName, () -> {
               try {
                  return createBean(beanName, mbd, args);
               }
               catch (BeansException ex) {
                  destroySingleton(beanName);
                  throw ex;
               }
            });

      }
   }
 }

注意:上面的代码是我对doGetBean方法进行了删减的代码,只保留了和本文讨论的循环依赖有关的代码,完整版可以参考spring的源码org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

好了,今天就讲到这里,下一篇文章笔者将对上述代码逐行来解释,如果大家觉得笔者写的还不错,或者感兴趣的话,可以点一个关注支持一下笔者!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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