8Spring 源码学习 ~ 自定义标签的解析

Posted 戴泽supp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8Spring 源码学习 ~ 自定义标签的解析相关的知识,希望对你有一定的参考价值。

自定义标签的解析

前面的章节,我们已经学习过 Spring 默认标签(bean、alias、import 和 beans)的解析,接下来我们来学习自定义标签的解析。

入口:

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 拿到一个元素时,首先是根据命名空间进行解析,如果是默认的命名空间,则使用 parseDefaultElement 方法进行解析,否则使用 parseCustomElement 方法解析。我们先来看下自定义标签的使用。

一、自定义标签的使用

1、创建用来接收配置文件的 POJO

package com.luo.spring.guides.helloworld.customtag;

import lombok.Data;

/**
 * @author : archer
 * @date : Created in 2022/10/25 20:46
 * @description :
 */
@Data
@ToString
public class User 
    
    private String userName;
    private String email;

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

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.lexueba.com/schema/user"
        targetNamespace="http://www.lexueba.com/schema/user"
        xmlns:tns="http://www.lexueba.com/schema/user"
        elementFormDefault="qualified">
    <element name="user">
        <complexType>
            <attribute name="id" type="string"/>
            <attribute name="userName" type="string"/>
            <attribute name="email" type="string"/>
        </complexType>
    </element>

</schema>

XML Schema 语法可以参考:菜鸟教程 ~ XML Schema 教程 或其他资料

3、实现 BeanDefinitionParser 接口

  • 用来解析 XSD 文件中的定义和组件定义
package com.luo.spring.guides.helloworld.customtag;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

/**
 * @author : archer
 * @date : Created in 2022/10/26 16:42
 * @description :
 */
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser 
    public static final String USER_NAME = "userName";
    public static final String EMAIL = "email";

    @Override
    protected Class<?> getBeanClass(Element element) 
        return User.class;
    

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder beam) 
        String userName = element.getAttribute(USER_NAME);
        String email = element.getAttribute(EMAIL);
        if (StringUtils.hasText(userName)) 
            beam.addPropertyValue(USER_NAME, userName);
        
        if (StringUtils.hasText(email)) 
            beam.addPropertyValue(EMAIL, email);
        
    


4、创建 Handler,继承 NamespaceHandlerSupport

  • 目的是将组件注册到 Spring 容器
package com.luo.spring.guides.helloworld.customtag;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

/**
 * @author : archer
 * @date : Created in 2022/10/26 16:55
 * @description :
 */
public class MyNamespaceHandler extends NamespaceHandlerSupport 
    public static final String USER = "user";

    @Override
    public void init() 
        registerBeanDefinitionParser(USER, new UserBeanDefinitionParser());
    


说明:当遇到自定义标签 <user:aaa 这样类似的以 user 开头的元素,就会把这个元素扔给对应的 UserBeanDefinitionParser 去解析。

5、编写 Spring.handlers 和 Spring.schemas 文件

  • 默认位置在工程的 /META-INF/ 文件夹下:可通过 Spring 的扩张或修改源码的方式改变路径

spring.handlers

http\\://www.xiaoluoluo.com/schema/user=com.luo.spring.guides.helloworld.customtag.MyNamespaceHandler

spring.schemas

http\\://www.lexueba.com/schema/user.xsd=META-INF/user.xsd

6、测试

引入自定义 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:myname="http://www.xiaoluoluo.com/schema/user"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.xiaoluoluo.com/schema/user http://www.xiaoluoluo.com/schema/user.xsd">
   <myname:user id="1" userName="aaa" email="nnn"/>

</beans>

Main.java

package com.luo.spring.guides.helloworld.customtag;

import com.luo.spring.guides.helloworld.common.TestBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author : archer
 * @date : Created in 2022/10/26 17:16
 * @description :
 */
public class Main 
    public static void main(String[] args) 
        ApplicationContext bf = new ClassPathXmlApplicationContext("customtag/customtag.xml");
        User user = (User) bf.getBean("userbean");
        System.out.println(user);
    


