第4章 自定义标签的解析
在之前的章节中,我们提到了在
Spring
中存在默认标签与自定义标签两种,而在上一章节中我们分析了Spring
中自定义标签的加载过程.同样,我们还是先再次回顾一下,当完成从配置文件到Document
的转换并提取对应的root
后,将开始了所有元素的解析,而在这一过程中便开始了默认标签与自定义标签两中格式的区分,方法如下:
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);
}
}
在本章中,所有的功能都是围绕其中的一句代码
delegate.parseCustomElement(root)
开展的.从上面的函数我们可以看出,当Spring
拿到一个元素时首先要做的是根据命名空间进行解析,如果是默认的命名空间,则使用parseDefaultElement
方法进行元素解析,否则使用parseCustomElement
方法进行解析.在分析自定义标签的解析过程前,我们先了解一下自定义标签的使用过程.
4.1 自定义标签使用
在很多情况下,我们需要为系统提供可配置化支持,简单的做法可以直接基于
Spring
的标准bean
来配置,但配置较为复杂或者需要更多丰富控制的时候,会显得非常笨拙.一般的做法会用原生态的方式去解析定义好的XML
文件,然后转化为配置对象.这种方式当然可以解决所有问题,但实现起来比较繁琐,特别是在配置非常复杂的时候,解析工作是一个不得不考虑的负担.Spring
提供了可扩展Schema
的支持,这个一个不错的折中方案,扩展Spring
自定义标签配置大致需要以下几个步骤(前提是要把Spring
的Core
包加入项目中).
- 创建一个需要扩展的组件.
- 定义一个
XSD
文件描述组件内容.- 创建一个文件,实现
BeanDefinitionParser
接口,用来解析XSD
文件中的定义和组件定义.- 创建一个
Handler
文件,扩展自NamespaceHandlerSupport
,目的是将组件注册到Spring
容器.- 编写
Spring.handlers
和Spring.schemas
文件.现在我们就按照上面的步骤和大家一起体验自定义标签的过程.
(1)首先我们创建一个普通的
POJO
,这个POJO
没有任何特别之处,只是用来接收配置文件.
public class User {
private String userName;
private String email;
// 省略get/set方法
}
(2)定义一个
XSD
文件描述组件内容.
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.test.com/schema/user"
xmlns:tns="http://www.test.com/schema/user"
elementFormDefault="qualified">
<element name="user">
<complexType>
<attribute name="id" type="string" />
<attribute name="userName" type="string" />
<attribute name="email" type="string" />
</complexType>
</element>
</schema>
在上面的
XSD
文件中描述了一个新的targetNamespace
,并在这个空间中定义了一个name
为user
的element
,user
有3个属性id
,userName
和string
.这3个类主要用于验证Spring
配置文件中自定义格式.XSD
文件是XML
,DTD
的替代者,使用XML Schema
语言进行编写,这里对XSD Schema
不做太多解释,大家有兴趣可以自己研究一下.
(3)创建一个文件,实现
BeanDefinitionParser
接口,用来解析XSD
文件中的定义和组件定义.
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
// Element对应的类
protected Class getBeanClass(Element element) {
return User.class;
}
// 从element中解析并提取对应的元素
protected void doParse(Element element, BeanDefinitionBuilder bean) {
String userName = element.getAttribute("userName");
String email = element.getAttribute("email");
// 将提取的数据放入到BeanDefinitionBuilder中,待到完成所有bean的解析后统一注册到beanFatory中
if (StringUtils.hasText(userName)) {
bean.addPropertyValue("userName", userName);
}
if (StringUtils.hasText(email)) {
bean.addPropertyValue("email", email);
}
}
}
(4) 创建一个
Handler
文件,扩展自NamespaceHandlerSupport
,目的是将组件注册到Spring
容器.
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}
以上代码很简单,无非是当遇到自定义标签
<user:aaa>
这样类似于以user
开头的元素,就会把这个元素扔给对应的UserBeanDefinitionParser
去解析.
(5) 编写
Spring.handlers
和Spring.schemas
文件,默认位置是在工程的/ META-INF/
文件夹下,当然,你可以通过Spring
的扩展或者修改源码的方式改变路径.
- Spring.handlers :
http\://www.test.com/schema/user=test.MyNamespaceHandler
- Spring.schema :
http\://www.test.com/schema/user.xsd=META-INF/Spring-test.xsd
到这里,自定义的配置就结束了,而
Spring
加载自定义的大致流程是遇到自定义标签然后就去Spring.handlers
和Spring.schemas
中去找对应的handler
和XSD
,默认位置是/META-INF/
下,进而有找到对应的handler
以及解析元素的Parser
,从而完成整个自定义元素的解析,也就是说自定义与Spring
中默认的标准配置不同在于Spring
将自定义标签解析的工作委托给了用户去实现.
(6) 创建测试配置文件,在配置文件中引入对应的命名空间以及
XSD
后,便可以直接使用自定义标签了.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myname="http://www.test.com/schema/user"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.test.com/schema/user http://www.test.com/scheame/user.xsd">
<myname:user id="testbean" userName="aaa" email="bbb" />
</beans>
(7) 测试
public class test {
public static void main(String[] args) {
ApplicationContext bf = new ClassPathXmlApplicationContext("test/test.xml");
User user = (User) bf.getBean("testbean");
System.out.println(user.getUserName() + ", " + user.getEmail());
}
}
不出意外的话,应该可以看到控制台打印了如下结果:
aaa
,bbb
在上面的例子中,我们实现了通过自定义标签通过属性的方式将user
类型的Bean
赋值,在Spring
中自定义标签非常常用,例如我们熟知的事物标签:tx(<tx:annotation-driven>)
.
4.2 自定义标签解析
了解了自定义标签的使用后,我们带着强烈的好奇心来探究一下自定义的解析过程.
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}
// containingBd为父类bean,对顶层元素的解析应该设置为null
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取对应的命名空间
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 根据命名空间找到对应的NamespaceHandler
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 调用自定义的NamespaceHandler进行解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
相信了解了自定义标签的使用方法后,或多或少会对自定义标签的实现过程有一个自己的想法.其实实现思路非常的简单,无非是根据对应的
bean
获取对应的命名空间,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析.可是有些事情说起来简单做起来难,我们接下来先看看如何获取命名空间.
4.2.1 获取标签的命名空间
标签的解析是从命名空间的提起开始的,无论是区分
Spring
中默认标签和自定义标签还是区分自定义标签中不同标签的处理器都是以标签所提供的命名空间为基础的,而至于如何提取对应元素的命名空间其实不需要我们亲自去实现,在org.w3c.dom.Node
中已经提供了方法供我们直接调用:
public String getNamespaceURI(Node node){
return node.getNamespaceURI();
}
4.2.2 提取自定义标签处理器
有了命名空间,就可以进行
NamespaceHandler
的提取了,继续之前的parseCustomElement
方法的跟踪,分析NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
,在readerContext
初始化的时候其属性namespaceHandlerResolver
已经被初始化为了DefaultNamespaceHandlerResolver
的实例,所以这里调用的resolve
方法其实调用的是DefaultNamespaceHandlerResolver
类中的方法.我们进入DefaultNamespaceHandlerResolver
的resolve
方法进行查看.
@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有已经配置的handler映射
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据命名空间找到对应的信息
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
// 已经做过解析的情况直接从缓存读取
return (NamespaceHandler) handlerOrClassName;
}
else {
// 没有做过解析则返回类路径
String className = (String) handlerOrClassName;
try {
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);
// 调用自定义的NamespaceHandler的初始化方法
namespaceHandler.init();
// 记录在缓存中
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);
}
}
}
上面的函数清晰地阐述了解析自定义
NamespaceHandler
的过程,通过之前的示例程序我们了解到如果要使用自定义标签,那么其中一项必不可少的操作就是在Spring.handlers
文件中配置命名空间与命名空间处理的映射关系.只有这样,Spring
才能根据映射关系找到匹配的处理器,而寻找匹配的处理器就是在上面方法中实现,当获取到自定义的NamespaceHandler
之后就可以进行处理器初始化并解析了.我们这里在回忆一下对命名空间处理器的定义内容:
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}
当得到自定义命名空间处理后会马上执行
namespaceHandler.init()
来进行自定义BeanDefinitionParser
的注册.在这里,可以注册多个标签解释器,当前实例中只有支持<myname:user>
的写法,也可以在这里注册多个解析器,如<myname:A> <myname:B>
等,是的myname
的命名空间中可以支持多种标签解析.
注册后,命名空间处理器就可以根据标签的不同来调用不同的解析器进行解析.那么,根据上面的函数与之前介绍过的例子,我们基本上可以推断getHandlerMappings
的主要功能就是读取Spring.handlers
配置文件并将配置文件缓存在map
中.
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
// 如果没有被缓存则开始进行缓存
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
// 将Properties格式文件合并到Map格式的handlerMappings中
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}
同我们想象的一样,接住了工具类
PropertiesLoaderUtils
对属性handlerMappingsLocation
进行了配置文件的读取,handlerMappingsLocation
被默认初始化为META-INF/Spring.handlers
.
4.2.3 标签解析
得到了解析器以及要分析的元素后,
Spring
就可以将解析工作委托给自定义解析器去解析了.在Spring
中的代码为:
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
以之前提到的示例进行分析,此时的
handler
已经被实例化成我们自定义的MyNamespaceHandler
了,而MyNamespaceHandler
也已经完成了初始化的工作,但是在我们实现的自定义命名空间处理器中并没有实现parse
方法,所以推断,这个方法是父类中的实现,查看父类NamespaceHandlerSupport
中的parse
方法.
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 寻找解析器并进行解析操作
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return (parser != null ? parser.parse(element, parserContext) : null);
}
解析过程中首先是寻找元素对应的解析器,进而调用解析器中的
parse
方法,那么结合示例来讲,其实就是首先获取在MyNameSpaceHandler
类中的init
方法中注册的对应的UserBeanDefinitionParser
实例,并调用其parse
方法进行进一步解析.
@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 获取元素名称,也就是<myname:user中的user,若在示例中,此时localName为user
String localName = parserContext.getDelegate().getLocalName(element);
// 根据user找到对应的解析器,也就是在registerBeanDefinitionParser("user", new UserBeanDefinitionParser());注册的解析器
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
而对于
parse
方法的处理
@Override
@Nullable
public final BeanDefinition parse(Element element, ParserContext parserContext) {
AbstractBeanDefinition definition = parseInternal(element, parserContext);
if (definition != null && !parserContext.isNested()) {
try {
String id = 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);
}
String[] aliases = null;
if (shouldParseNameAsAliases()) {
String name = element.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(name)) {
aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}
// 将AbstractBeanDefinition转化为BeanDefinitionHolder并注册
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
registerBeanDefinition(holder, parserContext.getRegistry());
if (shouldFireEvents()) {
// 需要通知监听器则进行处理
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
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;
}
虽说是对自定义配置文件的解析,但是,我们可以看到,在这个方法中大部分的代码是用来处理将解析后的
AbstractBeanDefinition
转化为BeanDefinitionHolder
并注册的功能,而真正去做解析的事情委托给了函数parseInternal
,正是这句代码调用了我们自定义的解析函数.
在parseInternal
中并不是直接调用自定义的doParse
函数,而是进行了一系列的数据准备,包括对beanClass
scope
lazyInit
等属性的准备.
@Override
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
// 获取自定义标签中的class,此时会调用自定义解析器如UserBeanDefinitionParser中的getBeanClass方法
Class<?> beanClass = getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
else {
// 若子类没有重写getBeanClass方法则尝试检查子类是否重写getBeanClassName方法
String beanClassName = getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}
builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
if (containingBd != null) {
// 若存在父类则使用父类的scope属性
// Inner bean definition must receive same scope as containing bean.
builder.setScope(containingBd.getScope());
}
if (parserContext.isDefaultLazyInit()) {
// 调用子类重写的doParse方法进行解析
// Default-lazy-init applies to custom bean definitions as well.
builder.setLazyInit(true);
}
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
}
回顾一下全部的自定义标签处理过程,虽然在实例中我们定义
UserBeanDefinitionParser
,但是在其中我们只是做了与自己业务逻辑相关的部分.不过我们没做但是并不代表没有,在这个处理过程中同样也是按照Spring
中默认标签的处理方式进行,包括创建BeanDefinition
以及进行相应默认属性的设置,对于这些工作Spring
都默默地帮我们实现了,只是暴露出一些接口来供用户实现个性化的业务.通过对本章的了解,相信读者对Spring
中自定义标签的使用以及在解析自定义标签过程中Spring
为我们做了哪些工作会有一个全面的了解,到此为止我们已经完成了Spring
中全部的解析工作,也就是说到现在为止我们已经理解了Spring
将bean
从配置文件到加载到内存中的全过程,而接下来的任务便是如何使用这些bean
.