Spring5源码分析(020)——IoC篇之解析自定义标签
注:《Spring5源码分析》汇总可参考:Spring5源码分析(002)——博客汇总
默认标签的解析,前面已基本分析完毕。剩下的就是自定义标签的解析这一块:
/** * Parse the elements at the root level in the document: * "import", "alias", "bean". * <p>文档中的 root 级别的元素解析: "import", "alias", "bean" * @param root the DOM root element of the document */ protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { // 检查 root Element (<beans /> 根标签) 的命名空间是否为空,或者是 http://www.springframework.org/schema/beans 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; // 检查 child Element 的命名空间是否为空,或者是 http://www.springframework.org/schema/beans // 即检查是否使用的是默认的命名空间,然后执行默认的解析 if (delegate.isDefaultNamespace(ele)) { parseDefaultElement(ele, delegate); } else { // 非默认的命名空间,进行自定义标签解析 delegate.parseCustomElement(ele); } } } } else { // 非默认的命名空间,进行自定义标签解析 delegate.parseCustomElement(root); } }
接下来主要从自定义标签的使用、具体解析过程等方面进行讲解,本文的目录结构如下:
1、自定义标签的使用
Spring 默认的标签配置,已覆盖了绝大多数使用场景,对于需要更为复杂或者更多丰富控制的,可以考虑 Spring 提供的可扩展 Schema 支持,进行自定义 XML 配置。如何定制 XML 配置及解析,具体可以参考 Spring 官方文档说明[1]或者《Spring源码深度解析(第2版)》[2]中的相关案例。Spring 从 2.0 开始就提供了自定义 XML 配置的扩展机制,创建新的 XML 配置扩展大致有以下几个步骤(Spring 文档原文):
1. Author an XML schema to describe your custom element(s).
2. Code a custom NamespaceHandler implementation.
3. Code one or more BeanDefinitionParser implementations (this is where the real work is done).
4. Register your new artifacts with Spring.
这里我们再细化下,分为以下6个步骤:
- 1、创建一个需要扩展使用的组件
- 简单理解就是 bean ,毕竟我们需要使用的就是 bean,这里可以是自定义的,也可以是已有的 POJO
- 2、创建定义 XML 模式的 xsd 文件,用于描述自定义元素,即描述组件内容
- 3、创建一个或者多个 BeanDefinitionParser 解析器实现,用来解析 XSD 文件中的定义和组件定义(实际解析的地方)
- 4、创建自定义的 NamespaceHandler 处理器实现,目的是将组件注册到 Spring 容器
- 5、编写 spring.handlers 和 spring.schemas 文件
- 6、使用自定义 bean
1.1、创建一个需要扩展使用的组件
从实际使用目的来看,配置都是为了定义、解析和使用 bean ,就算是自定义配置,其实也是为了最终用上对应的 bean 。因此,自定义配置首先需要做的就是创建一个需要扩展使用的组件,可以简单理解就是定义需要使用到的 bean,当然,可以是完全自定义的,也可以是使用现有的 POJO 。这里就自定义一个简单的 POJO 来进行说明。
package cn.wpbxin.bean.customxml; /** * 自定义bean */ public class CustomComponent { private String name; private boolean flag; // toString/getter/setter ... }
1.2、创建定义 XML 模式的 xsd 文件
组件有了,接下来就是对该组件进行描述,这里就是创建 xsd 文件,用于描述组件的自定义元素,也即是可以使用的元素( /META-INF/spring-xml-custom.xsd ):
<?xml version="1.0" encoding="UTF-8"?> <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.wpbxin.com.cn/schema/customComponent" elementFormDefault="qualified"> <element name="customComponent"> <complexType> <attribute name="id" type="string"/> <attribute name="name" type="string"/> <attribute name="flag" type="boolean"/> </complexType> </element> </schema>
1.3、创建一个或者多个 BeanDefinitionParser 解析器实现
组件和XSD描述配置都OK了,接下来就是解析的问题,这里我们选择直接继承Spring提供的抽象类 AbstractSingleBeanDefinitionParser ,这样就可以只关注最核心的解析方法,其他的则由Spring提供的默认处理:
package cn.wpbxin.bean.customxml; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; import org.w3c.dom.Element; /** * 自定义 BeanDefinition 解析器 */ public class CustomComponentBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { @Override protected Class<?> getBeanClass(Element element) { return CustomComponent.class; } @Override protected void doParse(Element element, BeanDefinitionBuilder builder) { String name = element.getAttribute("name"); String flag = element.getAttribute("flag"); if (StringUtils.hasText(name)) { builder.addPropertyValue("name", name); } if (StringUtils.hasText(flag)) { builder.addPropertyValue("flag", Boolean.valueOf(flag)); } } }
从这里可以看到,我们只需要实现自定义标签元素的解析即可,其他的则完全交给 Spring 处理,我们无需关心,开箱即用就是方便。
1.4、创建自定义的 NamespaceHandler 处理器实现,将组件注册到 Spring 容器
这里直接继承 NamespaceHandlerSupport 来进行处理,我们只关心注册的那一步,重写 init() 方法,委托给 registerBeanDefinitionParser 方法将组件注册到 Spring 容器中。
package cn.wpbxin.bean.customxml; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; /** * 自定义命名空间处理器 */ public class MyNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser("customComponent", new CustomComponentBeanDefinitionParser()); // 可以注册多个命名空间,处理多个元素标签 } }
1.5、编写 spring.handlers 和 spring.schemas 文件
默认是在 /META-INF/ 目录下,Spring 会自动加载
spring.handlers (加载过程可以参考 2.2、根据命名空间提取自定义标签处理器 )
http\://www.wpbxin.com.cn/schema/customComponent=wpbxin.bean.customxml.MyNamespaceHandler
spring.schemas (加载过程参考 《Spring5源码分析(009)——IoC篇之加载BeanDefinition:获取 Document 实例》中的说明 3.1.2、PluggableSchemaResolver)
http\://www.wpbxin.com.cn/schema/spring-xml-custom.xsd=META-INF/spring-xml-custom.xsd
到这里,自定义的配置就结束了。Spring 加载自定义配置的大致流程是遇到自定义标签然后就去 spring.handlers 中去找对应的 handler ,默认路径是 /META-INF/ ,进而找到对应的 handler 以及解析元素的 Parser ,从而完成整个自定义元素的解析,也就是说,自定义配置与Spring中默认的标准配置的差异在于Spring将自定义标签的解析工作委托给了用户去实现。
1.6、使用自定义标签
前面一系列自定义配置和解析完成时候,接下来我们只需要在配置文件中引用,就可以使用自定义标签了:
<?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.wpbxin.com.cn/schema/customComponent" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.wpbxin.com.cn/schema/customComponent http://www.wpbxin.com.cn/schema/spring-xml-custom.xsd"> <myns:customComponent id = "customComponent" name="cc" flag="true" /> </beans>
测试类:
package cn.wpbxin.bean.customxml; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * 自定义标签解析测试类 */ public class CustomxmlMainApp { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("wpbxin/bean/customxml/spring-customxml.xml"); CustomComponent cc = (CustomComponent) applicationContext.getBean("customComponent"); System.out.println(cc); } }
运行后可以看到以下的输出,说明已经成功解析了:
CustomComponent{name='cc', flag=true}
1.7、自定义标签的应用场景
事务标签 tx ,即 <tx:annotation-driven>、aop、jdbc 等模块都是有进行 xml 扩展的,有兴趣的可以自行观赏相关的源码。
2、自定义标签的解析
讲完了自定义标签的使用和应用场景,接下来看看Spring是如何进行自定义标签的解析的。自定义标签的解析,调用的是 org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(Element ele) 方法来进行的处理,详细如下:
/** * Parse a custom element (outside of the default namespace). * <p>解析自定义元素(在默认命名空间之外) * @param ele the element to parse * @return the resulting bean definition */ @Nullable public BeanDefinition parseCustomElement(Element ele) { // containingBd 是父类 bean ,对顶层元素的解析应置为 null return parseCustomElement(ele, null); } /** * Parse a custom element (outside of the default namespace). * <p>解析自定义元素(在默认的空间之外) * @param ele the element to parse * @param containingBd the containing bean definition (if any) * @return the resulting bean definition */ @Nullable public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { // 1、获取标签对应的命名空间 String namespaceUri = getNamespaceURI(ele); if (namespaceUri == null) { return null; } // 2、根据命名空间找到对应的命名空间处理器 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; } // 3、调用自定义的 NamespaceHandler 进行解析 return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); }
相信看了上面关于自定义标签的使用方法之后,大家或多或少对自定义标签的实现套路有所理解了,总结起来就是下面这3个步骤:
- 1、根据 bean 配置获取标签对应的命名空间;
- 2、根据命名空间解析获取对应的处理器 NamespaceHandler;
- 3、调用自定义的处理器 NamespaceHandler 进行标签解析。
下面将对这3个步骤进行详细的分析。
2.1、获取标签的命名空间
标签的解析都是从命名空间的提取开始的,无论是Spring本身的默认标签还是用户的自定义标签,而且区分自定义标签中的不同标签的处理器都是以标签所提供的命名空间为基础的,至于如何提取对应元素的命名空间,其实在 org.w3c.dom.Node 中已经提供了方法拱我们直接调用:
/** * Get the namespace URI for the supplied node. * <p>The default implementation uses {@link Node#getNamespaceURI}. * Subclasses may override the default implementation to provide a * different namespace identification mechanism. * <p>获取所提供节点的命名空间 URI,默认实现是 {@link org.w3c.dom.Node#getNamespaceURI} * <p>子类可以覆盖默认实现,以提供不同的命名空间标识机制。 * @param node the node */ @Nullable public String getNamespaceURI(Node node) { return node.getNamespaceURI(); }
2.2、根据命名空间提取自定义标签处理器
有了命名空间,就可以提取对应的 NamespaceHandler 了。继续跟踪可以发现这里是通过 NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); 来进行处理器的获取,而 readerContext 在初始化时其属性 namespaceHandlerResolver 已经被初始化为 DefaultNamespaceHandlerResolver 的实例对象了(具体参考前面的文章),所以这里其实就是委托 DefaultNamespaceHandlerResolver.resolve(String namespaceUri) 来获取自定义标签处理器,代码如下:
/** * Locate the {@link NamespaceHandler} for the supplied namespace URI * from the configured mappings. * <p>从配置的映射中找到提供的命名空间 URI 对应的 NamespaceHandler * @param namespaceUri the relevant namespace URI * @return the located {@link NamespaceHandler}, or {@code null} if none found */ @Override @Nullable public NamespaceHandler resolve(String namespaceUri) { // 1、获取所有已经配置的 handler 映射 Map<String, Object> handlerMappings = getHandlerMappings(); // 2、根据命名空间找到对应的处理器 Object handlerOrClassName = handlerMappings.get(namespaceUri); if (handlerOrClassName == null) { return null; } else if (handlerOrClassName instanceof NamespaceHandler) { // 2.1、已经做过解析的情况,直接从缓存读取 return (NamespaceHandler) handlerOrClassName; } else { // 2.2、没有做过解析,则返回的是类路径 String className = (String) handlerOrClassName; try { // 2.2.1、使用反射将类路径转化成类 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"); } // 2.2.2、实例化自定义的 NamespaceHandler ,然后调用其初始化方法 NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); namespaceHandler.init(); // 2.2.3、记录在缓存 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); } } }
根据命名空间提取自定义处理器的过程也比较清晰,主要就是获取映射 handlerMappings 、然后根据命名空间从映射中获取对应的处理器这2个步骤:
- 1、获取所有已经配置的 handler 映射 handlerMappings
- 从上面的用法来看,可以猜测,handlerMappings 其实就是上面 spring.handlers 中指定的 map 映射配置,包括自定义和 Spring 提供的,默认从 /META_INF/ 目录下加载。事实上也是如此,这里借助了工具类 PropertiesLoaderUtils 对 handlerMappingsLocation 路径下的配置文件进行了读取,而 handlerMappingsLocation 则默认初始化为 DEFAULT_HANDLER_MAPPINGS_LOCATION ,即 /META_INF/spring.handlers,代码如下
/** * The location to look for the mapping files. Can be present in multiple JAR files. */ public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers"; /** * Load the specified NamespaceHandler mappings lazily. * <p>延迟加载指定的 NamespaceHandler 映射,META-INF/spring.handlers */ 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; }
- 2、根据命名空间找到对应的处理器,以命名空间为 key ,从 handlerMappings 这个 map 映射中获取对应的处理器 value
- 2.1、如果前面已经做过解析,则处理器已经是实例化过的,可以从缓存获取并直接使用
- 2.2、如果不是,则表示还没做过解析,则返回的是类路径,此时需要处理的就是实例化然后记录到缓存中,即:
- 2.2.1、使用反射将类路径转化成 Class 对象,如果不是 NamespaceHandler 的 class 对象则抛出异常;
- 2.2.2、实例化自定义的 NamespaceHandler ,然后调用其初始化方法 init() ,这里主要就是注册解析器,我们其实可以注册多个解析器,这样就使得自定义的命名空间可以支持多种标签解析。注册后,命名空间处理器就可以根据标签的不同来调用不同的解析器进行解析。
- 2.2.3、将实例化的自定义 NamespaceHandler 对象,记录到缓存 handlerMappings 中,这样下次遇到同样的命名空间就可以直接使用了。
2.3、自定义标签解析
得到了处理器和要解析的元素后,就可以将标签的解析工作委托给自定处理器去处理了,即调用处理器的 parse 方法计行解析,而这里的处理器其实就是我们自定义的 MyNamespaceHandler,但是我们自定义的命名空间处理器并没有实现 parse 方法,而是只完成了初始化工作(注册解析器),这里可以推断是父类的实现通过找到对应的自定义解析器来进行处理,因此我们需要从父类实现中,即 NamespaceHandlerSupport 来寻找相关的解析过程,代码如下:
// org.springframework.beans.factory.xml.NamespaceHandlerSupport /** * Parses the supplied {@link Element} by delegating to the {@link BeanDefinitionParser} that is * registered for that {@link Element}. * <p>委托给因对应的 Element 而注册的 BeanDefinitionParser 来进行 Element 的解析 */ @Override @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { // 寻找解析器并进行解析操作 BeanDefinitionParser parser = findParserForElement(element, parserContext); return (parser != null ? parser.parse(element, parserContext) : null); } /** * Locates the {@link BeanDefinitionParser} from the register implementations using * the local name of the supplied {@link Element}. * <p>寻找解析器 BeanDefinitionParser 实现 */ @Nullable private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { // 获取 Element 的 localName String localName = parserContext.getDelegate().getLocalName(element); // 获取对应的 BeanDefinitionParser 解析器对象 BeanDefinitionParser parser = this.parsers.get(localName); if (parser == null) { parserContext.getReaderContext().fatal( "Cannot locate BeanDefinitionParser for element [" + localName + "]", element); } return parser; }
这过程也很清晰,就是首先寻找元素对应的解析器,然后调用解析器中的 parse 方法进行解析。结合前面的示例,这里其实就是寻找处理器 MyNamespaceHandler 通过 init 方法注册的与对应元素相关的解析器 Parser (init 方法中注册多个解析器,就可以解析多个元素),并调用其 parse 方法进行进一步的解析:
// org.springframework.beans.factory.xml.AbstractBeanDefinitionParser @Override @Nullable 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 = null; if (shouldParseNameAsAliases()) { 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) { String msg = ex.getMessage(); parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element); return null; } } return definition; }
虽说是对自定义配置文件的解析,但是这里可以看到,这个方法实现中大部分代码是用来处理将解析后的 AbstractBeanDefinition 转换成 BeanDefinitionHolder 并注册的,而真正的解析其实是委托给 parseInternal 方法,也正是这里来调用了我们自定义的解析方法,当然,parseInternal 还做了其他很多的操作,它进行了一系列的数据准备,包括对 beanClass、scope、lazyInit 等属性的准备等,代码如下:
// org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser /** * Creates a {@link BeanDefinitionBuilder} instance for the * {@link #getBeanClass bean Class} and passes it to the * {@link #doParse} strategy method. * @param element the element that is to be parsed into a single BeanDefinition * @param parserContext the object encapsulating the current state of the parsing process * @return the BeanDefinition resulting from the parsing of the supplied {@link Element} * @throws IllegalStateException if the bean {@link Class} returned from * {@link #getBeanClass(org.w3c.dom.Element)} is {@code null} * @see #doParse */ @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)); BeanDefinition containingBd = parserContext.getContainingBeanDefinition(); if (containingBd != null) { // Inner bean definition must receive same scope as containing bean. // 做存在父类则使用父类的 scope 属性 builder.setScope(containingBd.getScope()); } if (parserContext.isDefaultLazyInit()) { // Default-lazy-init applies to custom bean definitions as well. // 配置延迟加载 builder.setLazyInit(true); } // 调用子类重写的 doParse 方法进行解析 doParse(element, parserContext, builder); return builder.getBeanDefinition(); } /** * Parse the supplied {@link Element} and populate the supplied * {@link BeanDefinitionBuilder} as required. * <p>The default implementation delegates to the {@code doParse} * version without ParserContext argument. * @param element the XML element being parsed * @param parserContext the object encapsulating the current state of the parsing process * @param builder used to define the {@code BeanDefinition} * @see #doParse(Element, BeanDefinitionBuilder) */ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { doParse(element, builder); }
从自定义标签的一整个解析处理过程中可以发现,虽然我们自定义了解析器,但其中只处理了与自定义业务相关的逻辑,其他的都是 Spring 按照默认标签的处理方式来进行的处理,包括 BeanDefinition 及其相应默认属性的配置。这些都得益于 Spring 提供的大量的默认实现,而且只暴露出一些接口来供用户实现个性化的业务,开箱即用。通过本篇文章的分析,相信大家对自定义标签的使用以及解析过程都有了一个比较全面的了解。
至此我们已经完成了Spring中全部的解析分析,也即是Spring将bean从配置文件到加载至内存中的全过程。接下来就是如何使用这些bean,即bean的加载,后续文章将对 bean 的加载进行分析。
3、参考
- [1]spring 官方文档 5.2.3.RELEASE:9.2. XML Schema Authoring https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#xml-custom
- [2]Spring源码深度解析(第2版),郝佳,P77-P85
- [3]相关注释和案例可参考笔者 github 链接:https://github.com/wpbxin/spring-framework