Spring解析之IoC:<bean>解析及Bean的注册

前言
上一篇文章分析了Bean工厂的创建,其真正的实现类和核心为DefaultListableBeanFactory,XML配置文件是封装成了Resource,由XmlBeanDefinitionReader进行了加载,最后在BeanDefinitionParserDelegate解析类中将XML根元素<beans>中的属性进行了处理。现在所有的准备工作完成,正式进入我们严格意义上的“有效”标签的解析

前面说到,默认情况下DefaultBeanDefinitionDocumentReader对于XML的前处理和后处理都为空实现,真正的处理逻辑在parseBeanDefinitions(Element, DefaultBeanDefinitionDocumentReader)

图1. DefaultBeanDefinitionDocumentReader的parseBeanDefinitions(Element, BeanDefinitionParserDelegate)

两次判断红框内代码判断根元素和其下所有子元素是否处在默认名称空间定义内,来看一下怎么判断的
图2. BeanDefinitionParserDelegate中判断是否默认名称空间逻辑

根据获得的节点名称空间判断如果为空或者等于常量BEANS_NAMESPACE_URI即为默认名称空间,该常量的值为http://www.springframework.org/schema/beans,正是XML最基础的名称空间,回到图1,根据当前节点是否属于默认名称空间下分别调用红线处两个不同的方法,目前我们只分析默认名称空间下标签解析流程
图3. 解析默认标签元素

我们根据判断的四个常量可知,这里的默认标签元素分别为<import><alias><bean><beans>
图4. DefaultBeanDefinitionDocumentReader的processBeanDefinition(Element, BeanDefinitionParserDelegate)

处理<bean>大体可分为两步:1. 解析<bean>;2. 注册bean实例,与本文名称契合的这两部分并不是为了让大家更好的理解Spring运行流程特意杜撰的,而是Spring内部确实是这么划分的,上图中的两行代码就对应了这两个部分,我们先来第一部分
<bean>解析流程经过一段方法调用走到解析委派类的parseBeanDefinitionElement(Element, BeanDefinition)
图5. BeanDefinitionParserDelegate的parseBeanDefinitionElement(Element, BeanDefinition)

标注1解析<bean>的id和name属性,其中name属性可以配置多个,会将多个name转成alias数组。标注2判断id属性是否存在,如果存在赋值给beanName,不存在用别名数组中第一个别名作为beanName的值,标注3方法判断beanName或者别名数组中的别名没有使用过,也就是说一个XML配置文件中同id或者同名不能重复注册
图6. BeanDefinitionParserDelegate的checkNameUniqueness(String, List<String>, Element)

也许有人产生疑问了,在第一篇文章中命名说到可以存在同名的<bean>,这里怎么又不可以了呢?其实答案在保存使用过的名称集合usedNames中写的很清楚,在同一层次<beans>下的所有<bean>只能存在唯一不重复的ids/names,但是如果有多个XML配置文件,两个相同的ids/names出现在不同的配置文件中,这种情况是可以被允许的。初始化加载时usedNames内没有内容,每一次<bean>的解析都会向内存储对应的id/name和所有的alias。回到图5标注4创建出bean的实例
图7. BeanDefinitionParserDelegate的parseBeanDefinitionElement(Element, String, BeanDefinition)

标注1将当前<bean>压入成员变量parseState内部栈的栈顶,该对象用于记录解析到的一些重要标签或者属性对象,当解析发生错误时就可以知道到底解析到哪个对象出现了问题。标注2很明显是解析classparent属性,其中的最后一句会生成AbstractBeanDefinition的子类GenericBeanDefinition,其中根据是否设置bean的类加载器决定该类中的变量beanClass保存的是className还是类的实例。因为本文的例子中刚刚进入到解析XML和创建对应BeanFactory的流程,此时类加载器为null,为了证明这点我特意debug截图为证
图8. 创建GenericBeanDefinition时debug图

