Spring IoC 源码解析:自定义标签的解析过程

Spring 中的标签分为默认标签和自定义标签两类,上一篇我们探究了默认标签的解析过程,当然在阅读源码的过程中我们也看到默认标签的解析过程中嵌套了对自定义标签的解析,这是因为默认标签中可以嵌套使用自定义标签,但是这和本篇所要讨论的自定义标签还是有些区别的,上一篇中介绍的自定义标签可以看做是 <bean/> 标签的子标签元素,而本篇所指的标签是与 <bean/> 这类标签平级的自定义标签。

一. 自定义标签的定义和使用方式

在具体开挖源码之前,我们还是来回忆一下自定义标签的定义和使用方式,整体上与上一篇 1.2 小节所定义的方式类似,但还是有些许差别。要自定义标签,分为 5 步:

  1. 创建标签实体类
  2. 定义标签的描述 XSD 文件
  3. 创建一个标签元素解析器,实现 BeanDefinitionParser 接口
  4. 创建一个 handler 类,继承自 NamespaceHandlerSupport
  5. 编写 spring.handlers 和 spring.schemas 文件

这里我们自定义实现一个类似 <alias/> 功能的标签来为指定的 bean 添加别名。第一步,先创建标签对应的实体:

1
2
3
4
5
6
7
public class Alias {

private String name;
private String alias;

// 省略 getter 和 setter
}

第二步,定义标签的 XSD 文件 custom-alias.xsd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.zhenchao.org/schema/alias"
xmlns:tns="http://www.zhenchao.org/schema/alias"
elementFormDefault="qualified">
<element name="alias">
<complexType>
<attribute name="id" type="string"/>
<attribute name="name" type="string"/>
<attribute name="parentName" type="string"/>
<attribute name="c_name" type="string"/>
<attribute name="c_alias" type="string"/>
</complexType>
</element>
</schema>

第三步,创建标签元素解析器,实现 BeanDefinitionParser 接口,这里我们继承该接口的子接口 AbstractSingleBeanDefinitionParser,并覆盖相应的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CustomBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

@Override
protected Class<?> getBeanClass(Element element) {
return Alias.class;
}

@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
String beanName = element.getAttribute("c_name");
Assert.hasText(beanName, "The 'name' in alias tag is missing!");
Assert.isTrue(parserContext.getRegistry().containsBeanDefinition(beanName), "No bean name " + beanName + " exist!");
String alias = element.getAttribute("c_alias");
Assert.hasText(beanName, "The 'alias' in alias tag is missing!");
String[] aliasArray = alias.replaceAll("\\s+", "").split("[,;]");
for (final String ali : aliasArray) {
parserContext.getRegistry().registerAlias(beanName, ali);
}
}
}

方法中的逻辑先判断对应的 beanName 是否存在,如果存在的话就建立 beanName 与 alias 之间的映射关系。

第四步,创建标签 handler 类,继承自 NamespaceHandlerSupport,用于注册第三步中定义的标签解析器:

1
2
3
4
5
6
7
8
public class CustomNamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
this.registerBeanDefinitionParser("alias", new CustomBeanDefinitionParser());
}

}

第五步,编写 spring.handlers 和 spring.schemas 文件:

spring.handlers

http://www.zhenchao.org/schema/alias=org.zhenchao.handler.CustomNamespaceHandler

spring.schemas

http://www.zhenchao.org/schema/alias.xsd=META-INF/custom-alias.xsd

接下来演示一下上述自定义标签的使用方式,首先需要在 <beans/> 标签属性中定义标签的命名空间:

1
2
3
4
5
6
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myalias="http://www.zhenchao.org/schema/alias"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.zhenchao.org/schema/alias http://www.zhenchao.org/schema/alias.xsd"

然后使用我们自定义的标签为已定义的 bean 添加别名:

1
2
<!-- my-parent-bean 是一个已定义的 bean -->
<myalias:alias id="my-alias" c_name="my-parent-bean" c_alias="aaa; bbb"/>

