在MyBatis中涉及多个xml文件,解析这些xml文件自然离不开解析器。本文就来分析一下解析器模块。
xml常见的解析方式分为以下三种:
详细的解析xml学习可以参考 Java解析XML 在这里我们需要重点看下DOM解析,DOM解析主要的好处就是易于编程,可以跟根据需求在树形结构的各个节点之间导航。
MyBatis 在初始化过程中处理 mybatis-config.xml
以及映射文件时使用的是DOM解析方式,并结合使用XPath解析XML配置文件。DOM会将整个XML文档加载到内存中形成数据结构。
XPathParser类封装了XPath 、Document和EntityResolver 依赖关系如图所示
XPathParser中字段含义和功能如下
private final Document document; //Document 对象 private boolean validation; //是否开启校验 private EntityResolver entityResolver; //用于加载本地DTD文件 private Properties variables; //mybatis-config.xml <properties> 标签定义的键值对集合 private XPath xpath; //XPath对象
mybatis-config.xml
文件时,默认联网加载 http://mybatis.org/dtd/mybatis-3-config.dtd
这个DTD文档,当网络比较慢会使加载变缓慢。其实在MyBatis中已经配置了关于DTD文件的映射关系。 XMLMapperEntityResolver
中实现了EntityResolver接口,并配置加载本地的DTD文件。关系如图所示:
EntityResolver接口的核心是resolveEntity() 方法,XMLMapperEntityResolver的实现如下:
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"; /** * 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 */ //实现EntityResolver接口的resolveEntity方法 @Override 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()); } } // getInputSource()方法负责读取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) { // ignore, null is ok } } return source; } }
介绍完 XMLMapperEntityResolver
之后,我们回到XPathParser这个类上接下来我们按照组成部分挨个拆分出来。
XPathParser构造方法有16种,应该是满足各种各样不同使用场景下的需求吧。
<img src="http://qiniu-cdn.janker.top/oneblog/20200105215713359.jpg" style="zoom:50%;" />
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(); } //调用createDocument发放之前一定要先调用 commonConstructor() 方法完成初始化 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() { @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); } }
XPathParser.createDocument()
方法中封装了创建 Document
对象的过程并触发了加载XML文档的过程。 XPathParser
中提供了一系列的eval*()方法用于解析boolean、short、Integer、Long、Float、Sting、Double、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>节点下配置是否开启默认值功能的对应配置项 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 = ":"; private PropertyParser() { // Prevent Instantiation } public static String parse(String string, Properties variables) { VariableTokenHandler handler = new VariableTokenHandler(variables); //创建 GenericTokenParser 解析器对象 并制定其占位符为 ${} GenericTokenParser parser = new GenericTokenParser("${", "}", handler); return parser.parse(string); }
方法创建
GenericTokenParser 解析器,并将默认值的处理委托给
GenericTokenParser.parse() `
方法。 GenericTokenParser是通用的占位符解析器,具体代码如下:
package org.apache.ibatis.parsing; /** * 通用的占位符解析器 * @author Clinton Begin */ public class GenericTokenParser { private final String openToken; //占位符的开始标记 private final String closeToken; //占位符的结束标记 private final TokenHandler handler; //TokenHandler接口的实现会按照一定的逻辑解析占位符 public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } public String parse(String text) { //检测 text 是否为空 if (text == null || text.isEmpty()) { return ""; } // search open token // 查找开始标记 int start = text.indexOf(openToken); 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] == '//') { // 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位置 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 = text.indexOf(openToken, offset); //移动start } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } }
GenericTokenParser.parse()
方法比较简单,具体实现就是顺序查找 openToken
和 closeToken
,解析得到占位符的字面值 Tokenhandler
处理,然后将解析结果重新拼装成字符串返回。 TokenHandler
是解析占位符接口,总共有四个实现,如图:
<img src="http://qiniu-cdn.janker.top/oneblog/20200105225624602.png" style="zoom:67%;" />
PropertyParser是使用VariableTokenHandler和GenericTokenParser配合完成占位符解析。代码如下:
package org.apache.ibatis.parsing; import java.util.Properties; /** * @author Clinton Begin * @author Kazuki Shimizu */ public class 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 = ":"; private PropertyParser() { // Prevent Instantiation } public static String parse(String string, Properties variables) { VariableTokenHandler handler = new VariableTokenHandler(variables); //创建 GenericTokenParser 解析器对象 并制定其占位符为 ${} GenericTokenParser parser = new GenericTokenParser("${", "}", handler); return parser.parse(string); } private static class VariableTokenHandler implements TokenHandler { private final Properties variables; private final boolean enableDefaultValue; 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) { // 检测 variables 集合是否为空 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) { //在variable集合中查找指定占位符 return variables.getProperty(key, defaultValue); } } // 不支持默认值的功能,直接查找variables集合 if (variables.containsKey(key)) { return variables.getProperty(key); } } return "${" + content + "}"; //variables集合为空 直接返回 } } }
是
PropertyParser `
类中的一个静态内部类。 VariableTokenHandler
实现了 TokenHandler
接口中的 handlerToken()
方法 <properties>
节点下未定义相应的键值对,则将切分得到额默认值作为解析结果返回。 XPathParser.evalNode()方法返回的类型为XNode,他对org.w3c.dom.Node对象惊醒了封装和解析,具体代码如下:
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.xml配置文件中<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); } public XNode newXNode(Node node) { return new XNode(xpathParser, node, variables); } public XNode getParent() { Node parent = node.getParentNode(); if (!(parent instanceof Element)) { return null; } else { return new XNode(xpathParser, parent, variables); } } public String getPath() { StringBuilder builder = new StringBuilder(); Node current = node; while (current instanceof Element) { if (current != node) { builder.insert(0, "/"); } builder.insert(0, current.getNodeName()); current = current.getParentNode(); } return builder.toString(); } public String getValueBasedIdentifier() { StringBuilder builder = new StringBuilder(); XNode current = this; while (current != null) { if (current != this) { builder.insert(0, "_"); } String value = current.getStringAttribute("id", current.getStringAttribute("value", current.getStringAttribute("property", null))); if (value != null) { value = value.replace('.', '_'); builder.insert(0, "]"); builder.insert(0, value); builder.insert(0, "["); } builder.insert(0, current.getName()); current = current.getParent(); } return builder.toString(); } public String evalString(String expression) { return xpathParser.evalString(node, expression); } public Boolean evalBoolean(String expression) { return xpathParser.evalBoolean(node, expression); } public Double evalDouble(String expression) { return xpathParser.evalDouble(node, expression); } public List<XNode> evalNodes(String expression) { return xpathParser.evalNodes(node, expression); } public XNode evalNode(String expression) { return xpathParser.evalNode(node, expression); } public Node getNode() { return node; } public String getName() { return name; } public String getStringBody() { return getStringBody(null); } public String getStringBody(String def) { if (body == null) { return def; } else { return body; } } public Boolean getBooleanBody() { return getBooleanBody(null); } public Boolean getBooleanBody(Boolean def) { if (body == null) { return def; } else { return Boolean.valueOf(body); } } public Integer getIntBody() { return getIntBody(null); } public Integer getIntBody(Integer def) { if (body == null) { return def; } else { return Integer.parseInt(body); } } public Long getLongBody() { return getLongBody(null); } public Long getLongBody(Long def) { if (body == null) { return def; } else { return Long.parseLong(body); } } public Double getDoubleBody() { return getDoubleBody(null); } public Double getDoubleBody(Double def) { if (body == null) { return def; } else { return Double.parseDouble(body); } } public Float getFloatBody() { return getFloatBody(null); } public Float getFloatBody(Float def) { if (body == null) { return def; } else { return Float.parseFloat(body); } } public <T extends Enum<T>> T getEnumAttribute(Class<T> enumType, String name) { return getEnumAttribute(enumType, name, null); } public <T extends Enum<T>> T getEnumAttribute(Class<T> enumType, String name, T def) { String value = getStringAttribute(name); if (value == null) { return def; } else { return Enum.valueOf(enumType, value); } } // ** 省略 get*()方法 @Override public String toString() { StringBuilder builder = new StringBuilder(); toString(builder, 0); return builder.toString(); } private void toString(StringBuilder builder, int level) { // ** 省略 toString ** } private void indent(StringBuilder builder, int level) { for (int i = 0; i < level; i++) { builder.append(" "); } } 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; } }
的构造函数会调用其
parseAttributes() 方法和
parseBody() 方法解析
org.w3c.dom.Node 对象中的信息,初始化
attributes 集合和
body `
字段。 XNode
中提供了多种 get*()
方法获取所需的节点信息,这些信息主要描述 attributes
集合、 body
字段、 node
字段。 XNode.node
。 以上就是 MyBatis
的解析器模块的全部内容,下一篇博客我们继续分析反射模块。
本文由 Janker 创作,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。