在上篇文章中,提到了在Spring中存在默认标签与自定义标签两种,并且详细分析了默认标签的解析,本文就来分析自定义标签的解析,像Spring中的AOP就是通过自定义标签来进行配置的,这里也是为后面学习AOP原理打下基础。

  这里先回顾一下,当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);
}
}

  从上面的函数中也可以看出,当Spring拿到一个元素时首先要做的是根据命名空间进行解析,如果是默认的命名空间,则使用parseDefaultElement()方法进行元素解析,否则使用parseCustomElement()方法进行解析。在本文中,所有的功能解析都是围绕其中的那句代码delegate.parseCustomElement(root)开展的。

  在分析自定义标签的解析过程之前,我们先了解一下自定义标签的使用过程,这里参考spring文档中的例子。

1. 自定义标签使用

  扩展Spring自定义标签配置大致需要以下几个步骤:

  • 定义一个XML文件来描述你的自定义标签元素
  • 创建一个Handler,扩展自NamespaceHandlerSupport
  • 创建若干个BeanDefinitionParser的实现,用来解析XML文件中的定义
  • 将上述文件注册到Spring中,这里其实是做一下配置

  接下来我们将创建一个自定义XML元素,便于通过一个更容易的方式配置SimpleDateFormat类型的bean。配置好之后我们可以通过下面的方式来定义一个SimpleDateFormat类型的bean:

<myns:dateformat id = "dateFormat" pattern = "yyyy-MM-dd HH:mm" lenient = "true"/>

1.1 编写schema

  给Spring IoC容器创建XML扩展标签的第一步是创建一个新的XML模式来描述对应的标签(下面是我们将要用来配置SimpleDateFormat对象的schema):

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.com/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.com/schema/myns"
elementFormDefault="qualified"
attributeFormDefault="unqualified"> <xsd:import namespace="http://www.springframework.org/schema/beans"/> <xsd:element name="dateformat">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="lenient" type="xsd:boolean"/>
<xsd:attribute name="pattern" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>

  定义了上面的schema之后,我们就可以直接使用元素<myns:dateformat/>来配置SimpleDateFormat类型的对象了:

<myns:dateformat id="dateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>

  如果没有做上面的工作,我们可能就需要通过下面的方式来配置SimpleDateFormat类型的对象了:

<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-HH-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>

1.2 编写一个BeanDefinitionParser

  这个是继承自AbstractSingleBeanDefinitionParser,主要是用来将自定义标签解析成BeanDefinition。

public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser{
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class;
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
bean.addConstructorArg(pattern);
// this however is an optional property
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}

1.3 编写一个NamespaceHandler

  这个是继承自NamespaceHandlerSupport,主要是将上面的BeanDefinitionParser注册到Spring容器:

public class MyNamespaceHandler extends NamespaceHandlerSupport{

    public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
} }

1.4 编写Spring.handlers和Spring.schemas文件

  这两个文件默认位置是在工程资源目录的/META-INF/文件夹下,内容如下(注意要改成自己的包名):

META-INF/spring.handlers
http\://www.mycompany.com/schema/myns=spring.customElement.MyNamespaceHandler META-INF/spring.schemas
http\://www.mycompany.com/schema/myns/myns.xsd=spring/customElement/myns.xsd

1.5 自定义标签使用示例

  使用自定义的扩展标签和使用Spring提供的默认标签是类似的,可以按照如下配置一个SimpleDateFormat类型的bean:

<?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:myns="http://www.mycompany.com/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.mycompany.com/schema/myns http://www.mycompany.com/schema/myns/myns.xsd"> <!-- as a top-level bean -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
</beans>

  配置好之后可以测试一下:

public static void main(String[] args) {
XmlBeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("customElement.xml"));
SimpleDateFormat myTestBean = (SimpleDateFormat)xmlBeanFactory.getBean("defaultDateFormat");
System.out.println( "now time --- "+ myTestBean.format(new Date()));
} // 输出结果:
now time --- 2020-03-07 20:37

2. 自定义标签解析

  了解了自定义标签的使用之后,我们来探究一下自定义标签的解析过程。接着文章开头提到的,我们要从BeanDefinitionParserDelegate的parseCustomElement()方法开始:

public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
} public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
// 获取对应的名称空间
String namespaceUri = getNamespaceURI(ele);
// 根据命名空间找到对应的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));
}

  这里可以看出对自定义标签进行解析的思路是根据Element获取对应的名称空间,然后根据名称空间获取对应的处理器,最后根据用户自定义的处理器进行解析,可是看起来简单,实现起来就不是这么简单了,先来看一下名称空间的获取吧。

2.1 获取标签的名称空间

  自定义标签的解析是从名称空间的提取开始的,无论是区分默认标签和自定义标签,还是区分自定义标签对应的不同处理器,都是以标签所提供的名称空间为基础的。至于如何提取对应元素的名称空间,已经有现成的实现可供使用,spring中是直接调用org.w3c.dom.Node提供的相应方法来完成名称空间的提取:

public String getNamespaceURI(Node node) {
return node.getNamespaceURI();
}

