简述

大家都知道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中常用的表达式:

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

parsing包整体概览

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对象
......省略......
}

默认情况下,对XML文档进行验证时,会根据XML文档开始位置指定的网址加载对应的DTD文件或XSD文件。

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

从类图中可以看出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;
}

Mybatis源码学习之parsing包(解析器)(二)的更多相关文章

  1. 【mybatis源码学习】mybtias基础组件-占位符解析器

    一.占位符解析器源码 1.占位符解析器实现的目标 通过解析字符串中指定前后缀中的字符,并完成相应的功能. 在mybtias中的应用,主要是为了解析Mapper的xml中的sql语句#{}中的内容,识别 ...

  2. mybatis源码学习(一) 原生mybatis源码学习

    最近这一周,主要在学习mybatis相关的源码,所以记录一下吧,算是一点学习心得 个人觉得,mybatis的源码,大致可以分为两部分,一是原生的mybatis,二是和spring整合之后的mybati ...

  3. mybatis源码学习:一级缓存和二级缓存分析

    目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...

  4. mybatis源码学习:插件定义+执行流程责任链

    目录 一.自定义插件流程 二.测试插件 三.源码分析 1.inteceptor在Configuration中的注册 2.基于责任链的设计模式 3.基于动态代理的plugin 4.拦截方法的interc ...

  5. mybatis源码学习:基于动态代理实现查询全过程

    前文传送门: mybatis源码学习:从SqlSessionFactory到代理对象的生成 mybatis源码学习:一级缓存和二级缓存分析 下面这条语句,将会调用代理对象的方法,并执行查询过程,我们一 ...

  6. Mybatis源码学习第六天(核心流程分析)之Executor分析

    今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...

  7. Spring源码情操陶冶#task:scheduled-tasks解析器

    承接前文Spring源码情操陶冶#task:executor解析器,在前文基础上解析我们常用的spring中的定时任务的节点配置.备注:此文建立在spring的4.2.3.RELEASE版本 附例 S ...

  8. springMVC源码分析--HandlerMethodReturnValueHandlerComposite返回值解析器集合(二)

    在上一篇博客springMVC源码分析--HandlerMethodReturnValueHandler返回值解析器(一)我们介绍了返回值解析器HandlerMethodReturnValueHand ...

  9. Mybatis源码学习之DataSource(七)_1

    简述 在数据持久层中,数据源是一个非常重要的组件,其性能直接关系到整个数据持久层的性能.在实践中比较常见的第三方数据源组件有Apache Common DBCP.C3P0.Proxool等,MyBat ...

随机推荐

  1. List和Dictionary互转

    // 声明Dictionary并初始化 Dictionary<string, string> dic = new Dictionary<string, string>() { ...

  2. Phoenix的jdbc封装

    一.Phoenix版本 <dependency> <groupId>org.apache.phoenix</groupId> <artifactId>p ...

  3. iTop4412开发板+虚拟机+tftp服务

    感觉好坑啊 利用路由器+2根网线+tftp服务 首先是开发板,主机,虚拟机相互之间能ping通(坑), 关闭主机防火墙,防止被强 关闭虚拟机防火墙 虚拟机装上tftpd服务端(通过网上教程嘛) 是不是 ...

  4. Marketing Cloud里取得系统contact数目的API

    Marketing Cloud里的Contact标准tile(下图红色tile)上是没有当前系统contact数字显示的,请对比profile tile(下图黑色tile). 客户有需求希望在Laun ...

  5. 运行时异常与受检异常有何异同、error和exception有什么区别

    1.运行时异常与受检异常有何异同? 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生.受检异常跟程序 ...

  6. C#面向对象 (访问修饰符、封装、继承、多态)

    先看一下创建一个新项目时的基本格式 using System; using System.Collections.Generic; using System.Linq; //引用的命名空间 using ...

  7. 《设计模式之美》 <02>评判代码质量好坏的维度

    如何评价代码质量的高低? 实际上,咱们平时嘴中常说的“好”和“烂”,是对代码质量的一种描述.“好”笼统地表示代码质量高,“烂”笼统地表示代码质量低.对于代码质量的描述,除了“好”“烂”这样比较简单粗暴 ...

  8. redis写入性能测试

    import timeit import redis def clock(func): def clocked(*args, **kwargs): t0 = timeit.default_timer( ...

  9. vim编辑命令

    vi命令 命令模式: yy:复制 光标所在的这一行 4yy:复制 光标所在行开始向下的4行 p: 粘贴 dd:剪切 光标所在的这一行 2dd:剪切 光标所在行 向下 2行 D:从当前的光标开始剪切,一 ...

  10. java_变量和常量

    一.变量(可以改变的量) 1.命名规则: a.遵循标识符命名规则: 1.关键字是不能用作标识符的 2.区分大小写 3.可以包含数字.字母.下划线.美元符号$,但是不能以数字作为开头 b.尽量使用有意义 ...