2019独角兽企业重金招聘Python工程师标准>>>

spring version : 4.3.x

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

一. 自定义标签的定义和使用方式

在具体开挖源码之前,我们还是来回忆一下自定义标签的定义和使用方式,整体上与上一篇 1.2 小节所定义的方式类似,但还是有些许差别。要自定义标签,分为 5 步:

  1. 创建标签实体类
  2. 定义标签的描述 XSD 文件
  3. 创建一个标签元素解析器,实现 BeanDefinitionParser 接口
  4. 创建一个 handler 类,继承自 NamespaceHandlerSupport
  5. 编写 spring.handlers 和 spring.schemas 文件

这里我们自定义实现一个类似 <alias/> 功能的标签,来为指定的 bean 添加别名。第一步,先创建标签对应的实体:

public class Alias {private String name;private String alias;// 省略 getter 和 setter
}

第二步,定义标签的 XSD 文件 custom-alias.xsd:

<?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,并覆盖对应的方法:

public class CustomBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {@Overrideprotected Class<?> getBeanClass(Element element) {return Alias.class;}@Overrideprotected 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,用于注册第三步中定义的标签解析器:

public class CustomNamespaceHandler extends NamespaceHandlerSupport {@Overridepublic void init() {this.registerBeanDefinitionParser("alias", new CustomBeanDefinitionParser());}}

第五步,编写 spring.handlers 和 spring.schemas 文件:

spring.handlers

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

spring.schemas

http://www.zhenchao.org/schema/alias.xsd=META-INF/custom-alias.xsd

接下来演示一下上述自定义标签的使用方式,首先需要在 <beans/> 标签属性中定义标签的命名空间:

<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.xsdhttp://www.zhenchao.org/schema/alias http://www.zhenchao.org/schema/alias.xsd"

然后使用我们自定义的标签为已定义的 bean 添加别名:

<!-- my-parent-bean 是一个已定义的 bean -->
<myalias:alias id="my-alias" c_name="my-parent-bean" c_alias="aaa; bbb"/>

这样我们完成了利用自定义的标签为 my-parent-bean 添加别名,接下来我们开挖自定义标签的解析过程。

二. 自定义标签的解析过程

再来回顾一下我们开始解析标签的入口函数 parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate):

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {if (delegate.isDefaultNamespace(root)) {// 解析默认标签(beans标签)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);}
}

上一篇中我们探究了默认标签的解析过程,也就是 parseDefaultElement(Element element, BeanDefinitionParserDelegate delegate) 方法,接下来我们来探究自定义标签的解析过程,及 parseCustomElement(Element ele) 方法:

public BeanDefinition parseCustomElement(Element ele) {return this.parseCustomElement(ele, null);
}
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {// 获取标签的命名空间String namespaceUri = this.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));
}

上述方法首先会去获取自定义标签的命名空间定义,然后基于命名空间解析得到对应的 NamespaceHandler,最后调用 handler 对标签进行解析处理,本质上调用的就是前面自定义实现的 doParse() 方法。我们先来看一下自定义标签 NamespaceHandler 的解析过程,位于 DefaultNamespaceHandlerResolver 的 resolve(String namespaceUri) 方法中:

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;} else if (handlerOrClassName instanceof NamespaceHandler) {// 已经解析过,直接返回handler实例return (NamespaceHandler) handlerOrClassName;} else {// 未做过解析,则解析对应的类路径classNameString className = (String) handlerOrClassName;try {// 使用反射创建handler实例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);// 调用init()方法namespaceHandler.init();// 缓存解析后的handler实例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);}}
}

方法中的逻辑可以概括如下:

  1. 从 spring.handlers 获取所有注册的 handler 集合
  2. 从集合中获取 namespace 对应 handler
  3. 如果 handler 已经被解析过则返回对应的 handler 实例,否则继续
  4. 利用反射创建 handler 实例,并初始化
  5. 调用 handler 的 init() 方法
  6. 缓存解析后的 handler 实例

上述过程中第 4 步稍微复杂一点,我们来看一下具体过程。我们在 spring.handlers 中会配置 namespaceUri 与对应 handler 全称类名的键值对:

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

