在上篇文章中, 提到了在 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 容器的重头戏, 后面会详细学习的.
posted on 2020-03-14 22:20 木瓜芒果 阅读(...) 评论(...) 编辑 收藏
来源: https://www.cnblogs.com/volcano-liu/p/12384863.html