2.2 获取自定义标签处理器

  有了名称空间,就可以此来提取对应的NamespaceHandler了,这项工作是由下面这句代码来完成的:

NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

  这里readerContext的getNamespaceHandlerResolver()方法返回的其实是DefaultNamespaceHandlerResolver,所以我们直接进入其resolve()方法中往下看:

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.init();
// 记录在缓存
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);
}
}
}

  上面函数中的流程还是比较清晰的,在前面的自定义标签使用示例中有说到,如果要使用自定义标签,需要在Spring.handlers文件中配置名称空间与名称空间处理器的映射关系。只有这样,Spring才能根据映射关系找到匹配的处理器。

  而寻找匹配的处理器就是在上面函数中实现的,当获取到自定义的NamespaceHandler之后就可以进行处理器初始化并解析了。这里我们再回忆一下前面自定义标签示例中,对于名称空间处理器的内容(我们在其init()方法中注册了一个解析器)。

  在上面的代码中,获取到自定义名称空间处理器后会马上执行其init()方法来进行自定义BeanDefinitionParser的注册。当然在init()中可以注册多个标签解析器,如<myns:A、<myns:B等,使得myns的名称空间中可以支持多种标签解析。

  注册好之后,名称空间处理器就可以根据标签的不同来调用不同的解析器进行解析。根据上面的函数和之前的例子,我们基本可以判断getHandlerMappings()的主要功能就是读取Spring.handlers配置文件并将配置文件缓存在map中:

private Map<String, Object> getHandlerMappings() {
// 如果没有被缓存则开始进行缓存
if (this.handlerMappings == null) {
synchronized (this) {
if (this.handlerMappings == null) {
try {
// this.handlerMappingsLocation在构造函数中已经被初始化为:META-INF/Spring.handlers
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded NamespaceHandler mappings: " + mappings);
}
Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(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 this.handlerMappings;
}

  这里是借助工具类PropertiesLoaderUtils对Spring.handlers配置文件进行了读取,然后将读取的内容放到缓存中并返回。

2.3 标签解析

  获取到解析器以及要解析的元素后,Spring将解析工作委托给自定义解析器来解析,即下面代码所完成的:

return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

  此时我们拿到的handler其实是我们自定义的MyNamespaceHandler了,但是我们前面并没有实现parse()方法,所以这里这个应该是调用的父类中的parse()方法,看一下NamespaceHandlerSupport中的parse()方法:

public BeanDefinition parse(Element element, ParserContext parserContext) {
// 寻找解析器并进行解析操作
return findParserForElement(element, parserContext).parse(element, parserContext);
} private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 获取元素名称,也就是<myns:dateformat中的dateformat,在上面示例中,localName为dateformat
String localName = parserContext.getDelegate().getLocalName(element);
// 根据dateformat找到对应的解析器,也就是在registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
// 注册的解析器
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}

  首先是寻找元素对应的解析器,然后调用其parse()方法。结合我们前面的示例,其实就是首先获取在MyNamespaceHandler类中的init()方法中注册对应的SimpleDateFormatBeanDefinitionParser实例,并调用其parse()方法进行进一步解析,同样这里parse()方法我们前面是没有实现的,我们也试着从其父类找一下:

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 = new String[0];
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) {
parserContext.getReaderContext().error(ex.getMessage(), 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,此时会调用自定义解析器中的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));
if (parserContext.isNested()) {
// 若存在父类则使用父类的scope属性
builder.setScope(parserContext.getContainingBeanDefinition().getScope());
}
if (parserContext.isDefaultLazyInit()) {
// 配置延迟加载
builder.setLazyInit(true);
}
// 调用子类重写的doParse方法进行解析
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
} // 这里就是调用前面示例中我们自己写的doParse()方法
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
doParse(element, builder);
}

  到这里就完成了对自定义标签转换成BeanDefinition的整个过程了,回顾一下整个过程,在我们定义的SimpleDateFormatBeanDefinitionParser中我们只是做了与自己业务逻辑相关的部分,剩下的包括创建BeanDefinition以及进行相应默认属性的设置,Spring都帮我们默认实现了,我们当然也可以自己来完成这一过程,比如AOP就是这样做的,但是本文还是用最简单的方式来做一个说明。

3. 总结

  其实从Spring对自定义标签的解析中也可以体会到Spring的可扩展式设计思路,通过暴露一些接口,我们就能够方便地实现自己的个性化业务,不仅如此,Spring自己便是这项功能的践行者,像AOP、事务都是通过这种方式来定制对应的标签来完成配置需求的。

  到这里我们已经完成了Spring中全部的解析工作的学习,也就是说到这里我们已经学习了Spring将bean从配置文件加载到内存的完整过程,接下来的任务便是如果使用这些bean,这才是IoC容器的重头戏,后面会详细学习的。

