MyBatis 的解析器模块,源码对应 parsing
包。如下图:
该模块主要提供两个功能:
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标签解析》 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
类相关成员变量的初始化赋值操作
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); } } 复制代码
主要依赖 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); } 复制代码
在面对一个Node时,假设我想要把Node的属性集合都以键、值对的形式,放到Properties对象里,同时把Node的body体也通过XPathParser解析出来,并保存起来( 一般是Sql语句 ),方便程序使用,代码可能会是这样的。
private Node node; private String body; private Properties attributes; private XPathParser xpathParser; 复制代码
Mybatis就把上面几个必要属性封装到一个类中,取名叫XNode。
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; } } 复制代码
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
对象
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 defaultValueSeparator
<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> 复制代码
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 进行处理,大家可以耐心看下这段逻辑,通过 源码包中相关的单元测试类
去打断点一行一行跟进