从上图可以很清楚的看到此时classLoader = null,那么创建的GenericBeanDefinition中保存真实对象的变量beanClass保存的实际上是类的名称而不是类的实例,记住这点非常关键否则下面没法分析
图7标注3parseBeanDefinitionAttributes(Element, String, BeanDefinition, AbstractBeanDefintion)根据spring不同的版本解析诸如scopeabstractlazy-init等属性。标注4三个方法解析<meta><lookup-method><replaced-method>子标签,其中<meta>用于向BeanDefinition中设置任意key-value对;<lookup-method>通常用于设置方法返回bean的类型;<replaced-method>使得一个类的某个方法可以被实现MethodReplacer接口的类的reimplement(Object, Method, Object[])代替。标注5中第一句就是大家熟悉的通过构造器实现注入的解析方式,对应的标签为<constructor-arg>代码清单1

public void parseConstructorArgElement(Element ele, BeanDefinition bd) {
  //    (1)
  String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE);
  String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE);
  String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
  //    (2)
  if (StringUtils.hasLength(indexAttr)) {
    try {
      int index = Integer.parseInt(indexAttr);
      if (index < 0) {
        error("'index' cannot be lower than 0", ele);
      }
      else {
        try {
          //    (3)
          this.parseState.push(new ConstructorArgumentEntry(index));
          //    (4)
          Object value = parsePropertyValue(ele, bd, null);
          ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
          if (StringUtils.hasLength(typeAttr)) {
            valueHolder.setType(typeAttr);
          }
          if (StringUtils.hasLength(nameAttr)) {
            valueHolder.setName(nameAttr);
          }
          valueHolder.setSource(extractSource(ele));
          //      (5)
          if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) {
            error("Ambiguous constructor-arg entries for index " + index, ele);
          }
          else {
            bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder);
          }
        }
        finally {
          this.parseState.pop();
        }
      }
    }
    catch (NumberFormatException ex) {
      error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele);
    }
  }
  else {
    try {
      //      (6)
      this.parseState.push(new ConstructorArgumentEntry());
      Object value = parsePropertyValue(ele, bd, null);
      ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
      if (StringUtils.hasLength(typeAttr)) {
        valueHolder.setType(typeAttr);
      }
      if (StringUtils.hasLength(nameAttr)) {
        valueHolder.setName(nameAttr);
      }
      valueHolder.setSource(extractSource(ele));
      bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder);
    }
    finally {
      this.parseState.pop();
    }
  }
}

我们可以根据参数的三种属性之一进行注入,第一种为下标,初始从0开始;第二种为根据参数类型,比如是java.lang.String还是java.lang.Integer,注意,对于基本数据类型是无法通过构造器注入的,需要转成基本数据类型的包装数据类型;第三种是通过参数的名称注入,这种没什么好说的。只有采用第一种基于下标的注入方式才会进入到if中,标注3首先构建出一个Entry的实现类ConstructorArgumentEntry,并压入栈顶,这里的Entry是变量parseState的一个内部标记接口,只有实现该接口的对象才能被放入parseState内部的栈内
标注4主要做了两件事:1. 处理<constructor-arg>中对应的refvalue等属性;2. 解析<constructor-arg>内部的子标签,具体流程我们来看一下

图9. BeanDefinitionParserDelegate的parsePropertyValue(Element, BeanDefinition, String)

标注1解析出类型为Element,标签名不为description也不为meta的子标签,引用指向subElement。标注2解析<constructor-arg>refvalue属性,确保两个属性不能同时出现。标注3得到ref的值封装成RuntimeBeanReference对象返回。标注4得到value的值封装成TypeStringValue返回。如果上面解析到<constructor-arg>的子标签元素就会调用标注5的方法,该方法内包含一些大家可能还比较熟悉的标签,比如<list><map>等,感兴趣的读者可以继续往下深入,这里就不做分析了
回到代码清单1看标注5处,上面说的在这个if中都是存在index属性的,这里就是判断解析到的index值是否已经存在,在ConstructorArgumentValues对象中持有一个Map<Integer, ValueHolder>集合,其中key即为下标,value为下标对应标签值封装的对象,如果通过验证就往map中塞入对应的键值对。标注6处对应着else的部分表示在没有index属性时解析<constructor-arg>的过程,同样调用parsePropertyValue(Element, BeanDefinition, String)方法,封装ValueHolder实例,根据属性类型的不同进行不同的设置,之前存在index属性的标签对象存放在map中,而不存在index属性的标签对象放在链表中
绕了一个大圈子终于分析完<constructor-arg>及其属性、子标签的解析流程,回到图7看标注5的第二句parsePropertyElements(Element, BeanDefinition),该方法和解析其他标签一开始的思路一致,都是遍历所有子标签,判断标签名称是否为property,是则进入parsePropertyElement(Element, BeanDefinition)
图10. BeanDefinitionParserDelegate的parsePropertyElement(Element, BeanDefinition)

