Mybatis 解析配置文件的源码解析
TSMYK Java技术编程
前言
使用过Mybatis 的都知道,Mybatis 有个配置文件,用来配置数据源,别名,一些全局的设置如开启缓存之类的, 在Mybatis 在初始化的时候,会加载该配置文件,会对该配置文件进行解析;它采用的是 DOM 的方式进行解析,它会把整个配置文件加载到内存中形成一种树形结构,之后使用 XPath 的方式可以从中获取我们到需要的值。下面来看下 Mybatis 是如何解析配置文件的。
XPath
在查看源码之前,先来看下 XPath 是什么东东,使用过 Python + selenium 进行过自动化的应该都知道,就是通过 XPath 来定位页面元素,如按钮等,之后添加事件来代替人工点击页面。简单点来说,XPath 就是用来定位 XML 元素的,它可用来在 XML 文档中对元素和属性进行遍历。XPath 有它自己的语法,具体的可以参考 XPath 教程
W3School,XPath 教程 (http://www.w3school.com.cn/xpath/index.ASP)
配置文件
Mybatis 的配置文件大概如下所示,源码分析通过它来进行分析:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties>
<property name="username" value="root"/>
<property name="password" value="root"/>
<!-- 启动默认值 -->
<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
</properties>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<typeAliases>
<typeAlias type="mybatis.pojo.Person" alias="person"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username:root}"/><!-- 使用默认值 -->
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis-mapper.xml"/>
</mappers>
</configuration>
源码分析
Mybatis 解析配置文件的类主要在 parsing 包下,该包下只有 6 个类,如下所示:
XPathParser : 是 Java 提供的 XPath 类的一个包装,主要的逻辑就是该类中实现的。
PropertyParser : 属性解析器
TokenHandler : 占位符解析器,是一个接口,由子类自己实现解析规则
GenericTokenParser : 通用的占位符解析器,用来处理 #{} 和 ${}参数
XNode :把文档加载到内存后,每个标签就是一个节点,通过它可以获取到节点的属性,父节点,子节点等。是对 Java Node类的包装
ParsingException : 自定义异常,可以忽略不看
上面这几个类的关系大致如下所示:
先来看看这几个类的源码,最后再来看这个图。
XPathParser
先来看下 XPathParser 类,它主要用来加载配置文件,提供获取文件中节点值的入口。
// 该类共有 5 个属性
public class XPathParser {
// 需要解析的文档
private Document document;
// 是否开启验证,即加载对应的DTD文件或XSD文件进行验证,如果开启的话,会联网加载,否则的话会加载本地的DTD文件进行验证
private boolean validation;
// 用于加载本地的 DTD 文件,可以忽略不看
private EntityResolver entityResolver;
// 对应 mybatis-config 配置文件中 <properties> 标签
private Properties variables;
// XPath 对象
private XPath xpath;
// XPathParser 提供了很多重载的构造方法,这里就不一一列出来了
public XPathParser(InputStream inputStream) {
// 设置上面 4 个属性
commonConstructor(false, null, null);
// 为 document 属性赋值
this.document = createDocument(new InputSource(inputStream));
}
// 构造方法调用,用于为属性赋值
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();
}
// 根据输入流来创建文档,返回代表该文档的一个 Document 对象
private Document createDocument(InputSource inputSource) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// ..........忽略........
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver);
builder.setErrorHandler(new ErrorHandler() {
// ..........忽略........
});
// 通过 DocumentBuilder 解析文档,返回代表该文档的一个 Document 对象
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
}
通过了以上部分的代码,就可以把配置文件通过流读取来创建代表该文档的一个 Document 对象;接下来,如果想获取文件中的值怎么办呢,就是通过 XPath 对象执行 XPath 表达式来获取的,
该类中有很多的 eval*() 方法,用于获取对应类型的值,但最终都会调用 XPath 对象的 evaluate() 来获取,以 evalString() 为代表看下它是怎么获取的:
// 执行 XPath 表达式
public String evalString(String expression) {
return evalString(document, expression);
}
// 在 root 上执行 XPath 表达式
public String evalString(Object root, String expression) {
String result = (String) evaluate(expression, root, XPathConstants.STRING);
result = PropertyParser.parse(result, variables);
return result;
}
// 根据表达式,文档对象,和返回类型,调用 XPath 对象的 evaluate 方法执行表达式
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);
}
}
// 文档的返回类型
public class XPathConstants {
// 数值类型
public static final QName NUMBER = new QName("http://www.w3.org/1999/XSL/Transform", "NUMBER");
// String 类型
public static final QName STRING = new QName("http://www.w3.org/1999/XSL/Transform", "STRING");
// boolean 类型
public static final QName BOOLEAN = new QName("http://www.w3.org/1999/XSL/Transform", "BOOLEAN");
// NodeList 类型
public static final QName NODESET = new QName("http://www.w3.org/1999/XSL/Transform", "NODESET");
// Node 类型
public static final QName NODE = new QName("http://www.w3.org/1999/XSL/Transform", "NODE");
// 不知道啥类型
// The URI for the DOM object model
public static final String DOM_OBJECT_MODEL = "http://java.sun.com/jaxp/xpath/dom";
}
在上述的 evalString 方法中,在调用 XPath 执行完后,再调用 PropertyParser 的 parse 方法对结果进行解析,该方法用于处理节点中相应的默认值,这里先不看该方法,后面再来看。
除了可以获取对应类型的值外,还可以返回对应的节点即 XNode 或 XNode 的集合:
// 根据表达式获取 XNode 集合
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);
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
// 获取单个 XNode
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);
}
以上这些就是 XPathParser 类的主要代码了,还是很好理解的,需要特别注意的是 evalString 方法中 PropertyParser.parse 。接下来就来看下该类的一个实现。
PropertyParser
PropertyParser 属性解析器,主要有两个作用,一是判断是否开启了默认值,二是如果开启了默认值,则根据 key 去获取不到值的时候,则取默认值。
源码如下:
public class PropertyParser {
// 是否开启默认值的前缀
private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
// 开启默认值的属性
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 = ":";
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
// 先忽略
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
// 占位符的一个实现类,
private static class VariableTokenHandler implements TokenHandler {
// 属性值,相当于一个 map,里面存放着属性和值的对应关系
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);
}
// 根据key在 variables 中获取对应的值
private String getPropertyValue(String key, String defaultValue) {
return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
}
// 主要方法
// 该方法会在 GenericTokenParser.parse() 方法中进行回调
// 当从 GenericTokenParser 中解析得到属性名的时候,会把属性名传入该方法来去 variables 中查找对应的值,如果找不到且开启了默认值,则返回默认值
@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) {
// 从属性名+分隔符+默认值(name:defaultVal)的字符串中获取属性名
key = content.substring(0, separatorIndex);
// 获取默认值
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
// 有默认值
if (defaultValue != null) {
// 在 属性集合中获取对应的属性值,如果不存在,则返回默认值
return variables.getProperty(key, defaultValue);
}
}
// 如果还没开启默认值,则直接中属性集合中获取,获取不到返回null
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
// 如果属性集合为空,则直接返回 ${name} 的形式
return "${" + content + "}";
}
}
}
在该类中,主要有两个方法:
-
parse() 方法,在该方法中,会调用 GenericTokenParser 的 parse() 方法进行解析,这里先不管,知道它是从 ${name} 形式的字符串获取name 字符串就可以了
- VariableTokenHandler 类的 handleToken() 方法,该类是它的一个内部类,实现了 TokenHandler 接口,当从 GenericTokenParser 的 parse() 方法得到属性名的时候,会拿属性名去属性集合中查找对应的值,如果找不到且开启了默认值,则会返回默认值,handleToken() 方法会在 GenericTokenParser 的 parse() 方法中进行回调。
该类主要是根据属性名去属性集合中获取值。接下来看下 GenericTokenParser 类
GenericTokenParser
在上述的 PropertyParser 类的 parse() 方法中创建了该类的对象,并传入了占位符处理器 VariableTokenHandler
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
GenericTokenParser 类是一个通用的占位符解析器,如解析 #{} 和 ${}等占位符,它的 parse() 方法会按照顺序查找占位符的开始标记和结束标记以及解析得到的占位符的字面值,然后将其交给占位符处理器 VariableTokenHandler进行处理,即执行 handleToken() 方法
接下来看下该类的源码:
public class GenericTokenParser {
// 占位符的开始标记
private final String openToken;
// 占位符的结束标记
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;
}
// 解析 ${name} 之类的字符串
public String parse(String text) {
// 省略.........
final StringBuilder builder = new StringBuilder();
// 调用占位符的 handleToken 方法处理
builder.append(handler.handleToken(expression.toString()));
// 省略.........
return builder.toString();
}
}
假如配置文件如下:
<properties>
<property name="username" value="root"/>
<property name="password" value="root"/>
<!-- 启动默认值 -->
<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
</properties>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username:root}"/><!-- 使用默认值 -->
<property name="password" value="${password}"/>
</dataSource>
到这里,再来看看 PropertyParser 的 parse 方法:
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
现在要去获取属性集合中获取 username 对应的值,如果找不到,则会返回默认值 root。
到这里,解析配置文件大部分的逻辑已经完了,现在还有一个 XNode 类,表示文档中节点,可以忽略不看,知道它是对Java 的 Node 类的一个包装,通过它可以获取到节点的属性,子节点,父节点等就可以了。
栗子
在开篇举了个栗子,现在来看下它的一个流程,UML 图如下:
当文档加载到 XPathParser 类中形成一个 Document 对象,现在要去获取某个属性的值,首先会 通过 XPath 获取到属性值,之后通过PropertyParser的 parse() 方法获取值,在 该方法中,会通过 GenericToenParser 去解析占位符,之后得到一个字面值字符串属性,然后在 通过 VariableTokenHandler的 handleToken 方法 去属性集合里面查找对应的值,如果找不到对应的值且开启了默认值的话,就会返回默认值,最终会得到属性对应的值。这就是在文档里面获取对应值的一个过程。
以上就是 Mybatis 解析配置文件的工具吧。