Spring IoC 自定义标签解析
前言
本系列全部基于 Spring 5.2.2.BUILD-SNAPSHOT 版本。因为 Spring 整个体系太过于庞大,所以只会进行关键部分的源码解析。
本篇文章主要介绍 Spring IoC 容器怎么解析自定义标签的。
正文
在分析自定义标签怎么解析之前,我们先看如何自定义一个标签以及让其能被 Spring 解析并加载成 bean。
自定义标签
编写 XML Schema 文件:定义 XML 结构
首先编写一个 users.xsd 文件,其中定义了 User 类型的属性(该类在Spring IoC BeanDefinition 的加载和注册一文中定义过);内容如下:
<xsd:schema xmlns="http://ioc.leisurexi.com/schema/users"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://ioc.leisurexi.com/schema/users">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<!-- 定义 User 类型(复杂类型) -->
<xsd:complexType name="User">
<xsd:attribute name="id" type="xsd:long" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
<!-- 定义 user 元素 -->
<xsd:element name="user" type="User"/>
</xsd:schema>
注意:上面 XML 中的头部的地址,我是根据自己的包名来修改的,你需要在
resource目录下创建对应的路径并把 xsd 文件放在该目录下。
编写 user-context.xml 文件,并在其中使用我们刚才的自定义标签;内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:users="http://ioc.leisurexi.com/schema/users"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://ioc.leisurexi.com/schema/users
http://ioc.leisurexi.com/schema/users.xsd">
<users:user id="1" name="leisurexi"/>
</beans>
自定义 BeanDefinitionParser 实现:XML 元素与 BeanDefinition 解析
接下来我们定义一个类来继承 AbstractSingleBeanDefinitionParser 重写标签的解析方法,如下:
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return User.class;
}
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
setPropertyValue("id", element, builder);
setPropertyValue("name", element, builder);
}
private void setPropertyValue(String attributeName, Element element, BeanDefinitionBuilder builder) {
String attributeValue = element.getAttribute(attributeName);
if (StringUtils.hasText(attributeValue)) {
builder.addPropertyValue(attributeName, attributeValue);
}
}
}
自定义 NamespaceHandler 实现:命名空间绑定
然后我们实现 NamespaceHandlerSupport 去注册元素对应的解析类,如下:
public class UserNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
// 将 "user" 元素注册对应的 BeanDefinitionParser 实现
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}
实现类写好了,但还没完事;还需要在 META-INF 目录下创建一个 spring.handlers 文件,写上命名空间和解析类的映射关系,这是 Spring 中的约定,文件名和目录得一样;内容如下:
http\://ioc.leisurexi.com/schema/users=com.leisurexi.ioc.metadata.UserNamespaceHandler
注册 XML 扩展:命名空间与 XML Schema 映射
在 META-INF 创建一个 spring.schemas 文件,这也是约定,文件名和目录得一样;内容如下:
http\://ioc.leisurexi.com/schema/users.xsd=com/leisurexi/ioc/metadata/users.xsd
接着编写测试类,看是否可以得到我们定义的 bean,代码如下:
public static void main(String[] args) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
reader.loadBeanDefinitions("META-INF/user-context.xml");
User user = beanFactory.getBean(User.class);
System.out.println(user);
}
最后我把目录结构贴出来:

注意:
com.leisurexi.ioc.metadata是层级目录,最好目录一个接一个往下创建。
代码解析
BeanDefinitionParserDelegate#parseCustomElement
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取自定义标签的命名空间
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));
}
上面代码中的 namespaceUri 就是我们在 XML 中定义的 http://ioc.leisurexi.com/schema/users。
DefaultNamespaceHandlerResolver#resolve
public NamespaceHandler resolve(String namespaceUri) {
// 命名空间和其处理类的映射
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据命名空间获取处理类的全类名或其实例
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
// 如果已经是NamespaceHandler类型,代表已经实例化过,直接返回
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
// 没有对handlerOrClassName实例化,进行实例化,并将实例缓存进handlerMappings
else {
String className = (String) handlerOrClassName;
try {
// 根据全类名获取对应的Class对象
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");
}
// 实例化对应的Class对象
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 执行init()方法,也就是在这里会注册元素的BeanDefinitionParser实现
namespaceHandler.init();
// 将实例化后的处理类,放入handlerMappings,下次就不用再次创建了
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
// 省略异常处理...
}
}
上面方法中的 getHandlerMappings() 方法返回的就是 spring.handlers 文件中的内容;key 为命名空间,value 为全类名。如下所示:

