解析器模块
1. 简介
解析器模块,对应的包是parsing,
如下图所示:

解析器模块,主要提供了两个功能:
- 对XPath进行封装,为Mybatis初始化时解析
mybatis-config.xml
配置文件以及mapper
映射配置文件提供支持。 - 为处理动态SQL语句中的占位符提供支持。
2. XML解析方式
在mybatis中涉及到多个XML配置文件,因此我们首先要介绍XML解析的相关内容。XML解析的常见的方式有三种:
- DOM(document object model)解析方式
- SAX(simple api for xml)解析方式
- Java6版本开始,JDK致辞的StAX(streaming api for xml)解析方式。
DOM是基于树形结构的XML解析方式,它将整个XML文档读入内存并构建成一个DOM树,基于这棵树形结构对各个节点(node)进行操作。XML文档中的每一个成分都是一个节点:整个文档是一个文档节点,每个XML标签对应一个元素节点,包含在XML标签中的文本是文本节点,每一个XML属性是一个属性节点,注释属于注释节点。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
解析后得到的树形结构为:
3. XPath
mybatis在初始化过程中处理mybatis-config.xml配置文件以及映射文件时,使用的是DOM解析方式,并结合XPath解析xml配置文件,正如前文所述,DOM会对整个XML文档加载到内存中形成树形数据结构,而xpth是一种为查询XML文档而设计的语言,它可以与DOM解析方式配合使用,实现对XML文档的解析。xpath之于XML就好比SQL语言之于数据库。
代码示例:
import org.apache.ibatis.io.Resources;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.io.IOException;
import java.io.InputStream;
/**
* XPath解析测试 {@link javax.xml.xpath.XPath}
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2019/11/25 10:11
*/
public class XPathTest {
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
//创建DOM解析器工厂
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//开启验证
documentBuilderFactory.setValidating(true);
documentBuilderFactory.setNamespaceAware(false);
documentBuilderFactory.setIgnoringComments(true);
documentBuilderFactory.setIgnoringElementContentWhitespace(false);
documentBuilderFactory.setCoalescing(false);
documentBuilderFactory.setExpandEntityReferences(true);
//得到一个DOM解析器对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
documentBuilder.setErrorHandler(new ErrorHandler() {
@Override
public void warning(SAXParseException exception) throws SAXException {
System.out.println("warning" + exception.getMessage());
}
@Override
public void error(SAXParseException exception) throws SAXException {
System.out.println("error" + exception.getMessage());
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
System.out.println("fatalError" + exception.getMessage());
}
});
String resource = "resources/xpath-demo.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//解析xml
Document document = documentBuilder.parse(inputStream);
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();
XPathExpression xPathExpression = xPath.compile("/configuration/mappers/mapper");
NodeList evaluate = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < evaluate.getLength() ; i++) {
Node node = evaluate.item(i);
String nodeName = node.getNodeName();
if (("mapper").equals(node.getNodeName())){
Node attributeNode = node.getAttributes().getNamedItem("resource");
String nodeValue1 = attributeNode.getNodeValue();
System.out.println(nodeName +"......"+nodeValue1);
}
}
}
}
运行结果:
mapper......org/mybatis/example/BlogMapper.xml
mapper......org/mybatis/example/UserMapper.xml
4. XPathParser
Mybatis提供的org.apache.ibatis.parsing.XPathParser
类封装了前面涉及到的XPath
、Document
和EntityResolver
,如图所示:
XPathParser中各个字段的含义和功能如下所示:
/**
* xml document 对象
*/
private final Document document;
/**
* 是否开启校验xml
*/
private boolean validation;
/**
* 用于加载本地DTD文件
*
* xml实体解析器,默认情况下,对XML校验是,会基于xml文档开始位置定义的DTD文件或者XSD文件,
* 例如:
* 解析mybatis-config.xml配置文件时,会加载http://mybatis.org/dtd/mybatis-3-config.dtd这个DTD文件。
* 但是,如果如果每个应用启动都从网络加载该DTD文件,势必在弱网络下体验非常下,甚至说应用部署在无网络的环境下,
* 还会导致下载不下来,那么就会出现XML校验失败的情况 ,所以,在实际场景下,MyBatis自定义了EntityResolver
* 的实现,达到使用本地DTD文件,从而避免下载网络 DTD 文件的效果。
*
* @see org.apache.ibatis.builder.xml.XMLMapperEntityResolver
*/
private EntityResolver entityResolver;
/**
* 变量 Properties对象
*/
private Properties variables;
/**
* java XPath对象
* 用于查询xml中的节点和元素
*/
private XPath xpath;
默认情况下,对XML文档进行验证时,会根据XML文档开始位置指定的网址加载对应的DTD
文件或者XSD
文件。如果是解析mybatis-config.xml
配置文件时,默认会加载http://mybatis.org/dtd/mybatis-3-config.dtd
这个DTD
文件。但是,如果如果每个应用启动都从网络加载该DTD
文件,势必在弱网络下体验非常下,甚至说应用部署在无网络的环境下,还会导致下载不下来,那么就会出现XML校验失败的情况 ,所以,在实际场景下,MyBatis
自定义了EntityResolver
的实现,达到使用本地DTD
文件,从而避免下载网络 DTD
文件的效果。XMLMapperEntityResolver
是mybatis
提供的EntityResolver
接口的实现类,如下图所示:
EntityResolver
接口的核心是resolveEntity()
方法,XMLMapperEntityResolver
是实现如下所示:
package org.apache.ibatis.builder.xml;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import org.apache.ibatis.io.Resources;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* XML映射器实体解析器
* Offline entity resolver for the MyBatis DTDs.
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class XMLMapperEntityResolver implements EntityResolver {
/**
* 指定mybatis-config.xml文件和映射文件对应的DTD的SystemId
*/
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.xml文件和映射文件对应的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";
/**
* {@link #resolveEntity(String, String)}是{@link EntityResolver} 接口中定义的方法,具体实现如下所示:
*
* 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);
/**
* 查找systemId指定的DTD文档,并调用{@link #getInputSource(String, String, String)}方法读取DTD文档
*/
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} 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());
}
}
/**
* 读取DTD文档并形成InputSource对象
* @param path 文件所在路径
* @param publicId 公共标志符
* @param systemId systemId
* @return InputSource
*/
private InputSource getInputSource(String path, String publicId, String systemId) {
InputSource source = null;
if (path != null) {
try {
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}
}
介绍完XMLMapperEntityResolver
之后,回到XPathParser
的分析。在XPathParser.createDocument()
方法中封装了前面介绍的创建Document
对象的过程并触发了加载XML文档的过程,具体实现如下:
/**
* 调用这个方法之前一定要先调用{@link #commonConstructor(boolean, Properties, EntityResolver)}方法完成初始化。
* 将xml解析为Document对象
* @param inputSource
* @return
*/
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
//创建DocumentBuilderFactory对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//对DocumentBuilderFactory进行一系列的配置
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setValidating(validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
//创建DocumentBuilder对象并进行配置
DocumentBuilder builder = factory.newDocumentBuilder();
//设置EntityResolver接口对象
builder.setEntityResolver(entityResolver);
builder.setErrorHandler(new ErrorHandler() {
//其中实现的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 {
// NOP
}
});
//加载xml文件
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
/**
* 通用构造:
* 就是通用的赋值操作,主要是的得到一个xpath解析器对document对象进行解析
* @param validation
* @param variables
* @param entityResolver
*/
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
XPathParser
中提供了一系列的eval*()
方法用于解析boolean
、short
、long
、int
、String
、Node
等类型的信息,它通过前面介绍代码中介绍的XPath.evaluate()
方法查找指定路径的节点或属性,并进行相应的类型转换。具体的代码比较简单,就不贴出来了,这里需要注意的是XPathParser.evalString()
方法,其中会调用PropertyParser.parse()
方法处理节点中相应的默认值,具体实现如下所示:
public String evalString(Object root, String expression) {
String result = (String) evaluate(expression, root, XPathConstants.STRING);
//处理节点中相应的默认值
result = PropertyParser.parse(result, variables);
return result;
}
在PropertyParser
中指定了是否开启默认值的功能以及默认的分隔符,相应字段如下所示:
private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
/**
* 在mybatis-config.xml中的<properties></properties>节点下配置是否开启默认值功能的对应配置项
* 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 = ":";
PropertyParser.parse()
方法中会创建GenericTokenParser解析器,
并将默认值的处理委托给GenericTokenParser.parse()
方法,实现如下:
/**
* 当前方法中会创建{@link GenericTokenParser}解析器,并将默认值的处理委托给{@link GenericTokenParser#parse(String)}
* @param string
* @param variables
* @return
*/
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
//创建GenericTokenParser对象,并指定其处理的占位符格式为"${}"
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
GenericTokenParser
是一个通用的占位符解析器,其字段的含义如下:
/**
* 占位符的开始标记
*/
private final String openToken;
/**
* 占位符的结束标记
*/
private final String closeToken;
/**
* {@link TokenHandler}接口的实现会按照一定的逻辑解析占位符
*/
private final TokenHandler handler;
GenericTokenParser.parse()
方法的逻辑并不复杂,它会顺序查找openToken
和closeToken
,解析得到的占位符的字面值,并将其交给TokenHandler
处理,然后将解析结果重新拼装成字符串并返回。该方法的实现如下:
/**
* 此方法的逻辑并不复杂,它会顺序查找{@link #openToken}和{@link #closeToken} ,解析得到占位符的字面值,
* 将将其交给{@link TokenHandler}进行处理,然后将解析结果重新拼装成字符串并返回。
* @param text 拼装之前的字符串
* @return 拼装之后后字符串
*/
public String parse(String text) {
//检测text是否为空
if (text == null || text.isEmpty()) {
return "";
}
// search open token
// 查找开始标记
int start = text.indexOf(openToken);
// 检测start是否为-1
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
// builder是用来记录解析后的字符串
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
// 遇到转移的开始标记,则直接将前面的字符串以及开始标记追加到builder中
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
// 查找到开始标记,且未转义
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
// 将前面的字符串追加到builder中
builder.append(src, offset, start - offset);
offset = start + openToken.length();
// 从offset向后继续查找结束标记
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中保存
expression.append(src, offset, end - offset);
break;
}
}
//未找到结束标记
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//将占位符的字面值交给TokenHandler处理,并将处理结果追加到builder中保存
//最终拼凑出解析后的完整内容
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
//移动start
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
占位符有TokenHandler
接口的实现进行解析,TokenHandler
接口总共有四个实现,如下图所示:
通过对PropertyParser.parse()
方法的介绍,我们知道了PropertyParser
是使用VariableTokenHandler
与GenericTokenParser
配合完成占位符解析的,VariableTokenHandler
是PropertyReslover
中的一个私有静态内部类,其字段的含义如下所示:
/**
* <properties><properties/>节点下定义的键值对,用于替换占位符
*/
private final Properties variables;
/**
* 是否支持占位符中使用默认值的功能
*/
private final boolean enableDefaultValue;
/**
* 指定占位符和默认值之间的分隔符
*/
private final String defaultValueSeparator;
VariableTokenHandler
实现了TokenHandler
接口中的handlerToken()
方法,该实现首先会按照defaultValueSeparator
字段指定的分隔符对整个占位符切分,得到占位符的名称和默认值,然后按照切分得到的占位符名称查找对应的值,如果在<properties>
节点下未定义相应的键值对,则将切分得到的默认值作为解析结果返回。
/**
* 实现了{@link TokenHandler}接口中的{@link TokenHandler#handleToken(String)}方法,该实现:
* 1. 首先会按照{@link #defaultValueSeparator}字段指定的分隔符对整个占位符切分,得到占位符的名称和默认值;
* 2. 然后按照切分得到的占位符名称查找对应的值,如果在<properties><properties/>节点中未定义对应的键值对,
* 则将切分得到的默认值作为解析结果返回。
* @param content
* @return
*/
@Override
public String handleToken(String content) {
// 检测variable集合是否为空
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) {
//在variables集合中查找指定的占位符
return variables.getProperty(key, defaultValue);
}
}
// 不支持默认值的功能,则直接查找variables集合
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
//variables集合为空,直接返回
return "${" + content + "}";
}
}
GenericTokenParser
此类不仅用于默认值解析PropertyParser#parse(String, Properties)
,还会在动态SQL
语句的解析中用到。很明显,GenericTokenParser
只是查找到指定的占位符,而具体的解析行为会根据持有的TokenHandler实现的不同而不同,这有点策略模式的意思。
回到对XPathParser
的分析,XPathParser.evalNode()
方法返回值类型是XNode,它对org.w3c.dom.Node
对象做了封装和解析,其各个字段的含义如下:
/**
* org.w3c.dom.Node对象
*/
private final Node node;
/**
* Node节点名称
*/
private final String name;
/**
* Node节点内容
*/
private final String body;
/**
* 节点属性集合
*/
private final Properties attributes;
/**
* mybatis-config.xml配置文件中<properties><properties/>节点下定义的键值对
*/
private final Properties variables;
/**
* XPathParser对象,该Node对象由此XPathParser对象生成
*/
private final XPathParser xpathParser;
XNode
的构造函数中会调用其parseAttributes()
方法和parseBody()
方法解析org.w3c.dom.Node
对象中的信息,初始化attributes
集合和body
字段,具体初始化过程如下:
/**
* 解析Node对象中的Attributes属性集合
* @param n 节点对象
* @return
*/
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;
}
/**
* 解析Node对象中的信息body字段
* @param node
* @return
*/
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;
}
XPathParser测试用例
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import java.io.IOException;
import java.util.List;
/**
* @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
* @since : 2019/11/25 16:11
*/
public class XPathParserTest {
public static void main(String[] args) throws IOException {
String resource = "resources/xpath-demo.xml";
XPathParser xPathParser = new XPathParser(Resources.getResourceAsReader(resource),true,null,new XMLMapperEntityResolver());
XNode xNode = xPathParser.evalNode("/configuration");
List<XNode> children = xNode.getChildren();
for (int i = 0; i < children.size(); i++) {
System.out.println(children.get(i).getName());
}
}
}
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!