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

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

自定义标签的定义与使用

在开始分析自定义标签的解析过程之前,我们还是通过示例演示一下自定义标签的定义和使用方式,整体与上一篇所介绍的类似,但还是有些许差别。自定义标签分为 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
1
http://www.zhenchao.org/schema/alias=org.zhenchao.handler.CustomNamespaceHandler
  • spring.schemas
1
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
<!-- myBean 是一个已定义的 bean -->
<myalias:alias id="myAlias" c_name="myBean" c_alias="aaa; bbb"/>

这样我们完成了利用自定义的标签为 myBean 添加别名的功能。

自定义标签的解析过程

了解了如果自定义和使用自定义标签,下面开始分析 Spring 如何解析自定义标签。首先,回顾一下开始解析标签的入口函数 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions,如下::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 解析默认标签
if (delegate.isDefaultNamespace(root)) {
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);
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public BeanDefinition parseCustomElement(Element ele) {
return this.parseCustomElement(ele, null);
}

public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取标签的命名空间
String namespaceUri = this.getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 解析自定义标签命名空间处理器
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
this.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 实现类,最后调用 NamespaceHandler#parse 方法对自定义标签进行解析处理,这里本质上调用的就是前面自定义实现的 CustomBeanDefinitionParser#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
34
35
36
37
38
39
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;
}
// 已解析过,直接返回 handler 实例
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
// 未解析过,基于 className 构造对应的 handler 实例,并注册
else {
String className = (String) handlerOrClassName;
try {
// 获取 handler 对应的 Class 对象
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");
}
// 实例化 handler
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 初始化 handler
namespaceHandler.init();
// 注册 handler
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
} catch (ClassNotFoundException ex) {
throw new FatalBeanException(
"Could not find NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]", ex);
} catch (LinkageError err) {
throw new FatalBeanException(
"Unresolvable class definition for NamespaceHandler class [" + className + "] for namespace [" + namespaceUri + "]", err);
}
}
}

上述方法的执行逻辑可以概括为:

  1. 从 spring.handlers 获取所有注册的 handler 集合;
  2. 从集合中获取 namespace 对应 handler 实例;
  3. 如果 handler 已经被解析过,则返回对应的 handler 实例;
  4. 否则,利用反射创建 handler 实例,并初始化;
  5. 注册解析得到的 handler 实例。

上述过程中的第 4 步稍微复杂一些,下面进一步说明一下具体过程。我们在 spring.handlers 文件中会配置 namespaceUri 与对应 handler 全称类名的映射关系:

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

所以,这一步会基于该配置获取到对应 handler 类的全称类名;然后基于反射机制创建 handler 实例;接下来就是调用 NamespaceHandler#init 方法对 handler 实例进行初始化。该方法是由开发人员自己实现的,我们前面的例子中通过该方法将我们自定义的解析器 CustomBeanDefinitionParser 注册到 handler 实例中。

完成了对 handler 实例的解析,接下来就是调用 NamespaceHandler#parse 方法处理自定义标签:

1
2
3
4
5
6
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 寻找对应的解析器
BeanDefinitionParser parser = this.findParserForElement(element, parserContext);
// 基于解析器执行对自定义标签的解析(这里的解析过程是开发者自定义实现的)
return (parser != null ? parser.parse(element, parserContext) : null);
}

上述实现主要分为 获取解析器解析自定义标签 两个步骤,其中获取解析器就是依据我们使用的标签名从之前注册的 map 数据结构中获取相应的对象,然后调用 AbstractBeanDefinitionParser#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);
// BeanDefinition 实例存在且不是嵌套的
if (definition != null && !parserContext.isNested()) {
try {
// 2. 获取标签的 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. 使用 BeanDefinitionHolder 封装 BeanDefinition 实例,便于注册到容器中
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) {
String msg = ex.getMessage();
parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), 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
40
41
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));

// 如果当前标签是嵌套的,则继承外围 bean 的 scope 属性
BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
if (containingBd != null) {
// Inner bean definition must receive same scope as containing bean.
builder.setScope(containingBd.getScope());
}

// 解析并设置延迟加载
if (parserContext.isDefaultLazyInit()) {
// Default-lazy-init applies to custom bean definitions as well.
builder.setLazyInit(true);
}

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

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

上述方法首先会初始化创建一个 BeanDefinitionBuilder 对象,然后依据配置设置对象的相应属性,其中会尝试调用自定义标签解析器覆盖实现的 AbstractSingleBeanDefinitionParser#getBeanClass 方法获取 bean 对应的 Class 对象。然后会调用 AbstractSingleBeanDefinitionParser#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 解析用户自定义标签的实现。分析到此,对于配置文件的解析过程也就基本分析完了。Spring 将一个个 bean 的静态配置解析成 BeanDefinition 实例注册到 IoC 容器中,接下去就可以调用 BeanFactory#getBean 方法获取 bean 实例了。建和初始化 bean 实例的过程也都由该方法触发,我们将在下一篇对这一过程的具体实现进行探究。

参考

  1. Spring 源码深度解析