首先获得<Property>必填属性name的值,如若没有记录错误日志并抛出异常,对于一个BeanDefinition来说,对象内存在一个MutablePropertyValues对象,该对象中维护了一个List<PropertyValue>集合,该集合就保存了所有的<property>name-value/ref对,既然如此,在解析的过程中必然要判断集合中是否已经存在同名称的PropertyValue,如果存在自然也会报错,不存在就需要真正解析<property>,而解析的过程和解析<constructor-arg>调用的方法一样,见图9,与前者解析唯一不同的地方在于第三个参数,对于<constructor-arg>来说propertyNamenull,而<property>自然就是属性name的值了,解析并创建PropertyValue对象后,同样要使用parseMetaElement(Element, BeanMetadataAttributeAccessor)解析内嵌的<meta>标签,最后塞入List<PropertyValue>
图7标注5中最后一句用来解析<qualifer>,在注解注入大行其道的编程界,相信也很少人使用这种方式进行注入操作,在这里就不展开分析了,关于几种注解注入的方式的源码解析后面会有单独文章分析。至此我们终于又回到了图5解析<bean>大纲流程,看一看标注4和标注5之间的代码做了什么,如果我们在配置<bean>时既没有填写id,也没有填写name,Spring会为我们生成一个name,由于containingBean在这里为null,因此最终的流程会走到else内,最终使用BeanDefinitionReaderUtils来生成beanName
图11. BeanDefinitionReaderUtils的generateBeanName(BeanDefinition, BeanDefinitionRegistry, boolean)

首先获得BeanDefinition中的class name,因为上面的步骤已经解析过<bean class="">,此时必然有值,再有第三个参数isInnerBean = false(至于为什么请读者顺着流程走一次便知),因此最后形成返回值的公式即为while中的generatedBeanName + GENERATED_BEAN_NAME_SEPARATOR + counter;,其中的常量值为#,举个例子,如果<bean class = "com.xiaomi.Student/>",那么最终生成的beanName即为com.xiaomi.Student#0。将解析的所有这一些按图5标注5代码封装成BeanDefinitionHolder后标志着<bean>全部解析完
下面我们开始Bean实例的注册流程进入图3第二个下划线代码内,在正式分析之前我们需要先看几个重要的成员变量,第一篇文章中曾经说过BeanFactory的核心实现类为DefaultListableBeanFactory,在该类中有几个非常重要的成员变量
图12. DefaultListableBeanFactory中成员变量

先来说两个现在要用到的,beanDefinitionMapbeanDefinitionNames,前者保存有id/beanName-bean实例键值对,后者保存所有的beanName
图13. DefaultListableBeanFactory的registerBeanDefinition(String, BeanDefinition)

注册bean的逻辑远远没有解析<bean>的复杂,上图中的红框内即为核心代码。首先从beanDefinitionMap根据beanName查找是否存在对应的实例,如果存在判断是否开启了实例覆盖标识,没有开启抛出异常,再将beanName和对应实例放入beanDefinitionMap中,最后一句的作用是清除所有beanName所属实例及其衍生类的本地缓存信息

后记
即便有意隐去的非重点流程,<bean>解析及注册的调用关系依然很深,我们宏观上只需要记住经过本文对应的处理,Spring解析了所有XML配置文件,生成的bean工厂,但此时bean工厂中<bean>对应的实例并没有真正创建。回想Spring解析之IoC:XML配置文件的加载及BeanFactory的创建中图5,Spring初始化的总纲,现在也才走到第二步,漫漫长路刚刚开始,继续努力吧!

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

推荐阅读更多精彩内容