这样我们完成了利用自定义的标签为 my-parent-bean 添加别名的功能,接下来我们开挖自定义标签的解析过程。

二. 自定义标签的解析过程

再来回顾一下我们开始解析标签的入口函数 parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
// 解析默认标签(beans标签)
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 解析默认标签(子级嵌套)
this.parseDefaultElement(ele, delegate);
} else {
// 解析自定义标签(子级嵌套)
delegate.parseCustomElement(ele);
}
}
}
} else {
// 解析自定义标签
delegate.parseCustomElement(root);
}
}

上一篇中我们探究了默认标签的解析过程,也就是 parseDefaultElement 方法,接下来我们来探究自定义标签的解析过程,即 parseCustomElement 方法:

1
2
3
public BeanDefinition parseCustomElement(Element ele) {
return this.parseCustomElement(ele, null);
}
1
2
3
4
5
6
7
8
9
10
11
12
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
// 获取标签的命名空间
String namespaceUri = this.getNamespaceURI(ele);
// 提取自定义标签命名空间处理器
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 解析标签
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

上述方法首先会去获取自定义标签的命名空间定义,然后基于命名空间解析得到对应的 NamespaceHandler,最后调用 handler 对标签进行解析处理,本质上调用的就是前面自定义实现的 doParse 方法。我们先来看一下自定义标签 NamespaceHandler 的解析过程,位于 DefaultNamespaceHandlerResolver#resolve 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有已注册的handler集合
Map<String, Object> handlerMappings = this.getHandlerMappings();
// 获取namespaceUri对应的handler全程类名或handler实例
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
} else if (handlerOrClassName instanceof NamespaceHandler) {
// 已经解析过,直接返回handler实例
return (NamespaceHandler) handlerOrClassName;
} else {
// 未做过解析,则解析对应的类路径className
String className = (String) handlerOrClassName;
try {
// 使用反射创建handler实例
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// 初始化实例
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 调用init()方法
namespaceHandler.init();
// 缓存解析后的handler实例
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
} catch (ClassNotFoundException ex) {
throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "] not found", ex);
} catch (LinkageError err) {
throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]: problem with handler class file or dependent class", err);
}
}
}

方法中的逻辑可以概括如下:

  1. 从 spring.handlers 获取所有注册的 handler 集合
  2. 从集合中获取 namespace 对应 handler
  3. 如果 handler 已经被解析过则返回对应的 handler 实例,否则继续
  4. 利用反射创建 handler 实例,并初始化
  5. 调用 handler 的 init() 方法
  6. 缓存解析后的 handler 实例

上述过程中第 4 步稍微复杂一点,我们来看一下具体过程。我们在 spring.handlers 中会配置 namespaceUri 与对应 handler 全称类名的键值对:

http://www.zhenchao.org/schema/alias=org.zhenchao.handler.CustomNamespaceHandler

这里会拿到对应 handler 的全称类名,然后基于反射来创建 handler 实例,过程中会设置构造方法为 accessible。接下来就是轮到第五步中的调用 init 方法,该方法是由开发人员自己实现的,我们前面的例子中通过该方法将我们自定义的解析器 CustomBeanDefinitionParser 注册到 handler 实例中。接下来就是调用 handler 实例处理自定义标签:

1
2
3
4
5
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 寻找解析器并进行解析
return this.findParserForElement(element, parserContext) // 找到对应的解析器
.parse(element, parserContext); // 进行解析(这里的解析过程是开发者自定义实现的)
}

