前言
上一篇文章分析了Bean工厂的创建,其真正的实现类和核心为DefaultListableBeanFactory
,XML配置文件是封装成了Resource
,由XmlBeanDefinitionReader
进行了加载,最后在BeanDefinitionParserDelegate
解析类中将XML根元素中的属性进行了处理。现在所有的准备工作完成,正式进入我们严格意义上的“有效”标签的解析
前面说到,默认情况下DefaultBeanDefinitionDocumentReader
对于XML的前处理和后处理都为空实现,真正的处理逻辑在parseBeanDefinitions(Element, DefaultBeanDefinitionDocumentReader)
中
两次判断红框内代码判断根元素和其下所有子元素是否处在默认名称空间定义内,来看一下怎么判断的
根据获得的节点名称空间判断如果为空或者等于常量
BEANS_NAMESPACE_URI
即为默认名称空间,该常量的值为
http://www.springframework.org/schema/beans
,正是XML最基础的名称空间,回到图1,根据当前节点是否属于默认名称空间下分别调用红线处两个不同的方法,目前我们只分析默认名称空间下标签解析流程
我们根据判断的四个常量可知,这里的默认标签元素分别为
、
、
和
处理
大体可分为两步:1. 解析
;2. 注册
bean
实例,与本文名称契合的这两部分并不是为了让大家更好的理解Spring运行流程特意杜撰的,而是Spring内部确实是这么划分的,上图中的两行代码就对应了这两个部分,我们先来第一部分
解析流程经过一段方法调用走到解析委派类的
parseBeanDefinitionElement(Element, BeanDefinition)
中
标注1解析
的id和name属性,其中name属性可以配置多个,会将多个name转成alias数组。标注2判断id属性是否存在,如果存在赋值给
beanName
,不存在用别名数组中第一个别名作为
beanName
的值,标注3方法判断
beanName
或者别名数组中的别名没有使用过,也就是说一个XML配置文件中同id或者同名不能重复注册
也许有人产生疑问了,在第一篇文章中命名说到可以存在同名的
,这里怎么又不可以了呢?其实答案在保存使用过的名称集合
usedNames
中写的很清楚,在同一层次
下的所有
只能存在唯一不重复的
ids/names
,但是如果有多个XML配置文件,两个相同的
ids/names
出现在不同的配置文件中,这种情况是可以被允许的。初始化加载时
usedNames
内没有内容,每一次
的解析都会向内存储对应的
id/name
和所有的
alias
。回到图5标注4创建出
bean
的实例
标注1将当前
压入成员变量
parseState
内部栈的栈顶,该对象用于记录解析到的一些重要标签或者属性对象,当解析发生错误时就可以知道到底解析到哪个对象出现了问题。标注2很明显是解析
class
和
parent
属性,其中的最后一句会生成
AbstractBeanDefinition
的子类
GenericBeanDefinition
,其中根据是否设置
bean
的类加载器决定该类中的变量
beanClass
保存的是
className
还是类的实例。因为本文的例子中刚刚进入到解析XML和创建对应
BeanFactory
的流程,此时类加载器为
null
,为了证明这点我特意debug截图为证
从上图可以很清楚的看到此时
classLoader = null
,那么创建的
GenericBeanDefinition
中保存真实对象的变量
beanClass
保存的实际上是类的名称而不是类的实例,记住这点非常关键否则下面没法分析
图7标注3
parseBeanDefinitionAttributes(Element, String, BeanDefinition, AbstractBeanDefintion)
根据spring不同的版本解析诸如
scope
、
abstract
、
lazy-init
等属性。标注4三个方法解析
、
和
子标签,其中
用于向
BeanDefinition
中设置任意
key-value
对;
通常用于设置方法返回
bean
的类型;
使得一个类的某个方法可以被实现
MethodReplacer
接口的类的
reimplement(Object, Method, Object[])
代替。标注5中第一句就是大家熟悉的通过构造器实现注入的解析方式,对应的标签为
,
代码清单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. 处理
中对应的ref
和value
等属性;2. 解析
内部的子标签,具体流程我们来看一下
标注1解析出类型为
Element
,标签名不为
description
也不为
meta
的子标签,引用指向
subElement
。标注2解析
的
ref
和
value
属性,确保两个属性不能同时出现。标注3得到
ref
的值封装成
RuntimeBeanReference
对象返回。标注4得到
value
的值封装成
TypeStringValue
返回。如果上面解析到
的子标签元素就会调用标注5的方法,该方法内包含一些大家可能还比较熟悉的标签,比如
、
等,感兴趣的读者可以继续往下深入,这里就不做分析了
回到代码清单1看标注5处,上面说的在这个
if
中都是存在
index
属性的,这里就是判断解析到的
index
值是否已经存在,在
ConstructorArgumentValues
对象中持有一个
Map
集合,其中
key
即为下标,
value
为下标对应标签值封装的对象,如果通过验证就往
map
中塞入对应的键值对。标注6处对应着
else
的部分表示在没有
index
属性时解析
的过程,同样调用
parsePropertyValue(Element, BeanDefinition, String)
方法,封装
ValueHolder
实例,根据属性类型的不同进行不同的设置,之前存在
index
属性的标签对象存放在
map
中,而不存在
index
属性的标签对象放在链表中
绕了一个大圈子终于分析完
及其属性、子标签的解析流程,回到图7看标注5的第二句
parsePropertyElements(Element, BeanDefinition)
,该方法和解析其他标签一开始的思路一致,都是遍历所有子标签,判断标签名称是否为
property
,是则进入
parsePropertyElement(Element, BeanDefinition)
首先获得
必填属性
name
的值,如若没有记录错误日志并抛出异常,对于一个
BeanDefinition
来说,对象内存在一个
MutablePropertyValues
对象,该对象中维护了一个
List
集合,该集合就保存了所有的
的
name-value/ref
对,既然如此,在解析的过程中必然要判断集合中是否已经存在同名称的
PropertyValue
,如果存在自然也会报错,不存在就需要真正解析
,而解析的过程和解析
调用的方法一样,见图9,与前者解析唯一不同的地方在于第三个参数,对于
来说
propertyName
为
null
,而
自然就是属性
name
的值了,解析并创建
PropertyValue
对象后,同样要使用
parseMetaElement(Element, BeanMetadataAttributeAccessor)
解析内嵌的
标签,最后塞入
List
中
图7标注5中最后一句用来解析
,在注解注入大行其道的编程界,相信也很少人使用这种方式进行注入操作,在这里就不展开分析了,关于几种注解注入的方式的源码解析后面会有单独文章分析。至此我们终于又回到了图5解析
大纲流程,看一看标注4和标注5之间的代码做了什么,如果我们在配置
时既没有填写
id
,也没有填写
name
,Spring会为我们生成一个
name
,由于
containingBean
在这里为
null
,因此最终的流程会走到
else
内,最终使用
BeanDefinitionReaderUtils
来生成
beanName
首先获得
BeanDefinition
中的
class name
,因为上面的步骤已经解析过
,此时必然有值,再有第三个参数
isInnerBean = false
(至于为什么请读者顺着流程走一次便知),因此最后形成返回值的公式即为
while
中的
generatedBeanName + GENERATED_BEAN_NAME_SEPARATOR + counter;
,其中的常量值为
#
,举个例子,如果
,那么最终生成的
beanName
即为
com.xiaomi.Student#0
。将解析的所有这一些按图5标注5代码封装成
BeanDefinitionHolder
后标志着
全部解析完
下面我们开始
Bean
实例的注册流程进入图3第二个下划线代码内,在正式分析之前我们需要先看几个重要的成员变量,第一篇文章中曾经说过
BeanFactory
的核心实现类为
DefaultListableBeanFactory
,在该类中有几个非常重要的成员变量
先来说两个现在要用到的,
beanDefinitionMap
和
beanDefinitionNames
,前者保存有
id/beanName-bean实例
键值对,后者保存所有的
beanName
注册
bean
的逻辑远远没有解析
的复杂,上图中的红框内即为核心代码。首先从
beanDefinitionMap
根据
beanName
查找是否存在对应的实例,如果存在判断是否开启了实例覆盖标识,没有开启抛出异常,再将
beanName
和对应实例放入
beanDefinitionMap
中,最后一句的作用是清除所有
beanName
所属实例及其衍生类的本地缓存信息
后记
即便有意隐去的非重点流程,
解析及注册的调用关系依然很深,我们宏观上只需要记住经过本文对应的处理,Spring解析了所有XML配置文件,生成的bean工厂,但此时bean工厂中
对应的实例并没有真正创建。回想Spring解析之IoC:XML配置文件的加载及BeanFactory的创建中图5,Spring初始化的总纲,现在也才走到第二步,漫漫长路刚刚开始,继续努力吧!