1.总览
MyBatis解析器模块主要提供了两个功能:
- 一个功能,是对 XPath 进行封装,为 MyBatis 初始化时解析
mybatis-config.xml
配置文件以及映射配置文件提供支持。- 另一个功能,是为处理动态 SQL 语句中的占位符提供支持。
下面,我们就来看看具体的源码。
2.XPathParser
XPathParser是基于 Java XPath 解析器,用于解析 MyBatis mybatis-config.xml
和 **Mapper.xml
等 XML 配置文件。
/**
* XML被解析后生产的Document对象
*/
private final Document document;
/**
* 是否校验 XML 。一般情况下,值为true
*/
private boolean validation;
/**
* XML 实体解析器。
*/
private EntityResolver entityResolver;
/**
* 变量 Properties 对象,用来替换需要动态配置的属性值。
*/
private Properties variables;
/**
* 用于查询 XML 中的节点和元素。
*/
private XPath xpath;
entityResolver
属性,XML 实体解析器。默认情况下,对 XML 进行校验时,会基于 XML 文档开始位置指定的 DTD 文件或 XSD 文件。例如说,解析mybatis-config.xml
配置文件时,会加载http://mybatis.org/dtd/mybatis-3-config.dtd
这个 DTD 文件。但是,如果每个应用启动都从网络加载该 DTD 文件,势必在弱网络下体验非常差,甚至说应用部署在无网络的环境下,还会导致下载不下来,那么就会出现 XML 校验失败的情况。所以,在实际场景下,MyBatis 自定义了 EntityResolver 的实现,达到使用本地 DTD 文件,从而避免下载网络 DTD 文件的效果。variables
属性,用于替换动态配置的属性值,如:<dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource>
variables
的来源,即可以在常用的 Java Properties 文件中配置,也可以使用 MyBatis<property />
标签中配置。<properties resource="org/mybatis/example/config.properties"> <property name="username" value="123"/> <property name="password" value="456"/> </properties>
- 这里配置的
username
和password
属性,就可以替换上面的${username}
和${password}
这两个动态属性。
- 这里配置的
2.1 构造方法
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)));
}
调用 #commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver)
方法,公用的构造方法逻辑。代码如下:
// XPathParser.java
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();
}
调用 #createDocument(InputSource inputSource)
方法,将 XML 文件解析成 Document 对象。代码如下:
/**
* 创建文档
* @param inputSource
* @return
*/
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
// 创建并配置工厂属性
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 校验xml
factory.setValidating(validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
// 创建文档构建对象
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 {
}
});
// 解析文档
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
2.2 eval 方法族
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);
}
}
2.2.1 eval 元素
eval 元素的方法,用于获得 Boolean
、Short
、Integer
、Long
、Float
、Double
、String
类型的元素的值。我们以 #evalString(Object root, String expression)
方法为例子,代码如下:
// XPathParser.java
public String evalString(Object root, String expression) {
// 获得值
String result = (String) evaluate(expression, root, XPathConstants.STRING);
// 基于 variables 替换动态值,如果 result 为动态值
result = PropertyParser.parse(result, variables);
return result;
}
2.2.2 eval 节点
eval 元素的方法,用于获得 Node 类型的节点的值。代码如下:
/**
* 解析Node数组
* @param expression
* @return
*/
public List<XNode> evalNodes(String expression) {
// 从根文档开始解析
return evalNodes(document, expression);
}
public List<XNode> evalNodes(Object root, String expression) {
List<XNode> xnodes = new ArrayList<XNode>();
// 获取节点数组
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
// 封装问XNode数组
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
/**
* 解析单个节点
* @param expression
* @return
*/
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);
}
3. XMLMapperEntityResolver
org.apache.ibatis.builder.xml.XMLMapperEntityResolver
,实现 EntityResolver
接口,MyBatis 自定义 EntityResolver
实现类,用于加载本地的 mybatis-3-config.dtd
和 mybatis-3-mapper.dtd
这两个 DTD 文件。代码逻辑比较简单,简单看下即可:
/**
* MyBatis dtd的离线实体解析器
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
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";
/**
* 将公共DTD转换为本地DTD
*
* @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);
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
// mybatis
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
// ibatis
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 {
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}
}
4. GenericTokenParser
通用的token
解析器,主要是提取被openToken
和closeToken
包裹的token
,并交由`处理替换。
代码逻辑不复杂,主要流程就是找到合适的openToken
,再查询合适的closeToken
,然后再交由handler
来处理token
内容。
// 关键属性
/**
* 包裹token的左表达式
*/
private final String openToken;
/**
* 包裹token的右表达式
*/
private final String closeToken;
/**
* token的替换处理类
*/
private final TokenHandler handler;
// 关键方法
/**
* 替换方法
* @param text 需要解析替换的文本
* @return
*/
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
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] == '\\') {
// 转义字符,不应该被标记为openToken,并把转义的斜杠“\”去掉
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// 找到openToken后,开始处理token部分
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
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();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
// 找到后退出循环
break;
}
}
if (end == -1) {
// 没有找到close token,说明不符合表达式,不进行TokenHandler处理
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 找到对应的token,由TokenHandler处理替换逻辑
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
5. PropertyParser
参数解析器的静态工具类,用于解析动态属性,用于解析型如:${xxx:yyy}
的参数,关键方法如下:
/**
* 转换方法
* @param string
* @param variables
* @return
*/
public static String parse(String string, Properties variables) {
// 变量替换的处理器
VariableTokenHandler handler = new VariableTokenHandler(variables);
// token提取,提取后交由handler进行匹配替换
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
GenericTokenParser
在上面已经解析过,这里就是替换${xxx}
(是不是很熟悉)的变量,然后交给VariableTokenHandler
来处理,那我们就看看这个处理类做了什么吧。
VariableTokenHandler
是PropertyParser内部类,实现了TokenHandler
接口,具体代码如下:
/**
* 参数变量提取处理器
*/
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);
}
/**
* 用于解析${key:defaultValue},默认分割符为“:”,可通过配置修改
* @param content
* @return
*/
@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);
}
}
// 如果没有配置属性,直接返回原值
return "${" + content + "}";
}
}
代码逻辑很清晰,其中包含了两个私有的配置项:
enableDefaultValue
:是否使用默认值,如果找不到配置值,则使用默认值替换,默认是false
defaultValueSeparator
:默认值的分隔符,默认是:
,可以通过配置项修改
6. TokenHandler
该接口只定义了一个通用方法,用于解析token
public interface TokenHandler {
/**
* 处理token
* @param content
* @return
*/
String handleToken(String content);
}
TokenHandler
的实现类有4个,前面已经VariableTokenHandler
,剩余的在后续模块中再解析,敬请期待。
7.XNode
扩展了dom的Node节点,提供了便捷的获取父子节点,动态参数替换后的属性、元素体等,是MyBatis解析器的基础,也是解析器的结果。
关键属性如下:
/**
* 原始文档节点
*/
private final Node node;
/**
* 节点名称
*/
private final String name;
/**
* 节点的内容
*/
private final String body;
/**
* 节点属性
*/
private final Properties attributes;
/**
* 属性
*/
private final Properties variables;
/**
* xpathParser解析器
*/
private final XPathParser 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);
// 解析body
this.body = parseBody(node);
}
在构建XNode的时候,会把node节点上的参数和body先解析好,看看具体的解析方法:
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;
}
逻辑和简单,把节点上的属性转换为Properties
,其中参数的值使用PropertyParser
来处理。
body
的解析:
private String parseBody(Node node) {
// 获取内容
String data = getBodyData(node);
if (data == null) {
// 如果获取到的数据为空,这查询子节点的body数据
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;
}
/**
* 获取解析后的节点的文本内容(忽略子元素)
* @param child
* @return
*/
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提供了便捷的获取父子节点,动态参数替换后的属性、元素体等,我们具体来看几个方法:
- 获取子节点信息:
/**
* 获取子节点列表
* @return
*/
public List<XNode> getChildren() {
List<XNode> children = new ArrayList<XNode>();
NodeList nodeList = node.getChildNodes();
if (nodeList != null) {
for (int i = 0, n = nodeList.getLength(); i < n; i++) {
Node node = nodeList.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
children.add(new XNode(xpathParser, node, variables));
}
}
}
return children;
}
/**
* 把子节点转化的属性对象返回
* @return
*/
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
for (XNode child : getChildren()) {
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
}
public String getStringAttribute(String name) {
return getStringAttribute(name, null);
}
public String getStringAttribute(String name, String def) {
String value = attributes.getProperty(name);
if (value == null) {
return def;
} else {
return value;
}
}
首先获取所有子节点封装为XNode的数组,再从这些XNode的attributes
中提取name
和value
信息,汇总为Properties
输出。
- 获取父节点信息:
/**
* 获取父节点的XNode对象
* @return
*/
public XNode getParent() {
Node parent = node.getParentNode();
if (parent == null || !(parent instanceof Element)) {
return null;
} else {
return new XNode(xpathParser, parent, variables);
}
}
- 获取根节点到当前节点的路径:
/**
* 获取根节点到当前节点的路径
* 如返回root/node
* @return
*/
public String getPath() {
StringBuilder builder = new StringBuilder();
Node current = node;
while (current != null && current instanceof Element) {
if (current != node) {
builder.insert(0, "/");
}
// 在最前面插入当前循环的节点的名称
builder.insert(0, current.getNodeName());
// 往上循环,直到根节点
current = current.getParentNode();
}
return builder.toString();
}
- 从元素的属性中获取id值
/**
* 从该元素的属性中获取id值,优先级分别为:id 》 value 》 property
* 如节点:<namespace id="test"><sql id="sql.testSql"></sql></namespace>
* 则返回:[test]_sql_testSql
* 注意:这里的“.”会被替换为“_”
* @return
*/
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();
}
除了上面提到的方法外,XNode还提供了获取各种类型的参数和eval*
方法,都是简单的类型转换,这里就不一一列出了。