spring源码解析——IOC之自定义标签解析

2023年 9月 26日 104.3k 0

概述

之前我们已经介绍了spring中默认标签的解析,解析来我们将分析自定义标签的解析,我们先回顾下自定义标签解析所使用的方法,如下图所示:

我们看到自定义标签的解析是通过BeanDefinitionParserDelegate.parseCustomElement(ele)进行的,解析来我们进行详细分析。

自定义标签的使用

扩展 Spring 自定义标签配置一般需要以下几个步骤:

  • 创建一个需要扩展的组件
  • 定义一个 XSD 文件,用于描述组件内容
  • 创建一个实现 AbstractSingleBeanDefinitionParser 接口的类,用来解析 XSD 文件中的定义和组件定义
  • 创建一个 Handler,继承 NamespaceHandlerSupport ,用于将组件注册到 Spring 容器
  • 编写 Spring.handlers 和 Spring.schemas 文件
  • 下面就按照上面的步骤来实现一个自定义标签组件。

    创建组件

    该组件就是一个普通的 JavaBean,没有任何特别之处。这里我创建了两个组件,为什么是两个,后面有用到

    User.java

    package dabin.spring01;
    
    public class User {
    
        private String id;
    
        private String userName;
    
        private String email;public void setId(String id) {
            this.id = id;
        }public void setUserName(String userName) {
            this.userName = userName;
        }public void setEmail(String email) {
            this.email = email;
        }
    
        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("{");
            sb.append(""id":"")
                    .append(id).append('"');
            sb.append(","userName":"")
                    .append(userName).append('"');
            sb.append(","email":"")
                    .append(email).append('"');
            sb.append('}');
            return sb.toString();
        }
    }
    

    Phone.java

    package dabin.spring01;
    
    public class Phone {
    
        private String color;
    
        private int size;
    
        private String remark;
    
    
        public void setColor(String color) {
            this.color = color;
        }
    
        public void setSize(int size) {
            this.size = size;
        }
    
        public void setRemark(String remark) {
            this.remark = remark;
        }
    
        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("{");
            sb.append(""color":"")
                    .append(color).append('"');
            sb.append(","size":")
                    .append(size);
            sb.append(","remark":"")
                    .append(remark).append('"');
            sb.append('}');
            return sb.toString();
        }
    }
    

    定义 XSD 文件

    
    
        
            
                
                
                
            
        
    
        
            
                
                
                
                
            
        
    
    
    

    在上述XSD文件中描述了一个新的targetNamespace,并在这个空间里定义了一个name为user和phone的element 。user里面有三个attribute。主要是为了验证Spring配置文件中的自定义格式。再进一步解释,就是,Spring位置文件中使用的user自定义标签中,属性只能是上面的三种,有其他的属性的话,就会报错。

    Parser 类

    定义一个 Parser 类,该类继承 AbstractSingleBeanDefinitionParser ,并实现 getBeanClass()doParse() 两个方法。主要是用于解析 XSD 文件中的定义和组件定义。这里定义了两个Parser类,一个是解析User类,一个用来解析Phone类。

    UserBeanDefinitionParser.java

    package dabin.spring01;
    
    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
    import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
    import org.springframework.util.StringUtils;
    import org.w3c.dom.Element;
    
    public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
        @Override
        protected Class getBeanClass(Element ele){
            return User.class;
        }
    
        @Override
        protected void doParse(Element element, BeanDefinitionBuilder builder) {
            String id = element.getAttribute("id");
            String userName=element.getAttribute("userName");
            String email=element.getAttribute("email");
            if(StringUtils.hasText(id)){
                builder.addPropertyValue("id",id);
            }
            if(StringUtils.hasText(userName)){
                builder.addPropertyValue("userName", userName);
            }
            if(StringUtils.hasText(email)){
                builder.addPropertyValue("email", email);
            }
    
        }
    }
    

    PhoneBeanDefinitionParser.java

    package dabin.spring01;
    
    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
    import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
    import org.springframework.util.StringUtils;
    import org.w3c.dom.Element;
    
    public class PhoneBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
        @Override
        protected Class getBeanClass(Element ele){
            return Phone.class;
        }
    
        @Override
        protected void doParse(Element element, BeanDefinitionBuilder builder) {
            String color = element.getAttribute("color");
            int size=Integer.parseInt(element.getAttribute("size"));
            String remark=element.getAttribute("remark");
            if(StringUtils.hasText(color)){
                builder.addPropertyValue("color",color);
            }
            if(StringUtils.hasText(String.valueOf(size))){
                builder.addPropertyValue("size", size);
            }
            if(StringUtils.hasText(remark)){
                builder.addPropertyValue("remark", remark);
            }
    
        }
    }
    

    Handler 类

    定义 Handler 类,继承 NamespaceHandlerSupport ,主要目的是将上面定义的解析器Parser类注册到 Spring 容器中。

    package dabin.spring01;
    
    import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
    
    public class MyNamespaceHandler extends NamespaceHandlerSupport {
    
        @Override
        public void init() {
            registerBeanDefinitionParser("user",new UserBeanDefinitionParser());
    
            registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser());
        }
    
    }
    

    我们看看 registerBeanDefinitionParser 方法做了什么

    private final Map parsers = new HashMap();
    
    protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
        this.parsers.put(elementName, parser);
    }
    

    就是将解析器 UserBeanDefinitionParser和 PhoneBeanDefinitionParser 的实例放到全局的Map中,key为user和phone。

    Spring.handlers和Spring.schemas

    编写Spring.handlers和Spring.schemas文件,默认位置放在工程的META-INF文件夹下

    Spring.handlers

    http://www.dabin.com/schema/user=dabin.spring01.MyNamespaceHandler
    

    Spring.schemas

    http://www.dabin.com/schema/user.xsd=org/user.xsd
    

    而 Spring 加载自定义的大致流程是遇到自定义标签然后 就去 Spring.handlers 和 Spring.schemas 中去找对应的 handler 和 XSD ,默认位置是 META-INF 下,进而有找到对应的handler以及解析元素的 Parser ,从而完成了整个自定义元素的解析,也就是说 Spring 将向定义标签解析的工作委托给了 用户去实现。

    创建测试配置文件

    经过上面几个步骤,就可以使用自定义的标签了。在 xml 配置文件中使用如下:

    
    
    
        
    
        
    
        
    
    
    

    xmlns:myTag表示myTag的命名空间是 **www.dabin.com/schema/user ,**在文章开头的判断处 if (delegate.isDefaultNamespace(ele)) 肯定会返回false,将进入到自定义标签的解析

    测试

    import dabin.spring01.MyTestBean;
    import dabin.spring01.Phone;
    import dabin.spring01.User;
    import org.junit.Test;
    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.beans.factory.xml.XmlBeanFactory;
    import org.springframework.core.io.ClassPathResource;
    
    public class AppTest {
        @Test
        public void MyTestBeanTest() {
            BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml"));
            //MyTestBean myTestBean01 = (MyTestBean) bf.getBean("myTestBean");
            User user = (User) bf.getBean("user");
            Phone iphone = (Phone) bf.getBean("iphone");
    
            System.out.println(user);
            System.out.println(iphone);
        }
    
    }
    

    输出结果:

    ("id":"user","userName":"dabin","email":"dabin@163. com”}
    {"color":"black","size":128,"remark":"iphone XR"}
    

    自定义标签的解析

    了解了自定义标签的使用后,接下来我们分析下自定义标签的解析,自定义标签解析用的是方法:parseCustomElement(Element ele, @Nullable BeanDefinition containingBd),进入方法体:

    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;
        }
    
        // 调用自定义的 Handler 处理
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }
    

    相信了解了自定义标签的使用方法后,或多或少会对向定义标签的实现过程有一个自己的想法。其实思路非常的简单,无非是根据对应的bean 获取对应的命名空间 ,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析。

    获取标签的命名空间

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

    String namespaceUri = getNamespaceURI(ele);
    @Nullable
    public String getNamespaceURI(Node node) {
        return node.getNamespaceURI();
    }
    

    这里我们可以通过DEBUG看出myTag:user自定义标签对应的 namespaceUri 是 www.dabin.com/schema/user

    读取自定义标签处理器

    根据 namespaceUri 获取 Handler,这个映射关系我们在 Spring.handlers 中已经定义了,所以只需要找到该类,然后初始化返回,最后调用该 Handler 对象的 parse() 方法处理,该方法我们也提供了实现。所以上面的核心就在于怎么找到该 Handler 类。调用方法为:this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)

    public NamespaceHandler resolve(String namespaceUri) {
        // 获取所有已经配置的 Handler 映射
        Map handlerMappings = getHandlerMappings();
    
        // 根据 namespaceUri 获取 handler的信息:这里一般都是类路径
        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() 方法
                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);
            }
        }
    }
    

    首先调用 getHandlerMappings() 获取所有配置文件中的映射关系 handlerMappings ,就是我们在 Spring.handlers 文件中配置 命名空间与命名空间处理器的映射关系,该关系为 ,然后根据命名空间 namespaceUri 从映射关系中获取相应的信息,如果为空或者已经初始化了就直接返回,否则根据反射对其进行初始化,同时调用其 init()方法,最后将该 Handler 对象缓存。我们再次回忆下示例中对于命名空间处理器的内容:

    public class MyNamespaceHandler extends NamespaceHandlerSupport {
    
        @Override
        public void init() {
            registerBeanDefinitionParser("user",new UserBeanDefinitionParser());
    
            registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser());
        }
    
    }
    

    当得到自定义命名空间处理后会马上执行 namespaceHandler.init() 来进行自定义 BeanDefinitionParser的注册,在这里,你可以注册多个标签解析器,如当前示例中 getBeanClass(Element element) {
    return null;
    }

    protected void doParse(Element element, BeanDefinitionBuilder builder) {
    }

    在该方法中我们主要关注两个方法:getBeanClass()doParse()。对于 getBeanClass() 方法,AbstractSingleBeanDefinitionParser 类并没有提供具体实现,而是直接返回 null,意味着它希望子类能够重写该方法,当然如果没有重写该方法,这会去调用 getBeanClassName() ,判断子类是否已经重写了该方法。对于 doParse() 则是直接空实现。所以对于 parseInternal() 而言它总是期待它的子类能够实现 getBeanClass()doParse(),其中 doParse() 尤为重要,如果你不提供实现,怎么来解析自定义标签呢?最后将自定义的解析器:UserDefinitionParser 再次回观。

    public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
        @Override
        protected Class getBeanClass(Element ele){
            return User.class;
        }
    
        @Override
        protected void doParse(Element element, BeanDefinitionBuilder builder) {
            String id = element.getAttribute("id");
            String userName=element.getAttribute("userName");
            String email=element.getAttribute("email");
            if(StringUtils.hasText(id)){
                builder.addPropertyValue("id",id);
            }
            if(StringUtils.hasText(userName)){
                builder.addPropertyValue("userName", userName);
            }
            if(StringUtils.hasText(email)){
                builder.addPropertyValue("email", email);
            }
    
        }
    }
    

    我们看看 builder.addPropertyValue ("id",id) ,实际上是将自定义标签中的属性解析,存入 BeanDefinitionBuilder 中的 beanDefinition实例中

    private final AbstractBeanDefinition beanDefinition;
    
    public BeanDefinitionBuilder addPropertyValue(String name, @Nullable Object value) {
        this.beanDefinition.getPropertyValues().add(name, value);
        return this;
    }
    

    最后 将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册 registerBeanDefinition(holder, parserContext.getRegistry());这就和默认标签的注册是一样了。

    至此,自定义标签的解析过程已经分析完成了。其实整个过程还是较为简单:首先会加载 handlers 文件,将其中内容进行一个解析,形成 这样的一个映射,然后根据获取的 namespaceUri 就可以得到相应的类路径,对其进行初始化等到相应的 Handler 对象,调用 parse() 方法,在该方法中根据标签的 localName 得到相应的 BeanDefinitionParser 实例对象,调用 parse() ,该方法定义在 AbstractBeanDefinitionParser 抽象类中,核心逻辑封装在其 parseInternal() 中,该方法返回一个 AbstractBeanDefinition 实例对象,其主要是在 AbstractSingleBeanDefinitionParser 中实现,对于自定义的 Parser 类,其需要实现 getBeanClass() 或者 getBeanClassName()doParse()。最后将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论