第3章自定义标签解析
在第1章中对Spring的简单使用进行了说明,简单介绍了bean标签的使用。bean标签属于Spring的原生标签,在Spring中除了原生标签以外还能够支持自定义标签,本章将介绍SpringXML配置文件中的自定义标签如何进行自定义、如何使用自定义标签,并对SpringXML的自定义标签相关的内容进行源码分析。
3.1创建自定义标签环境搭建
在开始自定义标签分析之前,需要先编写自定义标签解析相关的测试用例,编写自定义标签需要执行下面四个步骤。
(1) 编写XSD文件或者DTD文件。
(2) 编写NamespaceHandler实现类。
(3) 编写BeanDefinitionParser实现类。
(4) 编写注册方式,向Spring中注册。
接下来对上述四个步骤做详细说明。
3.1.1编写 XSD 文件
首先编写一个Java对象用来存储自定义标签解析后的数据,编写UserXsdJava对象,代码信息如下。
//省略getter&setter
public class UserXsd {
private String name;
private String idCard;
}
完成XSD文件解析结果的存储对象后进一步编写XSD文件,该XSD文件名为user.xsd,文件内容如下。
3.1.2编写 NamespaceHandler 实现类
完成XSD文件编写后进一步编写NamespaceHandler接口的实现类,Spring提供了NamespaceHandlerSupport对象让开发者更加简单地使用,开发者只需要重写init方法即可向Spring注册标签和标签的解析对象,编写UserXsdNamespaceHandler类,详细代码如下。
public class UserXsdNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("user_xsd",new UserXsdParser());
}
}
3.1.3编写 BeanDefinitionParser 实现类
在编写NamespaceHandler实现类的时候引入了一个新的Java对象UserXsdParser,该对象是BeanDefinitionParser接口的实现类,在Spring中可以通过继承AbstractSingleBeanDefinitionParser类重写getBeanClass和doParse方法即可完成BeanDefinitionParser的实现,下面是UserXsdParser的代码内容。
public class UserXsdParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class> getBeanClass(Element element) {
return UserXsd.class;
}
@Override
protected void doParse(Element element,BeanDefinitionBuilder builder) {
String name = element.getAttribute("name");
String idCard = element.getAttribute("idCard");
builder.addPropertyValue("name",name);
builder.addPropertyValue("idCard",idCard);
}
}
在这段代码中通过提取Element对象的name和idCard属性将其设置到BeanDefinitionBuilder对象的属性表中。
3.1.4编写注册方式
下面编写注册方式。注册自定义标签解析能力需要编写两个文件,一个是spring.handlers文件,另一个是spring.schemas文件。在这个测试用例中需要向spring.handlers文件中填写下面这段内容。
http\://www.huifer.com/schema/user=com.source.hot.ioc.book.namespace.handler.
UserXsdNamespaceHandler
对于spring.handlers文件可以分成两部分来进行理解,第一部分是等号前面的内容,等号前的内容是指命名空间和XSD文件中schema中的targetNamespace属性之间的关系; 第二部分是等号后面的内容,它是指接口NamespaceHandler实现类的完整类路径。
完成spring.handlers编写后进一步编写spring.schemas文件,向spring.schemas文件中添加下面这段代码。
http\://www.huifer.com/schema/user.xsd=META-INF/user.xsd
对于spring.schemas文件可以分成两部分来进行理解,第一部分是等号前面的内容,它是指schemaLocation的一个链接地址; 第二部分是等号后面的内容,它是指schemaLocation对应的XSD文件描述路径。
3.1.5测试用例的编写
完成了各项基本准备工作后进一步编写一个自定义标签处理的Java程序,首先需要编写SpringXML配置文件,文件名为customxml.xml,向customxml.xml文件中填写下面的内容。
完成SpringXML配置文件的编写后再编写一个测试类,测试类名称为CustomXmlTest,CustomXmlTest中代码如下。
class CustomXmlTest {
@Test
void testXmlCustom() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("META-INF/custom-xml.xml");
UserXsd testUserBean = context.getBean("testUserBean",UserXsd.class);
assert testUserBean.getName().equals("huifer");
assert testUserBean.getIdCard().equals("123");
context.close();
}
}
完成测试类及测试方法的编写后自定义标签测试环境即搭建完成。
3.2自定义标签解析
下面将进入自定义标签解析相关源代码分析,首先需要找到自定义标签解析的源码入口,该入口的方法签名为org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element,org.springframework.beans.factory.config.BeanDefinition),详细代码如下。
@Nullable
public BeanDefinition parseCustomElement(Element ele,@Nullable BeanDefinition containingBd) {
//获取命名空间的URL
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
//命名空间处理器
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));
}
3.2.1NamesapceHandler和 BeanDefinitionParser 之间的关系
图3.1NamespaceHandler关系图
parseCustomElement方法中体现了namespaceUri的一些关系,在测试用例中namespaceUri和spring.handlers中的文件存在关系,命名空间对应命名空间处理器,通过这个关系可以确认NamespaceHandler是UserXsdNamespaceHandler对象,在UserXsdNamespaceHandler类中有init方法来注册标签和标签的解析能力提供类的关系。具体关系如图3.1所示。
3.2.2获取命名空间地址
接下来将介绍命名空间地址的获取,命名空间地址及namespaceUri属性,获取该属性的方法是由org.w3c.dom提供,具体细节不做展开,获取命名空间地址后的数据是http://www.huifer.com/schema/user。
3.2.3NamespaceHandler对象获取
通过前文获得了namespaceUri数据信息后会通过该信息寻找到对应的NamespaceHandler对象,具体处理方法如下。
NamespaceHandler handler
= this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
在这段方法中负责命名空间解析的对象是NamespaceHandlerResolver接口,NamespaceHandlerResolver接口定义如下。
@FunctionalInterface
public interface NamespaceHandlerResolver {
/**
* 解析命名空间的 URL 获得命名空间处理器
*/
@Nullable
NamespaceHandler resolve(String namespaceUri);
}
在Spring中它只有一个实现类DefaultNamespaceHandlerResolver,在DefaultNamespaceHandlerResolver中有一个成员变量DEFAULT_HANDLER_MAPPINGS_LOCATION,成员变量详细信息如下。
public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION =
"META-INF/spring.handlers";
在DEFAULT_HANDLER_MAPPINGS_LOCATION成员变量中定义了默认的命名空间处理器存储路径,spring.handlers中存储的内容是(key,value)结构,key是命名空间,value是命名空间处理器的类全路径,接下来查看resolve方法,详细代码如下。
//删除异常处理和日志
public NamespaceHandler resolve(String namespaceUri) {
//获取 Namespace Handler 映射表
Map < String,Object > handlerMappings = getHandlerMappings();
//从映射表中获取URI对应的 Handler
//字符串(名称)
//实例
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if(handlerOrClassName == null) {
return null;
} else if(handlerOrClassName instanceof NamespaceHandler) {
return(NamespaceHandler) handlerOrClassName;
}
//其他情况都做字符串处理
else {
String className = (String) handlerOrClassName;
Class > handlerClass = ClassUtils.forName(className,this.classLoader);
//通过反射构造 namespaceHandler 实例
NamespaceHandler namespaceHandler =
(NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
//初始化
namespaceHandler.init();
//重写缓存
handlerMappings.put(namespaceUri,namespaceHandler);
return namespaceHandler;
}
}
在resolve方法中整体处理流程如下。
(1) 获取命名空间映射关系,即命名空间地址(namespaceUri)对应命名空间解析对象类全路径,这个关系是一个 map集合。
(2) 从map集合中获取命名空间地址(namespaceUri)对应的数据。
(3) 根据提取数据的不同情况进行处理。
① 通过命名空间地址获取的对象是NamespaceHandler类型,直接返回使用。
② 通过命名空间地址获取的对象不是NamespaceHandler类型。当类型不是NamespaceHandler对象时,它的类型只可能是字符串类型,字符串是没有办法直接调用NamespaceHandler所提供的方法的,因此需要将字符串转换成NamespaceHandler对象。
3.2.4getHandlerMappings获取命名空间的映射关系
在前文讲到字符串转换为NamespaceHandler的内容在本节会对其做补充,负责这部分处理的方法是getHandlerMappings,详细代码如下。
//删除异常处理和日志
private Map < String,Object > getHandlerMappings() {
//设置容器
Map < String,Object > handlerMappings = this.handlerMappings;
if(handlerMappings == null) {
synchronized(this) {
handlerMappings = this.handlerMappings;
if(handlerMappings == null) {
//读取资源文件地址
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation,this.classLoader);
handlerMappings = new ConcurrentHashMap < > (mappings.size());
//数据合并,将 mappings 数据复制给 handlerMappings
CollectionUtils.mergePropertiesIntoMap(mappings,handlerMappings);
this.handlerMappings = handlerMappings;
}
}
}
return handlerMappings;
}
在这段方法中首先需要关注的是下面这段代码。
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation,this.classLoader);
这段代码是用来提取METAINF/spring.handlers文件中的数据内容,Spring将METAINF/spring.handlers文件当作拓展名为properties类型的文件进行处理,处理之后得到的是Map对象,在完成METAINF/spring.handlers文件的读取后进行了合并操作,分别将历史的handlerMappings和本次读取得到的handlerMappings进行合并。图3.2为本次读取得到的handlerMappings数据。
图3.2handlerMappings数据信息
通过图3.2可以看到一条这样的信息: http://www.huifer.com/schema/user > com.source.hot.ioc.book.namespace.handler.UserXsdNamespaceHandler,这段信息的来源是前文在测试用例中文件spring.handlers的内容,除此之外,其他的数据是Spring项目中存放的,文件依旧还是在METAINF/spring.handlers中。
springbeans工程下的spring.handlers数据信息如下。
http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.
SimpleConstructorNamespaceHandler
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.
SimplePropertyNamespaceHandler
http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.
UtilNamespaceHandler
springaop工程下的spring.handlers数据信息如下。
http\://www.springframework.org/schema/aop=org.springframework.aop.config.
AopNamespaceHandler
springcontext工程下的spring.handlers数据信息如下。
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
上述这些文件是Spring容器中提供的NamespaceHandler数据,除此之外,还有一些其他的spring.handlers文件本文不做赘述。
3.2.5NamespaceHandler的获取
在前面的操作中已经准备好handlerMappings数据对象,下面需要将数据对象进行初始化(实例化,实例化的前提是类型不是NamespaceHandler)。有关NamespaceHandler的获取代码如下。
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if(handlerOrClassName == null) {
return null;
} else if(handlerOrClassName instanceof NamespaceHandler) {
return(NamespaceHandler) handlerOrClassName;
}
//其他情况都做字符串处理
else {
String className = (String) handlerOrClassName;
Class > handlerClass = ClassUtils.forName(className,this.classLoader);
//通过反射构造 namespaceHandler 实例
NamespaceHandler namespaceHandler =
(NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
//初始化
namespaceHandler.init();
//重写缓存
handlerMappings.put(namespaceUri,namespaceHandler);
return namespaceHandler;
}
上述代码中有以下三种情况需要处理。
(1) 容器中没有当前namespaceUri的值。
(2) 容器中有当前namespaceUri的值,并且类型是NamespaceHandler。
(3) 容器中有当前namespaceUri的值,但类型不是NamespaceHandler。
对于上述三种情况需要重点分析的是第三种。在第三种情况中,value的数据类型是String,Spring需要将String类型转换为最终的Java对象,通过类全路径转换成Java对象需要执行以下三个步骤。
(1) 通过Class.forName 得到Class对象。
(2) 通过Class对象提取构造函数。
(3) 通过构造函数创建实例。
上述三个操作步骤可以分别对应下面这些代码,第一步对应“Class> handlerClass = ClassUtils.forName(className,this.classLoader);”,第二步和第三步对应Spring项目中的一个工具类BeanUtils。通过BeanUtils.instantiateClass方法即可得到Java对象。
3.2.6NamespaceHandler的 init 方法
接下来将介绍NamespaceHandler的init方法,在测试用例中,UserXsdNamespaceHandler对象继承NamespaceHandlerSupport类,接下来需要分析的重点内容都在NamespaceHandlerSupport类中。首先需要指出一点,在Spring中的spring.handlers文件中的实现类都有继承NamespaceHandlerSupport类,可见它的重要性。在测试用例中使用了registerBeanDefinitionParser方法进行了标签和解析类的注册,下面对registerBeanDefinitionParser方法进行分析,先看registerBeanDefinitionParser代码。
protected final void registerBeanDefinitionParser(String elementName,BeanDefinitionParser parser) {
this.parsers.put(elementName,parser);
}
在这段代码中需要了解以下两个参数的含义。
(1) elementName: XML标签的名称。
(2) parser: 提供标签解析能力的实际对象,是BeanDefinitionParser接口的实现类。
下面介绍parsers的数据结构,在Spring中对parsers的定义如下。
private final Map parsers = new HashMap<>();
key表示XML标签的名称,value表示BeanDefinitionParser接口的实现类。parsers的数据存储情况如图3.3所示。
图3.3parsers数据信息
3.2.7NamespaceHandler缓存的刷新
在NamespaceHandler的生命周期方法init执行完成后会刷新namespaceUri对应的value,此时会产生handlerMappings中value的多种情况。
(1) value不存在。
(2) value存在,且是NamespaceHandler实例。
(3) value存在,且是字符串。
刷新handlerMappings变量的操作代码如下。
handlerMappings.put(namespaceUri,namespaceHandler)
这段代码的执行就是将namespaceUri和namespaceHandler的关系重新绑定,此时放入的value就会变成NamespaceHandler实例,后续在需要使用时就会进入下面这段代码。
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
在这里 Spring 通过了一个 Map 对象的刷新操作来提高了性能,避免了每次从字符串出发进行反射获取实例,获取实例之后再做其他操作。图3.4为经过刷新操作后的handlerMappings数据情况。
图3.4刷新后的handlerMappings
3.2.8解析标签BeanDefinitionParser对象准备
接下来将进入自定义标签解析的重要环节——BeanDefinitionParser实现类的准备阶段。首先阅读parse方法:
NamespaceHandler handler
= this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
return handler.parse(ele,new ParserContext(this.readerContext,this,containingBd));
在这段代码中得到了NamespaceHandler对象,这个对象的实际类型是UserXsdNamespaceHandler,确认实际类型后对于parse方法入口的查询有了方向,parse方法位于UserXsdNamespaceHandler的父类NamespaceHandlerSupport中,具体代码如下。
public BeanDefinition parse(Element element,ParserContext parserContext) {
//搜索 element 对应的 BeanDefinitionParser
BeanDefinitionParser parser = findParserForElement(element,parserContext);
//解析
return (parser != null ? parser.parse(element,parserContext): null);
}
从这段操作代码中可以发现一个重点类BeanDefinitionParser,在测试用例中编写的内容中有一个与之存在关系,这个类是UserXsdParser。在测试用例中和UserXsdParser对象产生关系的内容是一个注册方法registerBeanDefinitionParser("user_xsd",new UserXsdParser()),这个方法表示user_xsd标签需要通过UserXsdParser对象进行解析。在parse方法中出现的findParserForElement方法目的就是通过标签名称找到对应的标签处理对象。findParserForElement方法代码如下。
private BeanDefinitionParser findParserForElement(Element element,ParserContext parserContext) {
//获取 element 的名称
String localName = parserContext.getDelegate().getLocalName(element);
//从容器中获取
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]",element);
}
return parser;
}
在这段方法中可以看到以下两个处理步骤。
(1) 提取Element的名称数据。
(2) 从parsers中获取解析BeanDefinitionParser对象。
此时得到的BeanDefinitionParser是UserXsdParser对象。
3.2.9解析标签parse方法调用
在Spring中parse方法提供者是AbstractBeanDefinitionParser对象,详细代码如下。
//删除异常处理和日志
public final BeanDefinition parse(Element element,ParserContext parserContext) {
//解析 Element 得到 BeanDefinition 对象
AbstractBeanDefinition definition = parseInternal(element,parserContext);
if(definition != null && !parserContext.isNested()) {
//解析标签的 id
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()) {
//标签的 name 属性
String name = element.getAttribute(NAME_ATTRIBUTE);
if(StringUtils.hasLength(name)) {
//别名处理,根据逗号进行字符串切割
aliases =
StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}
//创建 BeanDefinitionHolder
BeanDefinitionHolder holder = new
BeanDefinitionHolder(definition,id,aliases);
//注册 BeanDefinition
registerBeanDefinition(holder,parserContext.getRegistry());
//是否需要触发事件
if(shouldFireEvents()) {
//组件注册事件
BeanComponentDefinition componentDefinition =
new BeanComponentDefinition(holder);
postProcessComponentDefinition(componentDefinition);
parserContext.registerComponent(componentDefinition);
}
}
return definition;
}
图3.5UserXsdParser类图
在这段代码中比较难以查看到UserXsdParser对象的踪迹,UserXsdParser类图信息如图3.5所示。
接下来看第一段代码。
AbstractBeanDefinition definition = parseInternal
(element,parserContext);
parseInternal方法在AbstractBeanDefinitionParser是一个抽象方法,真正的实现在 AbstractSingleBeanDefinitionParser中,下面是AbstractSingleBeanDefinitionParser#parseInternal方法代码。
@Override
protected final AbstractBeanDefinition parseInternal(Element element,ParserContext parserContext) {
//BeanDefinition 构造器
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
//获取 parent 属性
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
//获取 class 属性
Class> beanClass = getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
else {
String beanClassName = getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}
//设置源
builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
//获取已存在的 BeanDefinition,该对象仅用来设置 scope 属性
BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
if (containingBd != null) {
builder.setScope(containingBd.getScope());
}
if (parserContext.isDefaultLazyInit()) {
builder.setLazyInit(true);
}
//真正的调用
doParse(element,parserContext,builder);
//BeanDefinition 构造器中获取 BeanDefinition
return builder.getBeanDefinition();
}
在UserXsdParser类中重写了getBeanClass和doParse方法,测试用例中所编写的doParse方法的调用需要通过一层外部调用才可以抵达测试用例中UserXsdParser类的doParse方法,具体调用过程的代码如下。
//AbstractSingleBeanDefinitionParser#parseInternal 中调用
protected void doParse(Element element,ParserContext parserContext,
BeanDefinitionBuilder builder) {
doParse(element,builder);
}
//需要重写的方法
protected void doParse(Element element,BeanDefinitionBuilder builder) {
}
AbstractSingleBeanDefinitionParser#parseInternal方法处理的细节如下。
(1) 准备基本数据,基本数据包含parentName、beanClass、source和scope。
(2) 执行自定义的doParse方法。
(3) 通过BeanDefinitionBuilder来获取 BeanDefinition。
在parse方法中首先需要进行id的处理,代码如下。
String id = resolveId(element,definition,parserContext);
在这段代码中对于id属性的获取其本质是提取XML标签中的id属性。完成id数据获取后需要执行的事项是针对别名的处理,相关代码如下。
String[] aliases = null;
if (shouldParseNameAsAliases()) {
//标签的 name 属性
String name = element.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(name)) {
//别名处理,根据逗号进行字符串切割
aliases =
StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}
在别名处理阶段会判断是否需要处理别名,默认是都需要处理的。别名处理方式是将alias标签中的name属性根据分隔符(逗号)切分,关于切分其本质为name.split(“,”),具体的处理方法是StringUtils.commaDelimitedListToStringArray。在数据信息准备完成之后需要进行BeanDefinition对象的注册和事件发布,相关代码如下。
BeanDefinitionHolder holder =
new BeanDefinitionHolder(definition,id,aliases);
//注册 BeanDefinition
registerBeanDefinition(holder,parserContext.getRegistry());
//是否需要出发事件
if (shouldFireEvents()) {
//组件注册事件
BeanComponentDefinition componentDefinition =
new BeanComponentDefinition(holder);
postProcessComponentDefinition(componentDefinition);
parserContext.registerComponent(componentDefinition);
}
在这部分代码处理中,关于事件发布相关内容是一个预留方法,暂时是一个空处理,对于BeanDefinition对象的注册会在后续章节中进行详细分析,在这仅需要了解BeanDefinition对象的存储容器:
private final Map beanDefinitionMap =
new ConcurrentHashMap<>(256);
当BeanDefinition 对象被放置到容器后这段方法的处理就完成了。
小结
通过本章的阐述,相信读者对于自定义标签的处理流程有了更加详细的认知。在本章了解了NamespaceHandler、NamespaceHandlerSupport、AbstractSingleBeanDefinitionParser、AbstractBeanDefinitionParser和BeanDefinitionParser之间的关系,这些内容在 Spring 中是一个十分重要的技术点,Spring 后续的一些内容都强依赖于这一门技术(自定义标签解析),常见的有 SpringMVC、Spring事务、SpringAOP 等,它们都是基于此作为一个拓展实现了更强大的功能。