code-learning/mybatis/04-mybatis-解析器模块.md

743 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 精尽 MyBatis 源码分析 —— 解析器模块
# 1. 概述
本文,我们来分享 MyBatis 的解析器模块,对应 `parsing` 包。如下图所示:[![`parsing` 包](04-mybatis-解析器模块.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_01_07/01.png)`parsing`
在 [《精尽 MyBatis 源码解析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,简单介绍了这个模块如下:
> 解析器模块,主要提供了两个功能:
>
> - 一个功能,是对 [XPath](http://www.w3school.com.cn/xpath/index.asp) 进行封装,为 MyBatis 初始化时解析 `mybatis-config.xml` 配置文件以及映射配置文件提供支持。
> - 另一个功能,是为处理动态 SQL 语句中的占位符提供支持。
下面,我们就来看看具体的源码。因为 `parsing` 是基础支持层,**所以建议胖友在我们讲解到的类和方法中,打折断点一起来了解**。
# 2. XPathParser
`org.apache.ibatis.parsing.XPathParser` ,基于 Java **XPath** 解析器,用于解析 MyBatis `mybatis-config.xml``**Mapper.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
```
对象。
- `validation` 属性,是否校验 XML 。一般情况下,值为 `true` 。
- ```
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 文件的效果。详细解析,见
「3. XMLMapperEntityResolver」
- 另外Spring 也自定义了 EntityResolver 的实现,感兴趣的胖友,可以看看 [《【死磕 Spring】—— IoC 之获取验证模型》](http://svip.iocoder.cn/Spring/IoC-Validation-Mode-For-Resource) 。
- `xpath` 属性,`javax.xml.xpath.XPath` 对象,用于查询 XML 中的节点和元素。如果对 XPath 的使用不了解的胖友,请先跳转 [《Java XPath 解析器 - 解析 XML 文档》](https://www.yiibai.com/java_xml/java_xpath_parse_document.html) 中,进行简单学习,灰常简单。
- `variables` 属性,变量 Properties 对象,用来替换需要动态配置的属性值。例如:
```
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
```
- `variables` 的来源,即可以在常用的 Java Properties 文件中配置,也可以使用 MyBatis `<property />` 标签中配置。例如:
```
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
```
- 这里配置的 `username` 和 `password` 属性,就可以替换上面的 `${username}` 和 `${password}` 这两个动态属性。
- 具体如何实现的,我们来看下面的 `PropertyParser#parse(String string, Properties variables)` 方法。
## 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)));
}
```
- 调用 `#commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver)` 方法,公用的构造方法逻辑。代码如下:
```
// XPathParser.java
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();
}
```
- 调用 `#createDocument(InputSource inputSource)` 方法,将 XML 文件解析成 Document 对象。代码如下:
```
// XPathParser.java
/**
* 创建 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);
}
}
```
- 就是简单的 Java XML API 的使用,不了解的胖友,可以 Google 学习下。
## 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);
}
}
```
- 调用 `xpath` 的 `evaluate(String expression, Object root, QName returnType)` 方法,获得指定元素或节点的值。
### 2.2.1 eval 元素
eval 元素的方法,用于获得 Boolean、Short、Integer、Long、Float、Double、String 类型的**元素**的值。我们以 `#evalString(Object root, String expression)` 方法为例子,代码如下:
```
// XPathParser.java
public String evalString(Object root, String expression) {
// <1> 获得值
String result = (String) evaluate(expression, root, XPathConstants.STRING);
// <2> 基于 variables 替换动态值,如果 result 为动态值
result = PropertyParser.parse(result, variables);
return result;
}
```
- `<1>` 处,调用 `#evaluate(String expression, Object root, QName returnType)` 方法,获得值。其中,`returnType` 方法传入的是 `XPathConstants.STRING` ,表示返回的值是 String 类型。
- `<2>` 处,调用 `PropertyParser#parse(String string, Properties variables)` 方法,基于 `variables` 替换**动态值**,如果 `result` 为**动态值**。这就是 MyBatis 如何替换掉 XML 中的动态值实现的方式。关于 PropertyParser ,我们会在 [「5. PropertyParser」](https://svip.iocoder.cn/MyBatis/parsing-package/#) 详细解析。
### 2.2.2 eval 节点
eval 元素的方法,用于获得 Node 类型的**节点**的值。代码如下:
```
// XPathParser.java
public List<XNode> evalNodes(String expression) { // Node 数组
return evalNodes(document, expression);
}
public List<XNode> evalNodes(Object root, String expression) { // Node 数组
// <1> 获得 Node 数组
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
// <2> 封装成 XNode 数组
List<XNode> xnodes = new ArrayList<>();
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
public XNode evalNode(String expression) { // Node 对象
return evalNode(document, expression);
}
public XNode evalNode(Object root, String expression) { // Node 对象
// <1> 获得 Node 对象
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
// <2> 封装成 XNode 对象
return new XNode(this, node, variables);
}
```
- `<1>` 处,返回结果有 Node **对象**和**数组**两种情况,根据方法参数 `expression` 需要获取的节点不同。
- `<2>` 处, 最终结果会将 Node 封装成 `org.apache.ibatis.parsing.XNode` 对象,主要为了**动态值的替换**。例如:
```
// XNode.java
public String evalString(String expression) {
return xpathParser.evalString(node, expression);
}
```
- 其它方法,就不详细解析。感兴趣的胖友,可以自己翻翻。
# 3. XMLMapperEntityResolver
`org.apache.ibatis.builder.xml.XMLMapperEntityResolver` ,实现 EntityResolver 接口MyBatis 自定义 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";
/**
* 本地 mybatis-config.dtd 文件
*/
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
/**
* 本地 mybatis-mapper.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. GenericTokenParser
`org.apache.ibatis.parsing.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 "";
}
// search open token
// 寻找开始的 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] == '\\') {
// this open token is escaped. remove the backslash and continue.
// 因为 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] == '\\') {
// this close token is escaped. remove the backslash and continue.
// 因为 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) {
// close token was not found.
// closeToken 未找到,直接拼接
builder.append(src, start, src.length - start);
// 修改 offset
offset = src.length;
} else {
// <x> 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` 进行处理,即 `<x>` 处。
- 所以所以所以,胖友可以耐心看下这段逻辑,也可以忽略,大体理解就好。
- 关于 `handler` 这个 TokenHandler ,详细见 [「5. TokenHandler」](https://svip.iocoder.cn/MyBatis/parsing-package/#) 。当然,这也是为什么 GenericTokenParser 叫做**通用**的原因,而 TokenHandler 处理**特定**的逻辑。
# 5. PropertyParser
`org.apache.ibatis.parsing.PropertyParser` ,动态属性解析器。代码如下:
```
// PropertyParser.java
public class PropertyParser {
// ... 省略部分无关的
private PropertyParser() { // <1>
// Prevent Instantiation
}
public static String parse(String string, Properties variables) { // <2>
// <2.1> 创建 VariableTokenHandler 对象
VariableTokenHandler handler = new VariableTokenHandler(variables);
// <2.2> 创建 GenericTokenParser 对象
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
// <2.3> 执行解析
return parser.parse(string);
}
}
```
- `<1>` ,构造方法,修饰符为 `private` ,禁止构造 PropertyParser 对象,因为它是一个静态方法的工具类。
- ```
<2>
```
,基于
```
variables
```
变量,替换
```
string
```
字符串中的动态属性,并返回结果。
- `<2.1>` ,创建 VariableTokenHandler 对象。
- ```
<2.2>
```
,创建 GenericTokenParser 对象。
- 我们可以看到,`openToken = {` `closeToken = }` ,这不就是上面看到的 `${username}` 和 `{password}` 的么。
- 同时,我们也可以看到,`handler` 类型为 VariableTokenHandler ,也就是说,通过它实现**自定义**的处理逻辑。关于它,在 [「6.1 VariableTokenHandler」](https://svip.iocoder.cn/MyBatis/parsing-package/#) 中详细解析。
- `<2.3>` ,调用 `GenericTokenParser#parse(String text)` 方法,执行解析。
# 6. TokenHandler
`org.apache.ibatis.parsing.TokenHandler` Token 处理器接口。代码如下:
```
// TokenHandler.java
public interface TokenHandler {
/**
* 处理 Token
*
* @param content Token 字符串
* @return 处理后的结果
*/
String handleToken(String content);
}
```
- `#handleToken(String content)` 方法,处理 Token ,在 [「4. GenericTokenParser」](https://svip.iocoder.cn/MyBatis/parsing-package/#) 中,我们已经看到它的调用了。
TokenHandler 有四个子类实现,如下图所示:[![TokenHandler 子类](04-mybatis-解析器模块.assets/02.png)](http://static.iocoder.cn/images/MyBatis/2020_01_07/02.png)TokenHandler 子类
- 本文暂时只解析 VariableTokenHandler 类,因为只有它在 `parsing` 包中,和解析器模块相关。
## 6.1 VariableTokenHandler
VariableTokenHandler ,是 PropertyParser 的内部静态类,变量 Token 处理器。具体什么用途?上面不是已经整的明明白白啦,就不重复解释啦。
### 6.1.1 构造方法
```
// PropertyParser.java
private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
/**
* The special property key that indicate whether enable a default value on placeholder.
* <p>
* The default value is {@code false} (indicate disable a default value on placeholder)
* If you specify the {@code true}, you can specify key and default value on placeholder (e.g. {@code ${db.username:postgres}}).
* </p>
* @since 3.4.2
*/
public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
/**
* The special property key that specify a separator for key and default value on placeholder.
* <p>
* The default separator is {@code ":"}.
* </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 = ":";
// VariableTokenHandler 类里
/**
* 变量 Properties 对象
*/
private final Properties variables;
/**
* 是否开启默认值功能。默认为 {@link #ENABLE_DEFAULT_VALUE}
*/
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);
}
```
- 虽然看起来有一大坨的变量,但是不要怕。
- `variables` 属性,变量 Properties 对象。
- `enableDefaultValue` 属性,是否开启默认值功能。默认为 `ENABLE_DEFAULT_VALUE` ,即**不开启**。想要开启,可以配置如下:
```
<properties resource="org/mybatis/example/config.properties">
<!-- ... -->
<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> <!-- Enable this feature -->
</properties>
```
- `defaultValueSeparator` 属性,默认值的分隔符。默认为 `KEY_DEFAULT_VALUE_SEPARATOR` ,即 `":"` 。想要修改,可以配置如下:
```
<properties resource="org/mybatis/example/config.properties">
<!-- ... -->
<property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/> <!-- Change default value of separator -->
</properties>
```
- 分隔符被修改成了 `?:` 。
### 6.1.2 handleToken
```
// VariableTokenHandler 类里
@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 + "}";
}
```
- 比较简单,胖友自己看。
# 666. 彩蛋
小文一篇。按照前面的代码统计,我们已经看掉了小 1000 行的代码了。继续搞起。
参考和推荐如下文章:
- 徐郡明 [《MyBatis 技术内幕》](https://item.jd.com/12125531.html) 的 [「2.1 解析器模块」](https://svip.iocoder.cn/MyBatis/parsing-package/#) 小节
- 祖大俊 [《Mybatis3.3.x技术内幕Mybatis初始化之六个工具》](https://my.oschina.net/zudajun/blog/668596)