Mybatis 源码学习-解析器模块
Posted 凉茶方便面
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis 源码学习-解析器模块相关的知识,希望对你有一定的参考价值。
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);
以上是关于Mybatis 源码学习-解析器模块的主要内容,如果未能解决你的问题,请参考以下文章
浩哥解析MyBatis源码——Parsing解析模块之通用标记解析器(GenericTokenParser)与标记处理器(TokenHandler)