自定义标签的解析

简介

我们知道在Spring 中存在 默认标签和自定义标签 两种类型,前面说过默认标签了,现在来分析一下Spring中自定义标签的加载过程。同样,现在回顾一下,当完成从配置文件到Document 的转换并提取对应的root后,将开始了所有元素的解析,而在这一过程中便开始了默认标签与自定义标签两种格式的区分,函数如下:

DefaultBeanDefinitionDocumentReader.java
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 自定义标签大致需要如下几步:

  1. 创建需要扩展的组件 (spring.schemas)
  2. 定义XSD 文件描述组件的内容
  3. 创建一个类,实现BeanDefinitionParser接口,用来解析XSD文件中的定义和组件定义
  4. 创建一个类,继承 NamespaceHandlerSupport, 重新init方法,目的是将组件注册到Spring容器
  5. 创建 spring.handlers 文件,指定继承NamespaceHandlerSupport的实现类的位置。
栗子:

文件结构如下

在这里插入图片描述

创建POJO, 接收配置文件

public class User {

	private String userName;

	private String email;
	
	public String getUserName() {
		return userName;
	}

	public String getEmail() {
		return email;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public void setEmail(String email) {
		this.email = email;
	}
}

定义一个XSD文件描述组件内容

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.test.org/schema/custom"
		xmlns:xsd="http://www.w3.org/2001/XMLSchema"
		targetNamespace="http://www.test.org/schema/custom">
    <!-- targetNamespace中的值必须和xmlns 中的值一样-->

	<xsd:element name="test">
		<xsd:complexType>
			<xsd:attribute name="id" type="xsd:string"/>
			<xsd:attribute name="userName" type="xsd:string"/>
			<xsd:attribute name="email" type="xsd:string"/>
		</xsd:complexType>
	</xsd:element>
</xsd:schema>

创建UserBeanDefinitionParser 继承 AbstractSingleBeanDefinitionParser

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

	@Override
	protected Class getBeanClass(Element element){
		return User.class;
	}

	// 从element 中解析并提取对应的元素
	@Override
	protected void doParse(Element element, BeanDefinitionBuilder bean){
		String id = element.getAttribute("id");
		String userName = element.getAttribute("userName");
		String email = element.getAttribute("email");
		// 将提取的数据放入到BeanDefinitionBuilder 中,待到完成所有bean的解析后同一注册到beanFactory中
		if (StringUtils.hasText(userName)){
			bean.addPropertyValue("userName",userName);
		}
		if (StringUtils.hasText(email)){
			bean.addPropertyValue("email",email);
		}
	}
}

注意:我们没有直接实现BeanDefinitionParser接口,因为AbstractSingleBeanDefinitionParserBeanDefinitionParser接口的抽象类

创建 MyNamespaceHandler,继承NamespaceHandlerSupport,将组件注册到Spring 容器中

public class MyNamespaceHandler extends NamespaceHandlerSupport {

	// 将注册组件注册到Spring 容器
	@Override
	public void init() {
		System.out.println("init MyNamespaceHandler");
		registerBeanDefinitionParser("test", new UserBeanDefinitionParser());
	}
}

编写spring.handlersspring.schemas 文件

spring.handlers
http\://www.test.org/schema/custom=com.zh.sound.custom.MyNamespaceHandler

注意\的作用是转义
spring.schemas

http\://www.test.org/schema/custom/spring-custom.xsd=META-INF/spring-custom.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:custom="http://www.test.org/schema/custom"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
	   http://www.test.org/schema/custom http://www.test.org/schema/custom/spring-custom.xsd"
	default-lazy-init="false">

	<custom:test id="testBean" userName="zhangsan" email="123456" />

</beans>

测试

public class TestMain {
	public static void main(String[] args) {
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext("customerContext.xml");
		User user = (User) applicationContext.getBean("testBean");
		System.out.println(user.getEmail());
		System.out.println(user.getUserName());
	}
}

不出意外我们的输出结果为:123456 zhangsan
在上面的例子中,我们实现了通过自定义标签实现了通过属性的方式将user 类型的bean赋值,在Spring 中自定义标签非常常用,例如我们熟知的事务标签:tx(<tx:annotation-driven>)

自定义标签解析

了解了自定义标签的使用后,我们来分析一下自定义标签的解析过程。
BeanDefinitionParserDelegate.java

public BeanDefinition parseCustomElement(Element ele) {
		return parseCustomElement(ele, null);
	}

// containingBd 为父类bean, 对顶层元素的解析应设置为 null
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获取对应的命名空间,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析

获取标签的命名空间

标签的解析是从命名空间的提取开始的,无论是区分 Spring 中默认标签和自定义标签还是区分自定义标签中不同标签的处理器都是以标签所提供的命名空间为基础的,而至于如何提取对应元素的命名空间其实并不需要我们亲自去实现,在 org.w3c.dom.Node 中已经提供了方法供我们直接调用:

public String getNamespaceURI(Node node) {
		return node.getNamespaceURI();
	}
提取自定义标签处理器

有了命名空间,就可以进行NamespaceHandler的提取了,继续之前的parseCustomElement 函数的跟踪,分析 NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri),在 readerContext 初始化的时候其属性namespaceHandlerResolver 已经被初始化为了 DefaultNamespaceHandlerResolver 的实例,所以,这里调用的resolve 方法其实调用的是 DefaultNamespaceHandlerResolver 类中的方法。我们进入DefaultNamespaceHandlerResolverresolve 方法进行查看。

DefaultNamespaceHandlerResolver.java
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 {

	// 将注册组件注册到Spring 容器
	@Override
	public void init() {
		System.out.println("init MyNamespaceHandler");
		registerBeanDefinitionParser("test", new UserBeanDefinitionParser());
	}
}

当得到自定义命名空间处理后会马上执行 namespaceHandler.init() 来进行自定义 BeanDefinitionParser的注册。在这里,可以注册多个标签解析器,当前示例中只有支持 <custom:test> 的写法,也可以在这里注册多个解析器,如 <custom:testA>, <custom:testB> 等,使得 custom 的命名空间中可以支持多种标签解析。

注册后,命名空间处理器就可以根据标签的不同来调用不同的解析器进行解析。那么,根据上面的函数与之前介绍过的例子,我们基本上可以推断 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.isTraceEnabled()) {
						logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
					}
					try {
                        // this.handlerMappingsLocation 在构造函数中已经被初始化为:META-INF/spring.handlers
						Properties mappings =							PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
						if (logger.isTraceEnabled()) {
							logger.trace("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

标签解析

得到了解析器以及要分析的元素后,Spring就可以将解析工作委托给自定义解析器去解析了。在Spring 中的代码为:

return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值