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;
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: MyBatis 是一个开源的持久层框架,可以方便地将 SQL 语句和 Java 对象进行映射。如果您想要学习 MyBatis 源码,可以按照以下步骤进行: 1. 了解 MyBatis 的架构和设计原理。可以阅读官方文档和相关书籍,例如《MyBatis 技术内幕》。 2. 下载 MyBatis 的源代码,并导入到 IDE 中。MyBatis 使用 Maven 进行构建,您可以使用 IDE 的 Maven 插件来下载依赖项。 3. 查看 MyBatis 的源代码结构。MyBatis 的主要代码在 `mybatis-3` 模块中,括 `src/main/java` 和 `src/main/resources` 目录。其中,`src/main/java` 目录含了 MyBatis 的核心代码,例如 `org.apache.ibatis.session.SqlSession` 类;`src/main/resources` 目录含了 MyBatis 的配置文件和映射文件。 4. 阅读 MyBatis 的源代码。可以从 MyBatis 的入口处 `org.apache.ibatis.session.SqlSessionFactoryBuilder` 开始,深入了解 MyBatis 的初始化流程、SQL 语句的执行流程、映射文件的解析和缓存等。 5. 调试 MyBatis 的源代码。可以使用 IDE 的调试功能,对 MyBatis 进行单步调试,观察代码的执行流程,加深对 MyBatis 的理解。 6. 学习 MyBatis 的单元测试。MyBatis 的单元测试位于 `src/test/java` 目录中,可以通过单元测试来了解 MyBatis 的各个功能点的使用方法和测试用例。 7. 参与 MyBatis 的开发。如果您对 MyBatis 源码有深入的了解,并希望为 MyBatis 做出贡献,可以参与 MyBatis 的开发,贡献代码和文档,提交 issue 和 PR。MyBatis 的开发社区非常活跃,可以在官方网站和 GitHub 上找到相关信息。 希望这些步骤对您学习 MyBatis 源码有所帮助。 ### 回答2: MyBatis是一个开源的Java持久层框架,通过操作对象与数据库关系映射来提供数据持久化的功能。了解MyBatis源码学习和使用该框架的重要一步。 首先,MyBatis源码结构比较清晰,主要分为核心模块和附属模块。核心模块括XML配置解析、SQL语句解析、参数处理、数据库连接管理等功能的实现,是实现MyBatis基本功能的核心部分。附属模块括缓存、事务、插件等额外功能的实现,可以根据需要进行扩展和配置。 学习MyBatis源码可以从以下几个方面入手: 1. 配置文件解析MyBatis通过XML配置文件来进行相关的配置,了解配置文件的解析过程可以帮助理解MyBatis的初始化过程和各项配置的作用。 2. SQL语句解析与执行:MyBatis将SQL语句封装成MappedStatement对象进行管理,了解MappedStatement的生成过程,以及SQL语句的解析、参数处理和执行过程,可以深入了解MyBatis的SQL执行原理。 3. 会话管理和事务处理:MyBatis采用SqlSessionFactory和SqlSession来管理数据库连接和事务,在MyBatis源码中可以学习到如何管理数据库连接池、事务的提交和回滚等核心功能的实现。 4. 缓存机制:MyBatis提供了一级缓存和级缓存的功能,了解缓存的生成和更新过程,以及缓存的命中和失效原理,可以提高数据库查询性能。 总之,通过学习MyBatis源码,可以加深对该框架的理解,掌握其内部实现原理,有助于在使用时更加灵活和高效地进行开发。同时,也为以后解决一些特殊问题提供了更多的思路和方法。 ### 回答3: MyBatis是一个优秀的持久层框架,学习源码有助于理解其底层原理和设计思想。 首先,可以从MyBatis的入口开始学习,即SqlSessionFactoryBuilder类。该类负责解析配置文件、创建Configuration对象,并通过Configuration对象创建SqlSessionFactory实例。 接下来,可以学习Configuration类,该类负责管理整个MyBatis的配置信息。其中括了数据库连接信息、映射文件信息、缓存信息等。在该类内部,会调用XMLMapperBuilder类解析映射文件,在解析映射文件过程中,会创建MappedStatement对象,该对象表示一条SQL语句的映射信息。 学习MappedStatement对象可以了解MyBatis的SQL语句解析过程。该对象含了SQL语句的相关信息,括参数映射关系、返回结果映射关系等。在执行SQL语句时,会使用ParameterHandler类处理参数,通过ResultSetHandler类处理查询结果。 同时,学习到Executor接口及其实现类,可以了解MyBatis的执行过程。Executor负责执行SQL语句,其中括了写操作的update方法和读操作的query方法。在执行过程中,会通过StatementHandler类创建PreparedStatement对象,并通过ResultSetHandler类处理执行结果。 最后,还可以学习MyBatis的事务处理和缓存机制。Transaction接口及其实现类负责事务管理,通过JDBC的事务机制实现了事务的提交和回滚。而Cache接口及其实现类负责缓存查询结果,在查询时会先从缓存中查找结果。 总结来说,通过学习MyBatis源码可以深入理解其底层原理和设计思想。从SqlSessionFactory的创建开始,到Configuration的配置解析、MappedStatement的创建,再到Executor的执行过程和Transaction的事务管理,以及Cache的缓存机制,逐步掌握MyBatis的各个组件和它们之间的交互关系。这对于我们使用MyBatis开发项目,解决问题和优化性能都具有积极的意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值