这里会拿到对应 handler 的全称类名,然后基于反射来创建 handler 实例,过程中会设置构造方法为 accessible。接下来就是轮到第五步中的调用 init() 方法,这个方法是由开发人员自己实现的,我们前面的例子中通过该方法将我们自定义的解析器 CustomBeanDefinitionParser 注册到 handler 实例中。接下来就是调用 handler 实例处理自定义标签:

public BeanDefinition parse(Element element, ParserContext parserContext) {// 寻找解析器并进行解析return this.findParserForElement(element, parserContext) // 找到对应的解析器.parse(element, parserContext);  // 进行解析(这里的解析过程是开发者自定义实现的)
}

这里主要分为 获取 handler 实例执行解析 两个步骤,其中获取 handler 实例就是依据我们使用的标签名从之前的缓存 map 中拿到对应的对象,然后调用 handler 的 parse(Element element, ParserContext parserContext) 方法执行解析逻辑:

public final BeanDefinition parse(Element element, ParserContext parserContext) {// 1. 创建自定义标签BeanDefinition实例,并调用自定义解析器进行解析处理AbstractBeanDefinition definition = this.parseInternal(element, parserContext);if (definition != null && !parserContext.isNested()) { // nested:嵌套的// definition实例存在且不是嵌套的try {// 2. 获取标签的id属性,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. 将AbstractBeanDefinition转化成BeanDefinitionHolder并注册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) {parserContext.getReaderContext().error(ex.getMessage(), element);return null;}}return definition;
}

上述方法中的第一步是整个方法的核心,我们后面细讲,先来看一下第二、三步骤,对于自定义标签来说,id 属性是必备的,此外 Spring 还内置了 name 和 parentName 字段,这些名称是不允许使用的,否则达不到我们预期的结果,笔者第一次使用自定义标签时就踩了坑,用了 name 作为自定义标签属性名,结果就是各种奇怪的结果。

接下来看看第一步的逻辑,位于 AbstractSingleBeanDefinitionParser 的 parseInternal(Element element, ParserContext parserContext) 方法中:

protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {// 初始化自定义标签实例BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();// 如果设置了parentNameString 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));if (parserContext.isNested()) {// 如果当前标签是嵌套的,则使用父类的scope属性builder.setScope(parserContext.getContainingBeanDefinition().getScope());}// 设置延迟加载if (parserContext.isDefaultLazyInit()) {builder.setLazyInit(true);}// 调用自定义解析器覆盖的doParse方法进行解析this.doParse(element, parserContext, builder);// 返回自定义标签的beanDefinition实例return builder.getBeanDefinition();
}

上述方法中首先会初始化创建一个 BeanDefinitionBuilder 对象,然后依据配置设置对象的相应属性,其中包括调用我们之前在实现自定义标签解析器 CustomBeanDefinitionParser 时候覆盖的 getBeanClass 方法。然后会调用 doParse 方法,该方法由开发者实现,也是我们解析自定义标签的核心方法:

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 对于配置文件的解析工作已经做完了,容器将一个个 bean 的静态配置解析映射称为 beanDefinition 实例,并注册到容器的 Map 集合中,剩下的就是对 bean 实例的创建和初始化过程了,我们在下一篇中对这一过程的具体实现进行详细探究。

系列文章

  1. Spring源码解析:获取源码
  2. Spring源码解析:资源的描述与加载
  3. Spring源码解析:IoC容器的基本结构设计
  4. Spring源码解析:简单容器中Bean的加载过程初探
  5. Spring源码解析:默认标签的解析过程
  6. Spring源码解析:自定义标签的解析过程
  7. Spring源码解析:Bean实例的创建与初始化
  8. Spring源码解析:高级容器的扩展内幕
  9. Spring源码解析:循环依赖的探测与处理

鉴于作者水平有限,文中不免有错误之处,欢迎大家批评指正~

同步更新站点:www.zhenchao.org

转载于:https://my.oschina.net/wangzhenchao/blog/917481

