XPath
XPath 是通过表达式在 XML 文件中选择节点的表达式语言,通过 XPath 可以将 XML 中的节点,通过特定的规则将 DOM 对象,转化为 Boolean、Double、String 等 Java 对象。
表达式 | 含义 |
---|---|
nodename | 选取指定节点的子节点 |
/ | 从根节点开始选取指定节点 |
// | 根据指定的表达式,在整个文档中选取匹配的节点(不考虑节点的位置) |
. | 选取当前节点 |
… | 选取当前节点的父节点 |
@ | 选取属性 |
* | 匹配任意节点元素 |
@* | 匹配任意属性节点 |
node() | 匹配任何类型的节点 |
text() | 匹配文本节点 |
| | 选取若干路径 |
[] | 指定某个条件,用于查找某个特定节点 |
简单的举例来说,给定如下 xml 文件内容:
<foo>
<bar/>
<bar/>
<bar/>
</foo>
选择所有的bar 元素://bar
使用通配符选择所有的 foo 元素的子元素:/foo/*
根据 include 属性值选择 foo 元素://foo[@include='true']
Xpath 的核心类包括:
XPath:提供利用 xpath 语法进行处理的入口;
Node:根据选择表达式获取的单个元素节点(如果命中多个节点,node.getNextSibling 会指向后续的节点,直至最后一个);
NodeList:Node的列表;
XPathConstants:作为 XPath.evaluate 的参数,标记 Node 的类型,XPathConstants.NODESET(节点集合,对应类型为 NodeList)、XPathConstants.NODE(节点,对应类型为 Node);XPathConstants.STRING(字符串值,对应类型为String);XPathConstants.BOOLEAN(布尔值,对应类型为 Boolean);XPathConstants.NUMBER(数值,对应类型为 Double);
XPathExpression:预编译的 xpath 表达式,可以被多次使用,方便提高性能。
举例来看:
public class XPathExample {
private static final String xml =
"<inventory>\n" +
" <book year=\"2000\">\n" +
" <title>Snow Crash</title>\n" +
" <author>Neal Stephenson</author>\n" +
" <publisher>Spectra</publisher>\n" +
" <isbn>0553380958</isbn>\n" +
" <price>14.95</price>\n" +
" </book>\n" +
" <book year=\"2005\">\n" +
" <title>Burning Tower</title>\n" +
" <author>Larry Niven</author>\n" +
" <author>Jerry Pournelle</author>\n" +
" <publisher>Pocket</publisher>\n" +
" <isbn>0743416910</isbn>\n" +
" <price>5.99</price>\n" +
" </book>\n" +
" <book year=\"1995\">\n" +
" <title>Zodiac</title>\n" +
" <author>Neal Stephenson</author>\n" +
" <publisher>Spectra</publisher>\n" +
" <isbn>0553573862</isbn>\n" +
" <price>7.50</price>\n" +
" </book>\n" +
"</inventory>";
// 参考文档:https://docs.oracle.com/javase/10/docs/api/javax/xml/xpath/package-summary.html
// 直接使用 xpath 读取文件内容
@Test
public void javaSDKEg() throws Exception {
// parse the XML as a W3C Document
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(xml)));
//Get an XPath object and evaluate the expression
XPath xpath = XPathFactory.newInstance().newXPath();
String expression = "/inventory/book";
Node widgetNode = (Node) xpath.evaluate(expression, document, XPathConstants.NODE);
System.out.println(widgetNode.getNodeName());
// log.info("{}", JsonUtils.serialize(widgetNode));
}
@Test
public void book() throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
// 开启验证
// documentBuilderFactory.setValidating(true);
documentBuilderFactory.setNamespaceAware(false);
documentBuilderFactory.setIgnoringComments(true);
documentBuilderFactory.setIgnoringElementContentWhitespace(false);
documentBuilderFactory.setCoalescing(false);
documentBuilderFactory.setExpandEntityReferences(true);
// 创建 DocumentBuilder
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder(); //设置异常处理对象
builder.setErrorHandler(new ErrorHandler() {
@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());
}
@Override
public void warning(SAXParseException exception) throws SAXException {
System.out.println("WARN:" + exception.getMessage());
}
});
// 将文档加载到一个 Document 对象中
Document doc = builder.parse(new InputSource(new StringReader(xml)));
// 创建 XPathFactory
XPathFactory factory = XPathFactory.newInstance(); //创建 XPath 对象
XPath xpath = factory.newXPath();
// 编译 XPath 表达式
XPathExpression expr =
xpath.compile("//book[author='Neal Stephenson']/title/text()");
// 通过XPath表达式得到结果,第一个参数指定了 XPath 表达式进行查询的上下文节点,也就是在指定
// 节点下查找符合 XPath 的节点。本例中的上下文节点是整个文档;第二个参数指定了 XPath 表达式
// 的返回类型 。
Object result = expr.evaluate(doc, XPathConstants.NODESET);
System.out.println("查询作者为 Neal Stephenson 的图书的标题:");
NodeList nodes = (NodeList) result; // 强制类型转换
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
System.out.println("查询 1997 年之后的图书的标题:");
nodes = (NodeList) xpath.evaluate("//book[@year>1997]/title/text()", doc, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
System.out.println("查询 1997 年之后的图书的属性和标题:");
nodes = (NodeList) xpath
.evaluate("//book[@year>1997]/@* | //book[@year>1997]/title/text()", doc, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
}
}
总的来说,XPath 是使用选择器选择 XML 文件内元素的工具,在 Mybatis 中解析 config 和 mapper 文件均使用该工具。
XPathParser
XPathParser 提供了对 XML 文件解析的能力,它将 XPath、Document 对象做了封装,便于进行对象操作,此外,XPathParser 还对原始方法做了升级,使得原本只能处理 String、Integer、Double 类型的数据,升级为能够处理 String、Boolean、Short、Integer、Long、Float、Double。
public class XPathParser {
private final Document document; // Document 对象
private boolean validation; // 是否开启验证
private EntityResolver entityResolver; // 用于加载本地 DTD 文件
private Properties variables; // mybatis-config.xml 中<propteries>标签定义的键位对集合
private XPath xpath; // XPath 对象
public XPathParser(…) { // 多个重载的构造函数,用于加载 xml 文件
commonConstructor(…); // 通用构造器
this.document = createDocument(…); // 创建 Document 对象
}
// 设置外界参数,同时初始化 XPath 对象
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(); // 初始化 XPath 对象
}
// 调用 createDocument ()方法之前一定要先调用 commonConstructor()方法完成初始化
private Document createDocument(InputSource inputSource) {
try {
//创建 DocumentBuilderFactory 对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 为 Document 设置验证参数
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);
// 设置 ErrorHandler 处理器,所有的方法都是空方法
builder.setErrorHandler(new ErrorHandler() { … });
//加载 XML 文件,构造 Document 对象
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
public void setVariables(Properties variables) // 设置外部 properties
public String evalString(…) // 重载函数,处理 String
public Boolean evalBoolean(…) // 重载函数,处理 Boolean
public Short evalShort(…) // 重载函数,处理 Short(通过 Short.valueOf(evalString(…)))
public Integer evalInteger(…) // 重载函数,处理 Integer(通过 Integer.valueOf(evalString(…))
public Long evalLong(…) // 重载函数,处理 Long(通过 Long.valueOf(evalString(…))
public Float evalFloat(…) // 重载函数,处理 Float(通过 Float.valueOf(evalString(…))
public Double evalDouble(…) // 重载函数,处理 Double
public List<XNode> evalNodes(…) // 重载函数,将原始 NodeList 构造为 List<XNode>
public XNode evalNode(…) // 重载函数,将原始 Node 构造为 XNode
}
从上边的代码可以看出,初始化 XPathParser 时,会通过 createDocument 方法触发对 XML 文件的加载,并创建 Document 对象,此外在调用 createDocument 之前会要求必须执行 commonConstructor 方法以初始化 xpath 对象。除了初始化部分,XPathParser 提供了一系列 eval* 方法,这些方法被用于解析特定的数据类型,它们背后实际上调用的还是 xpath.evaluate(…) 方法。
总的来说,XPathParser 是Mybatis 中解析 XML 文件的工具,它封装了 XPath 的方法,使得解析的代码更加清晰、简单。
EntityResolver
默认情况下,在对 XML 文件进行验证时,会从网络下载 XML 文件对应的 DTD 文件或 XSD 文件,如,解析 mybatis-config.xml 时会从网络下载 http://mybatis.org/dtd/mybatis-3-config.dtd
,但是某些情况下,运行时,是没有网络,或者每次都网络下载,其开销也是非常大的,为了避免从网络下载验证文件,则需要使用 EntityResolver 加载本地验证文件。XMLMapperEntityResolver 是 MyBatis提供的 EntityResolver 接口的实现类。
EntityResolver 接口中仅存在一个方法:resolveEntity,程序会在读取任何外部资源之前,都会首先调用该方法。XMLMapperEntityResolver 中设置了网络文件与本地 jar 包中的 DTD 文件的路径对应关系,在加载文件时,会自动用本地文件替代网络文件。
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";
// resolveEntity ()方法是 EntityResolver 接 口中 定义的方法,
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
//查找 systemId 指定的 DTD 文档 , 并调用 getInputSource ()方法读取 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 对象
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) {
}
}
return source;
}
}
总的来说,EntityResolver 是用来用本地 DTD 文件替换远程网络文件,辅助验证 XML 文件的工具。
PropertyParser
在 XPathParser.evalString 方法中,存在对 PropertyParser 方法的调用,用于对节点的默认值进行处理。
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>节点下是否开启默认值功能的对应配置项
public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
// 配置占位符与默认值之间的默认分隔符的对应配置项
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 是通用的 token 解析器,用于根据参数,将特定的占位符去除并用属性值替换。
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
// 创建 GenericTokenParser 对象,并指定其处理的占位符格式为”${ }”
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
对于 GenericTokenParser 而言,它的核心方法就是 parse 方法,它会根据 openToken 和 closeToken 在待解析的字符串中使用 TokenHandler 处理占位符,处理完成后,并重构成新的字符串。
public class GenericTokenParser {
// … 初始化方法
public String parse(String text) {
// … 判断 text 是否是空的
// 检查 openToken 是否在 text 中出现过,未出现(indexOf == -1)则直接返回 text
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;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// 遇到转义的开始标记,则直接将前面的字符串以及开始标记追加到 builder 中
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// 查找到开始标记,且未转义
if (expression == null) {
expression = new StringBuilder();
} else {
// 如果已经存在 expression 则重置 expression 的长度,使得 expression 的数据为空
expression.setLength(0);
}
// 将前面的字符串追加到 builder 中,其中 offset 是上次的开始下标,start 本次查询到的位置
builder.append(src, offset, start - offset);
// 修改 offset 的位置,设置为 openToken 的位置 + openToken.length,即 openToken 之后的第一个位置
offset = start + openToken.length();
// 从 offset 向后查询 closeToken
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// 处理转义的结束标记,并去除转义字符
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
// 继续向后搜索 closeToken
end = text.indexOf(closeToken, offset);
} else {
// 将开始标记和结束标记之间的字符串追加到 expression 中保存
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// 未找到结束标记,将剩余字符串全部存储至 builder
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 将占位符的字面值交给 TokenHandler 处理,并将处理结果追加到 builder 中保存
// 最终拼凑出解析后的完整内容
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
// 继续向后搜索,注意 offset,如果 offset 是 test 的结尾,start 必定是 -1,即不会有后续的遍历
start = text.indexOf(openToken, offset);
}
// 处理最后剩余的字符串
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
TokenHandler 是专门解析占位符的工具类,它共有 4 种实现,每种实现均是内部类的方式。在 PropertyParser 中,使用它的内部类 VariableTokenHandler 进行占位符解析。
TokenHandler 中的 handleToken 方法是它的子类的唯一逻辑,VariableTokenHandler 提供了该方法。VariableTokenHandler 的 handleToken 方法
private static class VariableTokenHandler implements TokenHandler {
private final Properties variables; // <properties>节点下定义的键值对,用于替换占位符
private final boolean enableDefaultValue; // 是否支持占位符中使用默认值的功能
private final String defaultValueSeparator; // 指定占位符和默认值之间的分隔符
private VariableTokenHandler(Properties variables) {
this.variables = variables;
// 获取 <properties> 节点的设置
this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
}
// 根据 key 获取设置
private String getPropertyValue(String key, String defaultValue) {
return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
}
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) {
// 在 variables 集合中查找指定的占位符
return variables.getProperty(key, defaultValue);
}
}
// 不支持默认值的功能,则直接查找 variables 集合
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
// 无 variables 集合,直接返回(这里是因为 VariableTokenHandler 和 GenericTokenParser 需要配合使用才能这么返回)
return "${" + content + "}";
}
}
对 PropertyParser 举例而言,如果存在配置${userName:root}
,此时占位符是:
,PropertyParser 会在 <properties>
节点下的 variables 集合中寻找 key userName,如果存在,则使用 variables 指定的值,否则使用 root 作为默认值。
需要注意的是, GenericTokenParser 不仅仅用于默认值解析,还会用于动态 SQL 语句的解析。GenericTokenParser 只是查找到指定的占位符,而具体的解析行为会交给 TokenHandler,这是一种策略模式。
总结而言,PropertyParser 可以用来处理 XML 文件中的默认值,它可以使用 GenericTokenParser 在上下文中搜索待匹配的通配符,并使用 TokenHandler 处理通配符中的内容,将内容中的属性 key 和默认值分割,并从 properties 标签中获取默认值(properties 不存在默认值时,使用内容中的默认值)。
XNode
在 XPathParser 进行节点解析时(执行 XPathParser.evalNode)返回的是 XNode 对象,而不是 XPath 的原始 Node 对象。XNode 是对 Node 对象的封装,方便解析对应节点的 attribute 和 value。
public class XNode {
private final Node node; // org.w3c.dom.Node 对象
private final String name; // Node 节点名称
private final String body; // 节点的内容
private final Properties attributes; // 节点属性集合
private final Properties variables; // mybatis-config.xrnl 配置文件中<properties>节点下定义的键位对
private final XPathParser xpathParser; // XPathParser 对象,该 XNode 对象由此 XPathParser 对象生成
public XNode(XPathParser xpathParser, Node node, Properties variables) {
this.xpathParser = xpathParser;
this.node = node;
this.name = node.getNodeName();
this.variables = variables;
this.attributes = parseAttributes(node); // 初始化节点属性
this.body = parseBody(node); // 初始化节点内容
}
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;
}
// … 其他方法
}
XNode 中提供了多种 get*()方法获取所需的节点信息,这些信息主要来自上面介绍的 attribute 集合、body宇段、node宇段 。
总结
XPath 是解析 XML 文件的一种方式,它可以很方便的从 DOM 树中选择指定的节点。Mybatis 提供了对 XPath 的封装 XPathParser,它可以更容易的将 XPath 中搜索到的节点转化为基本数据类型。在 XPathParser 中会使用 EntityResolver 处理对 DTD 验证文件的读取,避免直接从网络获取 DTD 文件,Mybatis 的 XMLMapperEntityResolver 提供了对 mybatis-3-config.dtd
和 mybatis-3-mapper.dtd
的支持。PropertyParser 可以用来处理 XML 文件中的默认值,它使用 GenericTokenParser 遍历待匹配字符,使用 VariableTokenHandler 实际处理每个通配符内的内容。XNode 是对 org.w3c.dom.Node 对象的封装,它提供了对节点内容和属性的封装,便于实际使用和操作节点。
参考文档:《Mybatis 技术内幕》
本文的基本脉络参考自《Mybatis 技术内幕》,编写文章的原因是希望能够系统地学习 Mybatis 的源码,但是如果仅阅读源码或者仅从官方文档很难去系统地学习,因此希望参考现成的文档,按照文章的脉络逐步学习。
欢迎关注我的公众号:我的搬砖日记,我会定时分享自己的学习历程。