输出:

7、总结

  • 1、遇到自定义标签就去 Spring.handlers 和 Spring.schemas 中找对应的 handler 和 XSD,默认位置是 /META-INF/ 下
  • 2、找到对应的 handler 和解析元素的 Parser,来完成整个自定义元素的解析。如我们熟知的事务标签 tx(tx:annotation-driven)

二、自定义标签解析

了解了自定义标签使用后,我们来学习下自定义标签的解析过程,入口是函数 BeanDefinitionParserDelegate#parseCustomElement,代码如下:

@Nullable
public BeanDefinition parseCustomElement(Element ele) 
    return parseCustomElement(ele, null);


//containingBd 是父类 bean,对顶层元素的解析应设置为null
@Nullable
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));

我们分步来看下每步是怎么实现的

1、获取标签的命名空间

根据标签锁提供的命名空间,我们可以区分标签是 Spring 中的默认标签还是自定义标签,也可以区分自定义标签中不同标签的处理器。那该如何提取对应元素的命名空间呢?在 org.w3c.dom.Node 中,已经提供了 getNamespaceURI 方法来获取,如下:

@Nullable
public String getNamespaceURI(Node node) 
    return node.getNamespaceURI();

2、提取自定义标签处理器

根据上面的 parseCustomElement 函数,我们知道提取自定义标签处理器的代码如下:

NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

DefaultNamespaceHandlerResolver#resolve

@Override
@Nullable
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);
            // 判断 handlerClass 的类型是否是 NamespaceHandler 的子类,子接口,本身
            if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) 
                throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
                                             "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
            
            //实例化(instantiate)类
            NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
            //调用自定义的初始化方法
            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);
        
    

之前我们在 spring.handlers 中就定义了命名空间与命名空间处理器之间的映射缓存,当获取到命名空间处理器后,就可以进行处理器初始化并解析了。

执行 namespaceHandler.init() 时,会进行自定义 BeanDefinitionParser 的注册,当然你可以注册多个标签解析器,上述示例,只注册了 <myname:user 的标签解析。

注册后,命名空间处理器就可以根据标签的不同来调用不同的解析器。根据逻辑,我们基本可以推断函数 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#loadAllProperties 来进行配置文件的读取。handlerMappingsLocation 在构造函数中已经被初始化为: META-INF/Spring.handlers。

3、标签解析

提取标签处理器后,接下来是解析标签了,即

return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

源码如下:

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) 
    //寻找解析器
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    //parser.parse(element, parserContext) 进行解析
    return (parser != null ? parser.parse(element, parserContext) : null);


@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) 
    //获取元素名称,也就是 <myname:user 中的 user,若在示例中,此时 localName 为 user
    String localName = parserContext.getDelegate().getLocalName(element);
    //根据 user 找到对应的解析器,
    //就是在 registerBeanDefinitionParser(USER, new UserBeanDefinitionParser()); 注册的解析器
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) 
        parserContext.getReaderContext().fatal(
            "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    
    return parser;

而对于 parser.parse(element, parserContext) 方法的处理:

步骤

@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);
            // 注册 BeanDefinition
            registerBeanDefinition(holder, parserContext.getRegistry());
            if (shouldFireEvents()) 
                //通知监听器进行处理
                BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
                postProcessComponentDefinition(componentDefinition);
                parserContext.registerComponent(componentDefinition);
            
        
        catch (以上是关于8Spring 源码学习 ~ 自定义标签的解析的主要内容,如果未能解决你的问题,请参考以下文章

Spring源码学习自定义标签的解析

dubbo源码学习 : spring 自定义标签

Spring源码深度解析学习系列默认标签解析

3Spring 源码学习 ~ 默认标签的解析之 Bean 标签解析

Spring源码解析-自定义标签解析和SPI机制-3

Spring源码解析——自定义标签解析