zoukankan      html  css  js  c++  java
  • Mybatis源码学习之parsing包(解析器)(二)

    简述

    大家都知道mybatis中,无论是配置文件mybatis-config.xml,还是SQL语句,都是写在XML文件中的,那么mybatis是如何解析这些XML文件呢?这就是本文将要学习的就是,mybatis解析器XPathParser。

    MyBatis在初始化过程中处理mybatis-config.xml配置文件以及映射文件时,使用的是DOM解析方式,并结合使用XPath解析XML配置文件。DOM会将整个XML文档加载到内存中并形成树状数据结构,而XPath是一种为查询XML文档而设计的语言,它可以与DOM解析方式配合使用,实现对XML文档的解析。

    XPath使用路径表达式来选取XML文档中指定的节点或者节点集合,与常见的URL路径有些类似。

    XPath中常用的表达式:
    image

    XPath 语法概念:http://www.runoob.com/xpath/xpath-tutorial.html

    parsing包整体概览

    image

    GenericTokenParser——占位符解析器

    该类为mybatis中通用占位符解析器,解析xml文件中占位符 “${}”并返回对应的值,为了学习的便利性,我加了日志对入参和结果进行打印。

    GenericTokenParser.parse()方法的逻辑并不复杂,它会顺序查找openToken和closeToken,解析得到占位符的字面值,并将其交给TokenHandler处理,然后将解析结果重新拼装成字符串并返回。

    具体看源码:

    /**
     * mybatis通用标记解析器,对xml中属性中的占位符进行解析
     *
     * @author Clinton Begin
     */
    public class GenericTokenParser {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
        /**
         * 开始标记符
         */
        private final String openToken;
        /**
         * 结束标记符
         */
        private final String closeToken;
        /**
         * 标记处理接口,具体的处理操作取决于它的实现方法
         */
        private final TokenHandler handler;
    
        /**
         * 构造函数
         */
        public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
            this.openToken = openToken;
            this.closeToken = closeToken;
            this.handler = handler;
        }
    
        /**
         * 文本解析方法
         */
        public String parse(String text) {
            //文本空值判断
            if (text == null || text.isEmpty()) {
                return "";
            }
            // 获取开始标记符在文本中的位置
            int start = text.indexOf(openToken, 0);
            //位置索引值为-1,说明不存在该开始标记符
            if (start == -1) {
                return text;
            }
            //将文本转换成字符数组
            char[] src = text.toCharArray();
            //偏移量
            int offset = 0;
            //解析后的字符串
            final StringBuilder builder = new StringBuilder();
            StringBuilder expression = null;
            while (start > -1) {
                //判断开始标记符前边是否有转移字符,如果存在转义字符则移除转义字符
                if (start > 0 && src[start - 1] == '\') {
                    //移除转义字符
                    builder.append(src, offset, start - offset - 1).append(openToken);
                    //重新计算偏移量
                    offset = start + openToken.length();
                } else {
                    //开始查找结束标记符
                    if (expression == null) {
                        expression = new StringBuilder();
                    } else {
                        expression.setLength(0);
                    }
                    builder.append(src, offset, start - offset);
                    offset = start + openToken.length();
                    //结束标记符的索引值
                    int end = text.indexOf(closeToken, offset);
                    while (end > -1) {
                        //同样判断标识符前是否有转义字符,有就移除
                        if (end > offset && src[end - 1] == '\') {
                            // this close token is escaped. remove the backslash and continue.
                            expression.append(src, offset, end - offset - 1).append(closeToken);
                            //重新计算偏移量
                            offset = end + closeToken.length();
                            //重新计算结束标识符的索引值
                            end = text.indexOf(closeToken, offset);
                        } else {
                            expression.append(src, offset, end - offset);
                            offset = end + closeToken.length();
                            break;
                        }
                    }
                    //没有找到结束标记符
                    if (end == -1) {
                        // close token was not found.
                        builder.append(src, start, src.length - start);
                        offset = src.length;
                    } else {
                        //找到了一组标记符,对该标记符进行值替换
                        builder.append(handler.handleToken(expression.toString()));
                        offset = end + closeToken.length();
                    }
                }
                //接着查找下一组标记符
                start = text.indexOf(openToken, offset);
            }
            if (offset < src.length) {
                builder.append(src, offset, src.length - offset);
            }
            logger.debug("[GenericTokenParser]-[parse]-待解析文本:{},解析结果:{}",text,builder.toString());
            return builder.toString();
        }
    }

    为了更加深入的了解其解析过程,我们使用其提供的单元测试进行了跟踪调试,这里只复制了部分代码,具体可看其源码:

      @Test
      public void shouldDemonstrateGenericTokenReplacement() {
        GenericTokenParser parser = new GenericTokenParser("${", "}", new VariableTokenHandler(new HashMap<String, String>() {
          {
            put("first_name", "James");
            put("initial", "T");
            put("last_name", "Kirk");
            put("var{with}brace", "Hiya");
            put("", "");
          }
        }));
    
        assertEquals("James T Kirk reporting.", parser.parse("${first_name} ${initial} ${last_name} reporting."));
        }
    

    输出结果:

    DEBUG [main] - [GenericTokenParser]-[parse]-待解析文本:${first_name} ${initial} ${last_name} reporting.,解析结果:James T Kirk reporting.

    PropertyParser-默认值解析器

    通过对PropertyParser.parse()方法的学习,我们知道PropertyParser是使用VariableToken-Handler与GenericTokenParser配合完成占位符解析的。VariableTokenHandler是PropertyParser中的一个私有静态内部类。

    VariableTokenHandler实现了TokenHandler接口中的handleToken()方法,该实现首先会按照defaultValueSeparator字段指定的分隔符对整个占位符切分,得到占位符的名称和默认值,然后按照切分得到的占位符名称查找对应的值,如果在<properties>节点下未定义相应的键值对,则将切分得到的默认值作为解析结果返回。

    GenericTokenParser不仅仅用于这里的默认值解析,还会用于后面对动态SQL语句的解析。很明显,GenericTokenParser只是查找到指定的占位符,而具体的解析行为会根据其持有的TokenHandler实现的不同而有所不同,

    /**
     * 属性解析器,主要用于对默认值的解析
     *
     * @author Clinton Begin
     * @author Kazuki Shimizu
     */
    public class PropertyParser {
        private static final Logger logger= LoggerFactory.getLogger(PropertyParser.class);
    
        private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
        /**
         * 特殊属性键,指示是否在占位符上启用默认值。
         * <p>
         * 默认值是false,是禁用的占位符上使用默认值,当启用以后(true)可以在占位符上使用默认值。
         * 例如:${db.username:postgres},表示数据库的用户名默认是postgres
         * <p>
         * </p>
         *
         * @since 3.4.2
         */
        public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
    
        /**
         * 为占位符上的键和默认值指定分隔符的特殊属性键。
         * <p>
         * 默认分隔符是“:”
         * </p>
         *
         * @since 3.4.2
         */
        public static final String KEY_DEFAULT_VALUE_SEPARATOR = KEY_PREFIX + "default-value-separator";
    
        private static final String ENABLE_DEFAULT_VALUE = "false";
        private static final String DEFAULT_VALUE_SEPARATOR = ":";
    
    
        private PropertyParser() {
            // 私有构造函数,防止实例化
        }
    
        public static String parse(String string, Properties variables) {
            //解析默认值
            VariableTokenHandler handler = new VariableTokenHandler(variables);
           //解析占位符
            GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
            return parser.parse(string);
        }
    
        /**
         * 内部私有静态类
         */
        private static class VariableTokenHandler implements TokenHandler {
            /**
             * <properties>节点下定义的键值对,用于替换占位符
             */
            private final Properties variables;
            /**
             * 是否启用默认值
             */
            private final boolean enableDefaultValue;
            /**
             * 默认分隔符
             */
            private final String defaultValueSeparator;
    
            private VariableTokenHandler(Properties variables) {
                this.variables = variables;
                this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
                this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
            }
    
            private String getPropertyValue(String key, String defaultValue) {
                return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
            }
    
            @Override
            public String handleToken(String content) {
                //解析结果(为方便调试学习,自己加的)
                String parseResult="${" + content + "}";
                //变量值不为空
                if (variables != null) {
                    String key = content;
                    if (enableDefaultValue) {
                        //分隔符索引值
                        final int separatorIndex = content.indexOf(defaultValueSeparator);
                        String defaultValue = null;
                        if (separatorIndex >= 0) {
                            key = content.substring(0, separatorIndex);
                            //获取默认值
                            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
                        }
                        //默认值不为空
                        if (defaultValue != null) {
                            //优先使用变量集合中的值,其次使用默认值
                            parseResult= variables.getProperty(key, defaultValue);
                        }
                    }
                    if (variables.containsKey(key)) {
                        parseResult= variables.getProperty(key);
                    }
                }
                logger.debug("【PropertyParser】-【handleToken】-待解析内容{},解析结果{}",content,parseResult);
                return parseResult;
            }
        }
    
    }

    测试事例:

     @Test
      public void replaceToVariableValue() {
        Properties props = new Properties();
        props.setProperty(PropertyParser.KEY_ENABLE_DEFAULT_VALUE, "true");
        props.setProperty("key", "value");
        props.setProperty("tableName", "members");
        props.setProperty("orderColumn", "member_id");
        props.setProperty("a:b", "c");
        Assertions.assertThat(PropertyParser.parse("${key}", props)).isEqualTo("value");
        Assertions.assertThat(PropertyParser.parse("${key:aaaa}", props)).isEqualTo("value");
        Assertions.assertThat(PropertyParser.parse("SELECT * FROM ${tableName:users} ORDER BY ${orderColumn:id}", props)).isEqualTo("SELECT * FROM members ORDER BY member_id");
    
        //关闭默认值解析
        props.setProperty(PropertyParser.KEY_ENABLE_DEFAULT_VALUE, "false");
        Assertions.assertThat(PropertyParser.parse("${a:b}", props)).isEqualTo("c");
    
        props.remove(PropertyParser.KEY_ENABLE_DEFAULT_VALUE);
        Assertions.assertThat(PropertyParser.parse("${a:b}", props)).isEqualTo("c");
    
      }

    输出结果:

    DEBUG [main] - 【PropertyParser】-【handleToken】-待解析内容key,解析结果value
    DEBUG [main] - [GenericTokenParser]-[parse]-待解析文本:${key},解析结果:value
    DEBUG [main] - 【PropertyParser】-【handleToken】-待解析内容key:aaaa,解析结果value
    DEBUG [main] - [GenericTokenParser]-[parse]-待解析文本:${key:aaaa},解析结果:value
    DEBUG [main] - 【PropertyParser】-【handleToken】-待解析内容tableName:users,解析结果members
    DEBUG [main] - 【PropertyParser】-【handleToken】-待解析内容orderColumn:id,解析结果member_id
    DEBUG [main] - [GenericTokenParser]-[parse]-待解析文本:SELECT * FROM ${tableName:users} ORDER BY ${orderColumn:id},解析结果:SELECT * FROM members ORDER BY member_id
    DEBUG [main] - 【PropertyParser】-【handleToken】-待解析内容a:b,解析结果c
    DEBUG [main] - [GenericTokenParser]-[parse]-待解析文本:${a:b},解析结果:c
    DEBUG [main] - 【PropertyParser】-【handleToken】-待解析内容a:b,解析结果c
    DEBUG [main] - [GenericTokenParser]-[parse]-待解析文本:${a:b},解析结果:c
    

    XPathParser

    MyBatis提供的XPathParser类封装了XPath、Document和EntityResolver对象

    public class XPathParser {
    
      private final Document document;//Document 对象
      private boolean validation;//是否开启验证
      private EntityResolver entityResolver;//用于加载本地的DTD文
      private Properties variables;//mybatis-config中定义的propteries集合
      private XPath xpath;//XPath对象
        ......省略......
    }

    image

    默认情况下,对XML文档进行验证时,会根据XML文档开始位置指定的网址加载对应的DTD文件或XSD文件。
    如果解析mybatis-config.xml配置文件,默认联网加载http://mybatis.org/dtd/mybatis-3-config.dtd这个DTD文档,当网络比较慢时会导致验证过程缓慢。在实践中往往会提前设置EntityResolver接口对象加载本地的DTD文件,从而避免联网加载DTD文件。XMLMapperEntityResolver是MyBatis提供的EntityResolver接口的实现类,

    image

    从类图中可以看出EntityResolver接口的核心方法是 resolveEntity,接下来我们看一下XMLMapperEntityResolver的具体实现

    XPathParser.evalNode()方法返回值类型是XNode,它对org.w3c.dom.Node对象做了封装和解析,其各个字段的含义如下:

    private Node node; //org.w3c.dom.Node对象
    private String name; //Node节点名称 
    private String body; //节点的内容 
    private Properties attributes;//节点属性集合 
    private Properties variables;//mybatis-config.xml配置文件中<properties>节点下定义的键值对

    XNode的构造函数中会调用其parseAttributes()方法和parseBody()方法解析org.w3c.dom.Node对象中的信息,初始化attributes集合和body字段

    private Properties parseAttributes(Node n) {
            Properties attributes = new Properties();
            //获取节点属性集合
            NamedNodeMap attributeNodes = n.getAttributes();
            if (attributeNodes != null) {
                for (int i = 0; i < attributeNodes.getLength(); i++) {
                    Node attribute = attributeNodes.item(i);
                    //PropertyParser处理每个属性中的占位符
                    String value = PropertyParser.parse(attribute.getNodeValue(), variables);
                    attributes.put(attribute.getNodeName(), value);
                }
            }
            return attributes;
        }
    
        private String parseBody(Node node) {
            String data = getBodyData(node);
            //当前节点不是文本节点
            if (data == null) {
                //获取子节点
                NodeList children = node.getChildNodes();
                for (int i = 0; i < children.getLength(); i++) {
                    Node child = children.item(i);
                    data = getBodyData(child);
                    if (data != null) {
                        break;
                    }
                }
            }
            return data;
        }
    
        private String getBodyData(Node child) {
            //只处理文本内容
            if (child.getNodeType() == Node.CDATA_SECTION_NODE
                    || child.getNodeType() == Node.TEXT_NODE) {
                String data = ((CharacterData) child).getData();
                //使用PropertyParser处理文本节点中的占位符
                data = PropertyParser.parse(data, variables);
                return data;
            }
            return null;
        }
  • 相关阅读:
    Spring IoC
    常见切入点表达式的例子(aop execution 表达式 )
    数据结构与算法(2)栈、中缀表达式、递归
    数据结构与算法(1)稀疏数组、队列、链表
    airflow实践
    head first 设计模式笔记13-与设计模式相处,剩下的模式,模式的分类
    head first 设计模式笔记12-复合模式
    head first 设计模式笔记11-代理模式
    head first 设计模式笔记10-状态模式
    WebDriver自动化测试常用处理方法
  • 原文地址:https://www.cnblogs.com/liukaifeng/p/10052624.html
Copyright © 2011-2022 走看看