这里主要分为 获取 handler 实例执行解析 两个步骤,其中获取 handler 实例就是依据我们使用的标签名从之前的缓存 map 中拿到对应的对象,然后调用 handler 的 parse 方法执行解析逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public final BeanDefinition parse(Element element, ParserContext parserContext) {
// 1. 创建自定义标签BeanDefinition实例,并调用自定义解析器进行解析处理
AbstractBeanDefinition definition = this.parseInternal(element, parserContext);

if (definition != null && !parserContext.isNested()) { // nested:嵌套的
// definition实例存在且不是嵌套的
try {
// 2. 获取标签的id属性,id属性是必备的
String id = this.resolveId(element, definition, parserContext);
if (!StringUtils.hasText(id)) {
parserContext.getReaderContext().error(
"Id is required for element '" + parserContext.getDelegate().getLocalName(element) + "' when used as a top-level tag", element);
}

// 3. 获取name字段
String[] aliases = null;
if (this.shouldParseNameAsAliases()) {
String name = element.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(name)) {
aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}

// 4. 将AbstractBeanDefinition转化成BeanDefinitionHolder并注册
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
this.registerBeanDefinition(holder, parserContext.getRegistry());

// 5. 事件通知
if (this.shouldFireEvents()) {
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
this.postProcessComponentDefinition(componentDefinition);
parserContext.registerComponent(componentDefinition);
}
} catch (BeanDefinitionStoreException ex) {
parserContext.getReaderContext().error(ex.getMessage(), element);
return null;
}
}
return definition;
}

上述方法中的第一步是整个方法的核心,我们后面细讲,先来看一下第二、三步骤,对于自定义标签来说,id 属性是必备的,此外 Spring 还内置了 name 和 parentName 字段,这些名称是不允许使用的,否则达不到我们预期的结果,笔者第一次使用自定义标签时就踩了坑,用了 name 作为自定义标签属性名,结果就是各种奇怪的结果。

接下来看看第一步的逻辑,位于 AbstractSingleBeanDefinitionParser#parseInternal 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
// 初始化自定义标签实例
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
// 如果设置了parentName
String parentName = this.getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}

// 调用自定义BeanDefinitionParser中的getBeanClass方法
Class<?> beanClass = this.getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
} else {
// 如果自定义解析器没有重写getBeanClass方法,则检查子类是否重写了getBeanClassName方法
String beanClassName = this.getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}

builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));

if (parserContext.isNested()) {
// 如果当前标签是嵌套的,则使用父类的scope属性
builder.setScope(parserContext.getContainingBeanDefinition().getScope());
}

// 设置延迟加载
if (parserContext.isDefaultLazyInit()) {
builder.setLazyInit(true);
}

// 调用自定义解析器覆盖的doParse方法进行解析
this.doParse(element, parserContext, builder);

// 返回自定义标签的beanDefinition实例
return builder.getBeanDefinition();
}

上述方法中首先会初始化创建一个 BeanDefinitionBuilder 对象,然后依据配置设置对象的相应属性,其中包括调用我们之前在实现自定义标签解析器 CustomBeanDefinitionParser 时候覆盖的 getBeanClass 方法。然后会调用 doParse 方法,该方法由开发者实现,也是我们解析自定义标签的核心方法:

1
2
3
4
5
6
7
8
9
10
11
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
String beanName = element.getAttribute("c_name");
Assert.hasText(beanName, "The 'name' in alias tag is missing!");
Assert.isTrue(parserContext.getRegistry().containsBeanDefinition(beanName), "No bean name " + beanName + " exist!");
String alias = element.getAttribute("c_alias");
Assert.hasText(beanName, "The 'alias' in alias tag is missing!");
String[] aliasArray = alias.replaceAll("\\s+", "").split("[,;]");
for (final String ali : aliasArray) {
parserContext.getRegistry().registerAlias(beanName, ali);
}
}

最后,返回自定义标签对应的 beanDefinition 实例。

分析到这里,Spring 对于配置文件的解析工作已经做完了,容器将一个个 bean 的静态配置解析映射成为 beanDefinition 实例,并注册到容器的 Map 集合中,剩下的就是对 bean 实例的创建和初始化过程了,我们在下一篇中对这一过程的具体实现进行详细探究。


转载声明 : 版权所有,商业转载请联系作者,非商业转载请注明出处
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议
Powered by hexo & Theme by hiero   Copyright © 2015-2019 浙ICP备 16010916  号,指 · 间 All Rights Reserved.