NamespaceHandlerSupport#parse
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 获取BeanDefinition解析器
BeanDefinitionParser parser = findParserForElement(element, parserContext);
// 进行标签的解析,见下文详解
return (parser != null ? parser.parse(element, parserContext) : null);
}
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 获取标签元素,比如我们自定义的 <users:user id="1" name="leisurexi"/>,这里获取的就是user
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;
}
上面方法中的 parsers 中的内容,就是上面 UserNamespaceHandler 类中重写 init() 方法中注册的解析器。
AbstractBeanDefinitionParser#parse
public final BeanDefinition parse(Element element, ParserContext parserContext) {
// 获取调用自定义解析方法后的BeanDefinition,见下文详解
AbstractBeanDefinition definition = parseInternal(element, parserContext);
if (definition != null && !parserContext.isNested()) {
try {
// 获取id属性,如果有的话,没有的话就生成一个默认的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)) {
// 如果name属性不为空,根据逗号分割成数组作为别名
aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}
// 用definition、id和aliases构建成BeanDefinitionHolder
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
// 注册BeanDefinition
registerBeanDefinition(holder, parserContext.getRegistry());
if (shouldFireEvents()) {
// 发送bean注册完成事件
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;
}
上面方法中的 resolveId() 方法会获取标签中的 id 属性,如果没有的话会调用 BeanDefinitionReaderUtils.generateBeanName() 来生成一个,该方法和 registerBeanDefinition() 方法在Spring IoC 默认标签解析一文中有介绍。
AbstractSingleBeanDefinitionParser#parseInternal
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
// 获取父bean名称
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
// 获取bean的类型,这里就会调用UserBeanDefinitionParser重写的getBeanClass()方法
Class<?> beanClass = getBeanClass(element);
if (beanClass != null) {
// 设置进BeanDefinition中
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
// 如果没有重写getBeanClass()方法,就调用getBeanClassName()方法
else {
String beanClassName = getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}
builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
// 获取当前BeanDefinition的外层BeanDefinition
BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
if (containingBd != null) {
// 内嵌bean必需和外层bean有相同的作用域
builder.setScope(containingBd.getScope());
}
if (parserContext.isDefaultLazyInit()) {
builder.setLazyInit(true);
}
// 调用子类重写的doParse()方法
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
}
上面方法中 doParse() 方法就会调用我们自定义的 UserBeanDefinitionParser 类中的 doParse() 方法。
总结
本文主要介绍了 Spring 对 XML 文件中自定义标签的解析,我们可以重新梳理一下思路:
- 根据命名空间获取对应的
NamespaceHandler。 - 根据元素名称获取对应的
BeanDefinitionParser。 - 调用重写的
doParse()方法来构建BeanDefinition。 - 注册
BeanDefinition以及发送bean注册完成事件。
参考
Spring IoC 自定义标签解析的更多相关文章
- Spring IoC 默认标签解析
前言 本系列全部基于 Spring 5.2.2.BUILD-SNAPSHOT 版本.因为 Spring 整个体系太过于庞大,所以只会进行关键部分的源码解析. 本篇文章主要介绍 Spring IoC 容 ...
- spring源码深度解析— IOC 之 自定义标签解析
概述 之前我们已经介绍了spring中默认标签的解析,解析来我们将分析自定义标签的解析,我们先回顾下自定义标签解析所使用的方法,如下图所示: 我们看到自定义标签的解析是通过BeanDefinition ...
- 基于Spring开发——自定义标签及其解析
1. XML Schema 1.1 最简单的标签 一个最简单的标签,形式如: <bf:head-routing key="1" value="1" to= ...
- Spring——使用自定义标签
文章内容参考了<Spring源码深度解析>一书.自己照着书中内容做了一遍,不懂的地方以及采坑的地方会在文中记录. 推荐一篇post,关于Spring配置文件的命名空间: https://w ...
- Spring IOC设计原理解析:本文乃学习整理参考而来
Spring IOC设计原理解析:本文乃学习整理参考而来 一. 什么是Ioc/DI? 二. Spring IOC体系结构 (1) BeanFactory (2) BeanDefinition 三. I ...
- Spring IoC源码解析之invokeBeanFactoryPostProcessors
一.Bean工厂的后置处理器 Bean工厂的后置处理器:BeanFactoryPostProcessor(触发时机:bean定义注册之后bean实例化之前)和BeanDefinitionRegistr ...
- Spring IoC源码解析之getBean
一.实例化所有的非懒加载的单实例Bean 从org.springframework.context.support.AbstractApplicationContext#refresh方法开发,进入到 ...
- angularjs directive (自定义标签解析)
angularjs directive (自定义标签解析) 定义tpl <!-- 注意要有根标签 --> <div class="list list-inset" ...
- Spring系列(三):Spring IoC源码解析
一.Spring容器类继承图 二.容器前期准备 IoC源码解析入口: /** * @desc: ioc原理解析 启动 * @author: toby * @date: 2019/7/22 22:20 ...
随机推荐
- CSS用户自定义样式
在浏览器中打开前面写的例子页面,你会发现 <strong> 元素中的文字会比其他文字粗一些.这些样式就是在浏览器定义的默认HTML样式. 而<strong> 元素是红色的,这是 ...
- 使用 Typora 编辑器运用 Markdown 的语法编写文档
Markdown 介绍 Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档. Markdown 语言在 2004 由约翰·格鲁伯(英语:John Gruber)创建. ...
- Java获取当前时间到凌晨12点剩余秒数
有个获取登陆用户是否每天第一次登陆系统需求,考虑不需要入库操作,就用redis设置key每天凌晨0点删除 /** * 获取当前时间到凌晨12点的秒数 * @return */ public Long ...
- Html超链接和表单
超链接 a href="链接地址" target="链接打开方式" title="当鼠标放在连接上显示的文字"></a>链 ...
- 移动端border:1px问题解决方案
了解设备像素和css像素的因该知道,通常我们在写移动端时,是按照设计稿标注的像素除以设备的DPR来写真实的像素, 比如在iPhone6上,我们写的20px字体世界上在视觉效应上有20px; 所以当我们 ...
- Java实现 LeetCode 753 破解保险箱(递归)
753. 破解保险箱 有一个需要密码才能打开的保险箱.密码是 n 位数, 密码的每一位是 k 位序列 0, 1, -, k-1 中的一个 . 你可以随意输入密码,保险箱会自动记住最后 n 位输入,如果 ...
- Java实现 LeetCode 689 三个无重叠子数组的最大和(换方向筛选)
689. 三个无重叠子数组的最大和 给定数组 nums 由正整数组成,找到三个互不重叠的子数组的最大和. 每个子数组的长度为k,我们要使这3*k个项的和最大化. 返回每个区间起始索引的列表(索引从 0 ...
- java中Condition类的详细介绍(详解)
已找不到原文了,还望原文博主看到能告诉小白一下,一定标注原文地址 一 condition 介绍及demo Condition是在java 1.5中才出现的,它用来替代传统的Object的wait(). ...
- java实现第五届蓝桥杯猜字母
猜字母 题目描述 把abcd-s共19个字母组成的序列重复拼接106次,得到长度为2014的串. 接下来删除第1个字母(即开头的字母a),以及第3个,第5个等所有奇数位置的字母. 得到的新串再进行删除 ...
- Andorid中写文件后在电脑上看不到的解决办法
每次通过输出流往SD卡写入文件,连接上电脑,用MTP的方式模拟一个移动磁盘,打开磁盘却没有这样一个文件,而通过adb的方式查看就有,造成这个现象的原因是,每次写入之后,MTP的数据库并没有更新,因为更 ...