Spring源码解析:自定义标签的解析过程相关推荐

  1. spring 源码分析(1)-xml文件解析

    我们在最开始接触spring的时候,看到不少书spring入门的例子如下 ApplicationContext atx = new ClassPathXmlApplicationContext(&qu ...

  2. Spring源码分析之Bean的创建过程详解

    前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...

  3. spring源码分析06-spring配置类解析

    什么是spring配置类? 类上有注解:@Configuration .@Component.@ComponentScan.@Import.@ImportResource 或者类中的任意方法有@Bea ...

  4. 《Spring源码深度解析 郝佳 第2版》XML标签的解析

    目录 往期博客<Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 分析了xml文件的加载,接下来就是标签的解析,入口函数有两个 默认标签的解析 自定义标签的解析 一 ...

  5. 【Spring源码】4. 自己搞个标签?~自定义标签保姆级全过程(图解向,堆图预警)

    [Spring源码系列- IOC] 1 [Spring源码]0.安装Gradle环境 2 [Spring源码]1.下载与编译_pom relocation to an other version nu ...

  6. Spring 源码第三弹!EntityResolver 是个什么鬼?

    上篇文章和小伙伴们说了 Spring 源码中 XML 文件的解析流程,本来可以继续往下走看加载核心类了,但是松哥还是希望能够慢一点,既然要学就学懂,在 XML 文件解析的过程中还涉及到一些其他的类和概 ...

  7. Mac系统Spring源码导入

    Mac系统Spring源码导入 前言 工具准备 Gradle5.6.4安装 Spring-Framework5.2.8 Kotlin插件 spring-framework配置 修改settings.g ...

  8. Spring源码解析(五)——自定义标签解析

    2019独角兽企业重金招聘Python工程师标准>>> 前言 作为标签解析的第二分支,也正是因为自定义标签的存在,才让Spring框架的诸多功能在短短的几行配置代码后,就生效了. 源 ...

  9. java基础巩固-宇宙第一AiYWM:为了维持生计,Spring全家桶_Part1-3(学学Spring源码呗:默认的标签和自定义标签是咋解析的)~整起

    Part3:上一次说到了Spring的DefaultBeanDefinitionDocumentReader类中的parseBeanDefinitions(Element root, BeanDefi ...

最新文章

  1. 你猜,为什么Google和Facebook不用Docker?
  2. ARCGIS影像配准教程
  3. 数牍科技完成超3亿元A轮融资,隐私工程守护数据全生命周期流通协作
  4. 【数据挖掘】基于密度的聚类方法 - OPTICS 方法 ( 核心距离 | 可达距离 | 族序 )
  5. 开源路由器爱好者迎来利好 Linksys不变
  6. shell -- shift用法
  7. python给用户打标签_python用户评论标签匹配的解决方法
  8. Linux上Svn环境搭建
  9. 部署flas到服务器:No module named flask
  10. sysstat工具包提供的主要命令
  11. 我的vim和emacs配置文件
  12. MYSQL入门基础知识
  13. 怎么判断冠词用a还是an_怎么判断英语量词改用a还是an
  14. 朋友圈微信投票很麻烦?python开发个自动化刷票脚本,再也不用头痛了!
  15. 【NEW02】Servlet 基础
  16. 计算机快速格式化u盘启动,制作启动盘格式化u盘
  17. java高并发之魂:Synchronize
  18. NC-Verilog仿真
  19. Python编曲实践(八):我,乔鲁诺·乔巴那,能用两百行代码写出JOJO黄金之风里我自己的出场曲!
  20. 聊聊ClickHouse中的低基数LowCardinality类型

热门文章

  1. java中HashMap遍历详解
  2. pycuda installation error: command 'gcc' failed with exit status 1
  3. SciPy和Numpy处理能力
  4. java poi 解析excel_Java用POI解析excel并获取所有单元格数据
  5. java开源库web3j的以太坊过滤器(filter)和智能合约事件(event)教程
  6. 趁webpack5还没出,先升级成webpack4吧
  7. BZOJ 2720 [Violet 5]列队春游 ——期望DP
  8. Electron初步【02】--第一个Electron App
  9. Linux命令执行顺序— ||和和; 比较
  10. 网银无法登录解决办法