Spring源码阅读笔记05:自定义xml标签解析的更多相关文章

  1. Spring源码阅读笔记03:xml配置读取

    前面的文章介绍了IOC的概念,Spring提供的bean容器即是对这一思想的具体实现,在接下来的几篇文章会侧重于探究这一bean容器是如何实现的.在此之前,先用一段话概括一下bean容器的基本工作原理 ...

  2. Spring源码阅读笔记02:IOC基本概念

    上篇文章中我们介绍了准备Spring源码阅读环境的两种姿势,接下来,我们就要开始探寻这个著名框架背后的原理.Spring提供的最基本最底层的功能是bean容器,这其实是对IoC思想的应用,在学习Spr ...

  3. Spring源码阅读 之 配置的读取,解析

    在上文中我们已经知道了Spring如何从我们给定的位置加载到配置文件,并将文件包装成一个Resource对象.这篇文章我们将要探讨的就是,如何从这个Resouce对象中加载到我们的容器?加载到容器后又 ...

  4. Spring源码阅读笔记04:默认xml标签解析

    上文我们主要学习了Spring是如何获取xml配置文件并且将其转换成Document,我们知道xml文件是由各种标签组成,Spring需要将其解析成对应的配置信息.之前提到过Spring中的标签包括默 ...

  5. Spring源码阅读笔记

    前言 作为一个Java开发者,工作了几年后,越发觉力有点不从心了,技术的世界实在是太过于辽阔了,接触的东西越多,越感到前所未有的恐慌. 每天捣鼓这个捣鼓那个,结果回过头来,才发现这个也不通,那个也不精 ...

  6. Spring源码阅读笔记01:源码阅读环境准备

    1. 写在前面 对于做Java开发的同学来说,Spring就像是一条绕不过去的路,但是大多数也只是停留在对Spring的简单使用层面上,对于其背后的原理所知不多也不愿深究,关于这个问题,我在平时的生活 ...

  7. spring源码阅读笔记06:bean加载之准备创建bean

    上文中我们学习了bean加载的整个过程,我们知道从spring容器中获取单例bean时会先从缓存尝试获取,如果缓存中不存在已经加载的单例bean就需要从头开始bean的创建,而bean的创建过程是非常 ...

  8. spring源码阅读笔记08:bean加载之创建bean

    上文从整体视角分析了bean创建的流程,分析了Spring在bean创建之前所做的一些准备工作,并且简单分析了一下bean创建的过程,接下来就要详细分析bean创建的各个流程了,这是一个比较复杂的过程 ...

  9. spring源码阅读笔记09:循环依赖

    前面的文章一直在研究Spring创建Bean的整个过程,创建一个bean是一个非常复杂的过程,而其中最难以理解的就是对循环依赖的处理,本文就来研究一下spring是如何处理循环依赖的. 1. 什么是循 ...

随机推荐

  1. Linux把内存挂载成硬盘提高读写速度

    tmpfs是一种虚拟内存文件系统正如这个定义它最大的特点就是它的存储空间在VM里面,这里提一下VM(virtual memory),VM是由linux内核里面的vm子系统管理,现在大多数操作系统都采用 ...

  2. 关于va_list实例

    printf函数: #include <stdio.h> #include <stdarg.h> int myself_printf(char *format, ...) { ...

  3. docker E: Unable to locate package nginx

    在使用docker容器时,有时候里边没有安装vim,敲vim命令时提示说:vim: command not found,这个时候就需要安装vim,可是当你敲apt-get install vim命令时 ...

  4. Invalid action class configuration that references an unknown class问题原因之s:select

    早先做个练习项目就出现了这个错误,各种查资料,然后各种尝试,依然没有解决,不过可以确定是前台页面导致的. 今天又碰到了这个问题,头疼啊!不能再略过了,使用最笨的方法,一个模块一个模块的排除.先看下我的 ...

  5. mysql 事务处理 (转)

    事务处理在各种管理系统中都有着广泛的应用,比如人员管理系统,很多同步数据库操作大都需要用到事务处理.比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如 ...

  6. firefox45版本与seleniumIDE

    firefox45版本与seleniumIDE https://blog.csdn.net/seanlyly/article/details/80203896 seleniumIDE与firefox版 ...

  7. 自主知识产权受热捧 瑞星ESM SOHO版全力护卫小微企业

    小微企业现在可以说是我国国民经济中最重要的组成部分,在总产值.利税.解决就业等方面,都在为国家积极贡献着自己的力量.但在小微企业一片欣欣向荣的背后,却有着难言之隐--那就是困扰着广大小微企业多年的企业 ...

  8. 基于Jquery的textarea滚动条插件(原创)

    之前项目中自己写的滚动条插件.先前太忙没有好好整理.现在项目间歇期拿出来整理后贴出来 Demo Here css 我是把mCustomScrollbar 的UI 扣下来的. 这里我要介绍下这个插件不错 ...

  9. vue子组件使用自定义事件向父组件传递数据

    使用v-on绑定自定义事件可以让子组件向父组件传递数据,用到了this.$emit(‘自定义的事件名称’,传递给父组件的数据) <!DOCTYPE html> <html lang= ...

  10. python编程基础之集合

    集合是是基本数据类型的一种集合类型. 作用:去重 属性:intersection.union.difference.issubset 实例: list_1=[1,2,3,4,3,5,2,6,1]lis ...