Spring的BeanDefinition解析

Spring生成bean的过程中,我们提到了Spring会先加载配置文件中的BeanDefinition,然后才会getBean。像普通的<bean>标签,我们也能写一个简单的解析工具将它转换为BeanDefinition,而像<context:component-scan>这样非基本的bean定义又是怎么解析的呢?

Bean的配置解析

在之前的文章中,我们讲过了xml是如何被读取的,那读取到xml中的每一个节点后,又是怎么解析的?

在DefaultBeanDefinitionDocumentReader类中有一个方法doRegisterBeanDefinitions就是用来注册beanDefinition的。它的入参就是总节点<beans>,作用是注册这个节点下的所有bean。

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
/**
* Register each bean definition within the given root {@code <beans/>} element.
*/
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

其中parseBeanDefinitions(root, this.delegate);就是真正解析xml节点到beanDefinition的。

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
/**
* Parse the elements at the root level in the document:
* "import", "alias", "bean".
* @param root the DOM root element of the document
*/
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)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

不要小看这个方法,parseDefaultElement(ele, delegate);就是解析普通<bean>标签的,而delegate.parseCustomElement(ele);就是用来解析定制化的标签的。BeanDefinitionParserDelegate中parseCustomElement方法被调用。他的作用就是通过标签找到相应的解析器解析成beanDefinition。

1
2
3
4
5
6
7
8
9
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
String namespaceUri = 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));
}

在这个方法中,getNamespaceURI比较好理解,就是通过这个标签获取其命名空间,例如<context:component-scan>标签对应的命名空间就是http://www.springframework.org/schema/context。命名空间找到了,命名空间handler又是怎么回事?原来在spring的jar包中,在/META-INF/文件夹中有一个spring.handlers文件,这个文件记录了命名空间与具体handler类的对应关系。

例如spring-context这个jar包中的spring.handlers文件内容是:

1
2
3
4
5
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler

这个handler是干啥的?org.springframework.beans.factory.xml.NamespaceHandler接口表明,这个接口可以读取节点信息并转换为BeanDefinition(BeanDefinition parse(Element element, ParserContext parserContext);)。

一个命名空间不可能只有一种标签元素,如果就直接实现NamespaceHandler来解析beanDefinition,那代码可真不好看。于是spring写了一个抽象类NamespaceHandlerSupport,允许抽象类的继承类向他注册不同元素的解析器org.springframework.beans.factory.xml.BeanDefinitionParser,真正解析元素的是这些解析器。而这些解析器又是怎么被注册的?NamespaceHandler接口有一个init方法,NamespaceHandlerSupport可以让继承类在这个方法里注册元素解析器。

例如上边提到的ContextNamespaceHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ContextNamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}

}

接下来就是各找各妈了,给你一个节点元素,让你写一个解析器简单吗?是不是比摸不着头脑好多了。具体的BeanDefinitionParser是怎么实现的这里就不具体展开了,相信和处理<bean>元素解析器差不了太多。

自定义配置解析器

如果我也想自己实现一个自定义的解析器该怎么办?

首先你得有自己的命名空间对应的xsd文件,然后引入到xml配置中,然后添加元素使用。其次,你得把你的xsd文件位置映射关系写到自己项目的/META-INF/spring.schemas中。

类似下面这种写法即可

1
2
3
4
5
6
7
8
9
10
11
http\://www.springframework.org/schema/tool/spring-tool.xsd=org/springframework/beans/factory/xml/spring-tool-4.3.xsd
http\://www.springframework.org/schema/util/spring-util-2.0.xsd=org/springframework/beans/factory/xml/spring-util-2.0.xsd
http\://www.springframework.org/schema/util/spring-util-2.5.xsd=org/springframework/beans/factory/xml/spring-util-2.5.xsd
http\://www.springframework.org/schema/util/spring-util-3.0.xsd=org/springframework/beans/factory/xml/spring-util-3.0.xsd
http\://www.springframework.org/schema/util/spring-util-3.1.xsd=org/springframework/beans/factory/xml/spring-util-3.1.xsd
http\://www.springframework.org/schema/util/spring-util-3.2.xsd=org/springframework/beans/factory/xml/spring-util-3.2.xsd
http\://www.springframework.org/schema/util/spring-util-4.0.xsd=org/springframework/beans/factory/xml/spring-util-4.0.xsd
http\://www.springframework.org/schema/util/spring-util-4.1.xsd=org/springframework/beans/factory/xml/spring-util-4.1.xsd
http\://www.springframework.org/schema/util/spring-util-4.2.xsd=org/springframework/beans/factory/xml/spring-util-4.2.xsd
http\://www.springframework.org/schema/util/spring-util-4.3.xsd=org/springframework/beans/factory/xml/spring-util-4.3.xsd
http\://www.springframework.org/schema/util/spring-util.xsd=org/springframework/beans/factory/xml/spring-util-4.3.xsd

接下来,你得在/META-INF/spring.handlers文件中写上你的命名空间对应的自定义NamespaceHandler。

接下来,在自定义NamespaceHandler的init方法中注册上节点元素对应的自定义BeanDefinitionParser解析器。

最后,在你的自定义BeanDefinitionParser解析器中实现从节点元素到beanDefinition的解析代码即可。

小节

BeanDefinition的解析流程如下:

  1. 读取xml配置文件解析成节点元素集合<beans>
  2. 挨个解析<beans>中的子节点。
  • 2.1 如果子节点是<bean>元素,那就走普通的beanDefinition解析。
  • 2.2 如果不是<bean>元素,那就走定制化beanDefinition解析。
    • 2.2.1 通过节点元素获取对应的命名空间
    • 2.2.2 通过spring的jar包中/META-INF/spring.handlers文件找到相对象的处理器handler,如果该处理器没有被初始化并缓存,则实例化它(这里不走bean的生成那套流程),调用他的init方法。
    • 2.2.3 调用处理器handler的parse方法,parse方法会根据节点元素来调用具体的注册的BeanDefinitionParser来解析元素生成beanDefinition

在自定义配置解析器小节中,我们可以看到自己写一个配置解析器还是比较有难度的。像mybatis就不采用这种自定义元素的方式来实现自己的特殊bean加载的beanDefinition,而是采用实现BeanDefinitionRegistryPostProcessor接口org.mybatis.spring.mapper.MapperScannerConfigurer,向已有的BeanDefinitionRegistry中注册beanDefinition。