1.0 概述
MyBatis 的解析器模块,源码对应 parsing
包。如下图:
- 1.对Java XPath 进行封装,为MyBatis初始化时解析mybatis-config.xml配置文件以及映射配置文件提供支持。
- 2.为处理动态 SQL 语句中的占位符提供支持
2.0 XPathParser
org.apache.ibatis.parsing.XPathParser
基于Java XPath解析器,用于解析mybatis-config.xml和XXMapper.xml等XML配置文件。
属性如下:
// XPathParser.java
/**
* XML Document 对象
*/
private final Document document;
/**
* 是否校验
*/
private boolean validation;
/**
* XML 实体解析器
*/
private EntityResolver entityResolver;
/**
* 变量 Properties 对象
*/
private Properties variables;
/**
* Java XPath 对象
*/
private XPath xpath;
复制代码
document
属性,XML解析后生成的org.w3c.dom.Document
对象entityResolver
属性,org.xml.sax.EntityResolver
对象,XML实体解析器。默认情况下,对XML进行校验时,会基于XML文档开始位置指定的DTD文件或XSD文件。例如说: 解析mybatis-config.xml配置文件时,会加载http://mybatis.org/dtd/mybatis-3-config.dtd
这个DTD文件。但是,如果每个应用启动都从网络加载该DTD文件,势必在弱网络下体验非常差,甚至应用部署在无网络的环境下,还会导致下载不下来,那么就会出现XML校验失败的情况。所以在实际场景下,MyBatis自定义了EntityResolver的实现使用本地DTD文件,从而避免下载网络DTD文件的效果。xpath
属性,javax.xml.xpath.XPath
对象,用于查询XML中的节点和元素。对 XPath 的使用不了解的同学,可以去《XPath教程》和《Java XPath解析器》进行简单学习variables
属性,变量Properties对象,用来替换需要动态配置的属性值,详见《Mybatis技术内幕:初始化之properties标签解析》
2.1 构造方法
XPathParser
的构造方法重载了16个之多,基本都非常相似,我们挑选其中一个。代码如下:
// XPathParser.java
/**
* 构造 XPathParser 对象
*
* @param xml XML 文件地址
* @param validation 是否校验 XML
* @param variables 变量 Properties 对象
* @param entityResolver XML 实体解析器
*/
public XPathParser(String xml, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(new StringReader(xml)));
}
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
// 创建 XPathFactory 对象
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
/**
* 创建 Document 对象
*
* @param inputSource XML 的 InputSource 对象
* @return Document 对象
*/
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
// 1> 创建 DocumentBuilderFactory 对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation); // 设置是否验证 XML
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
// 2> 创建 DocumentBuilder 对象
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver); // 设置实体解析器
builder.setErrorHandler(new ErrorHandler() { // 实现都空的
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
// 3> 解析 XML 文件
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
复制代码
代码比较简单,主要是完成XPathParser
类相关成员变量的初始化赋值操作
2.2 eval 方法族
XPathParser
提供了一系列的#eval*
方法,用于获得Boolean、Short、Integer、Long、Float、Double、String、Node类型的元素或节点的值。 虽然方法很多,但是都是基于 #evaluate(String expression, Object root, QName returnType)
方法。代码如下:
// XPathParser.java
/**
* 获得指定元素或节点的值
*
* @param expression 表达式
* @param root 指定节点
* @param returnType 返回类型
* @return 值
*/
private Object evaluate(String expression, Object root, QName returnType) {
try {
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
复制代码
2.3 节点属性值的动态替换
主要依赖evalString(Object root, String expression)
方法,真正的替换动作由PropertyParser
类完成。PropertyParser
下面会讲到
// XPathParser.java
public String evalString(String expression) {
return evalString(document, expression);
}
public String evalString(Object root, String expression) {
String result = (String) evaluate(expression, root, XPathConstants.STRING);
result = PropertyParser.parse(result, variables);
return result;
}
public Integer evalInteger(Object root, String expression) {
return Integer.valueOf(evalString(root, expression));
}
复制代码
evalNode(String expression)
方法会在后面的配置文件初始化中大量用到,返回org.apache.ibatis.parsing.XNode
对象,主要为了动态值的替换
//XPathParser
public XNode evalNode(String expression) {
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) {
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
return new XNode(this, node, variables);
}
//XNode
public String evalString(String expression) {
return xpathParser.evalString(node, expression);
}
复制代码
2.4 org.apache.ibatis.parsing.XNode
在面对一个Node时,假设我想要把Node的属性集合都以键、值对的形式,放到Properties对象里,同时把Node的body体也通过XPathParser解析出来,并保存起来(一般是Sql语句),方便程序使用,代码可能会是这样的。
private Node node;
private String body;
private Properties attributes;
private XPathParser xpathParser;
复制代码
Mybatis就把上面几个必要属性封装到一个类中,取名叫XNode。
3.0 XMLMapperEntityResolver
org.apache.ibatis.builder.xml.XMLMapperEntityResolver
实现 EntityResolver 接口,用于加载本地的mybatis-3-config.dtd和mybatis-3-mapper.dtd这两个 DTD 文件。代码比较简单,代码如下:
// XMLMapperEntityResolver.java
public class XMLMapperEntityResolver implements EntityResolver {
private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
/**
* Converts a public DTD into a local one
*
* @param publicId The public id that is what comes after "PUBLIC"
* @param systemId The system id that is what comes after the public id.
* @return The InputSource for the DTD
*
* @throws org.xml.sax.SAXException If anything goes wrong
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
// 本地 mybatis-config.dtd 文件
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
// 本地 mybatis-mapper.dtd 文件
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}
private InputSource getInputSource(String path, String publicId, String systemId) {
InputSource source = null;
if (path != null) {
try {
// 创建 InputSource 对象
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
// 设置 publicId、systemId 属性
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}
}
复制代码
4.0 PropertyParser
PropertyParser
前面的XPathParser
小节中已经出现了,主要用于动态属性的解析,是一个提供静态方法的工具类。部分代码如下:
// PropertyParser.java
public class PropertyParser {
// private构造器 禁止构造 PropertyParser 对象
private PropertyParser() {
// Prevent Instantiation
}
public static String parse(String string, Properties variables) {
// 创建 VariableTokenHandler 对象
VariableTokenHandler handler = new VariableTokenHandler(variables);
// 创建 GenericTokenParser 对象
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
// 执行解析
return parser.parse(string);
}
}
复制代码
主要代码不多,解析过程主要依赖VariableTokenHandler
和GenericTokenParser
对象
5.0 TokenHandler
org.apache.ibatis.parsing.TokenHandler
Token处理器接口。代码如下:
// TokenHandler.java
public interface TokenHandler {
/**
* 处理 Token
* @param content Token 字符串
* @return 处理后的结果
*/
String handleToken(String content);
}
复制代码
TokenHandler 有四个子类实现,如下图所示:
本文暂时只解读VariableTokenHandler
类
##5.1 VariableTokenHandler VariableTokenHandler
是PropertyParser的内部静态类,变量 Token 处理器。代码如下:
// PropertyParser.java
private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
/**
* @since 3.4.2
*/
public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
/**
* @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 static class VariableTokenHandler implements TokenHandler {
private final Properties variables;
//是否开启默认值功能。默认为 {@link #ENABLE_DEFAULT_VALUE false}
private final boolean enableDefaultValue;
//默认值的分隔符。默认为 {@link #KEY_DEFAULT_VALUE_SEPARATOR} ,即 ":"
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) {
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) {
return variables.getProperty(key, defaultValue);
}
}
// 未开启默认值功能,直接替换
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
// 无 variables ,直接返回
return "${" + content + "}";
}
}
复制代码
代码比较简单,在3.4.2版本以后开始支持默认值功能(默认和spring一致),可以通过mybatis-config.xml配置修改
enableDefaultValue
属性,是否开启默认值功能。默认为 ENABLE_DEFAULT_VALUE(false) ,即不开启defaultValueSeparator
属性,默认值的分隔符。默认为 KEY_DEFAULT_VALUE_SEPARATOR ,即 ":"
<properties resource="org/mybatis/example/config.properties">
<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
<property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/>
</properties>
复制代码
6.0 GenericTokenParser
GenericTokenParser
通用的Token解析器,代码如下:
// GenericTokenParser.java
public class GenericTokenParser {
/**
* 开始的 Token 字符串
*/
private final String openToken;
/**
* 结束的 Token 字符串
*/
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 "";
}
// 寻找开始的 openToken 的位置
int start = text.indexOf(openToken, 0);
if (start == -1) { // 找不到,直接返回
return text;
}
char[] src = text.toCharArray();
int offset = 0; // 起始查找位置
// 结果
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null; // 匹配到 openToken 和 closeToken 之间的表达式
// 循环匹配
while (start > -1) {
// 转义字符
if (start > 0 && src[start - 1] == '\\') {
// 因为 openToken 前面一个位置是 \ 转义字符,所以忽略 \
// 添加 [offset, start - offset - 1] 和 openToken 的内容,添加到 builder 中
builder.append(src, offset, start - offset - 1).append(openToken);
// 修改 offset
offset = start + openToken.length();
// 非转义字符
} else {
// found open token. let's search close token.
// 创建/重置 expression 对象
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
// 添加 offset 和 openToken 之间的内容,添加到 builder 中
builder.append(src, offset, start - offset);
// 修改 offset
offset = start + openToken.length();
// 寻找结束的 closeToken 的位置
int end = text.indexOf(closeToken, offset);
while (end > -1) {
// 转义
if (end > offset && src[end - 1] == '\\') {
// 因为 endToken 前面一个位置是 \ 转义字符,所以忽略 \
// 添加 [offset, end - offset - 1] 和 endToken 的内容,添加到 builder 中
expression.append(src, offset, end - offset - 1).append(closeToken);
// 修改 offset
offset = end + closeToken.length();
// 继续,寻找结束的 closeToken 的位置
end = text.indexOf(closeToken, offset);
// 非转义
} else {
// 添加 [offset, end - offset] 的内容,添加到 builder 中
expression.append(src, offset, end - offset);
break;
}
}
// 拼接内容
if (end == -1) {
// closeToken 未找到,直接拼接
builder.append(src, start, src.length - start);
// 修改 offset
offset = src.length;
} else {
// closeToken 找到,将 expression 提交给 handler 处理 ,并将处理结果添加到 builder 中
builder.append(handler.handleToken(expression.toString()));
// 修改 offset
offset = end + closeToken.length();
}
}
// 继续,寻找开始的 openToken 的位置
start = text.indexOf(openToken, offset);
}
// 拼接剩余的部分
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
复制代码
代码比较冗长,但是就一个#parse(String text)
方法,循环(因为可能不只一个 ),解析以 openToken
开始,以closeToken
结束的Token,并提交给指定handler 进行处理,大家可以耐心看下这段逻辑,通过源码包中相关的单元测试类
去打断点一行一行跟进
参考和推荐如下文章
徐郡明 《MyBatis 技术内幕》 的 「2.1 解析器模块」 小节
祖大俊 《Mybatis3.3.x技术内幕(七):Mybatis初始化之六个工具》
失控的阿甘,乐于分享,记录点滴