2.2.将 Mybatis 全局配置文件对应的 DOM 转换为 XNODE 对象
在上文中我们完成了XmlConfigBuilder
对象的构建工作,准备好了解析XML
文件的基础环境。
所以接下来就是调用XmlConfigBuilder
暴露的parse()
方法来完成mybatis配置文件的解析工作了。
public Configuration parse() {
if (parsed) {
// 第二次调用XMLConfigBuilder
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 重置XMLConfigBuilder的解析标志,防止重复解析
parsed = true;
// 此处开始进行Mybatis配置文件的解析流程
// 解析 configuration 配置文件,读取【configuration】节点下的内容
parseConfiguration(parser.evalNode("/configuration"));
// 返回Mybatis的配置实例
return configuration;
}
在没有解析过的前提下,mybatis会调用parseConfiguration(XNode root)
方法来完成Configuration
对象的构建操作。
parseConfiguration(XNode root)
方法的入参是一个XNode
类型的对象实例,该对象的产生是通过调用我们上文创建的XPathParser
的XNode evalNode(String expression)
方法来完成的。
parseConfiguration(parser.evalNode("/configuration"));
evalNode
方法接收的是一个XPath地址表达式,字符串"/configuration"
中的/
表示从根节点获取元素,所以"/configuration"
则表示获取配置文件的根元素configuration
.
configuration
是Mybaits主配置文件的根节点,我们通常这样使用:<?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> ... </configuration>
/**
* 根据表达式解析Document对象获取对应的节点
*
* @param expression Xpath地址表达式
* @return XNode
*/
public XNode evalNode(String expression) {
// 从document对象解析出指定的节点
return evalNode(document, expression);
}
在evalNode
方法中,将上文获取到的XPathParser
的类属性document
作为参数,传递给他的重载方法:
/**
* 根据表达式获取节点
* @param root 根节点
* @param expression xpath表达式
* @return XNode
*/
public XNode evalNode(Object root, String expression) {
// 获取DOM节点
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
// 包装成XNode节点
return new XNode(this, node, variables);
}
在重载的evalNode
方法内部,将获取表达式对应的DOM节点的工作委托给了evaluate
方法来完成,如果解析出了对应的DOM节点,将会以XPathParser
对象本身和解析出来的DOM节点对象,以及用户传入的变量作为参数构造出一个XNode
对象实例返回给方法的调用方。
被委托的evaluate
方法利用XPath
解析器来完成将表达式解析成指定对象的工作。
private Object evaluate(String expression, Object root, QName returnType) {
try {
// 在指定的上下文中计算XPath表达式,并将结果作为指定的类型返回。
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
在XNode
类中定义了六个常量,这六个常量的初始化赋值操作都是在XNode
节点的构造方法中完成的。
/**
* XNode
*
* @param xpathParser XPath解析器
* @param node 被包装的节点
* @param variables 用户传入的变量
*/
public XNode(XPathParser xpathParser, Node node, Properties variables) {
// 初始化节点对应的解析器
this.xpathParser = xpathParser;
// 初始化DOM 节点
this.node = node;
// 初始化节点名称
this.name = node.getNodeName();
// 初始化用户定义的变量
this.variables = variables;
// 解析节点中的属性配置
this.attributes = parseAttributes(node);
// 解析节点包含的内容
this.body = parseBody(node);
}
其中attributes
和body
属性的取值操作需要分别通过parseAttributes
和parseBody
方法来完成。
/**
* 解析节点中的属性值
*
* @param n 节点
* @return 属性集合
*/
private Properties parseAttributes(Node n) {
// 定义 Properties对象
Properties attributes = new Properties();
// 获取属性节点
NamedNodeMap attributeNodes = n.getAttributes();
if (attributeNodes != null) {
for (int i = 0; i < attributeNodes.getLength(); i++) {
Node attribute = attributeNodes.item(i);
// 针对每个属性的值进行一次占位符解析替换的操作
String value = PropertyParser.parse(attribute.getNodeValue(), variables);
// 保存
attributes.put(attribute.getNodeName(), value);
}
}
return attributes;
}
/**
* 解析节点中的内容
*
* @param node 节点
* @return 节点中内容
*/
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;
}
/**
* 获取CDATA节点和TEXT节点中的内容
*
* @param child 节点
*/
private String getBodyData(Node child) {
if (child.getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNodeType() == Node.TEXT_NODE) {
// 获取CDATA节点和TEXT节点中的内容
String data = ((CharacterData) child).getData();
// 执行占位符解析操作
data = PropertyParser.parse(data, variables);
return data;
}
return null;
}
这两个方法比较简单,唯一需要注意的就是在处理属性值和body内容的时候,调用了PropertyParser.parse(String string, Properties variables)
方法对属性值和body体中的占位符进行了替换操作。
关于PropertyParser
PropertyParser在mybatis中担任着一个替换变量占位符的角色。主要作用就是将
${变量名}
类型的占位符替换成对应的实际值。PropertyParser
只对外暴露了一个String parse(String string, Properties variables)
方法,该方法的作用是替换指定的占位符为变量上下文中对应的值,该方法有两个入参:一个是String
类型的可能包含了占位符的文本内容,一个是Properties
类型的变量上下文。/** * 替换占位符 * @param string 文本内容 * @param variables 变量上下文 */ public static String parse(String string, Properties variables) { // 占位符变量处理器 VariableTokenHandler handler = new VariableTokenHandler(variables); // 占位符解析器 GenericTokenParser parser = new GenericTokenParser("${", "}", handler); // 返回闭合标签内的内容 return parser.parse(string); }
在
parse
方法中涉及到了两个类,VariableTokenHandler
和GenericTokenParser
.VariableTokenHandler
是TokenHandler
接口的一个实现类,TokenHandler
定义了一个String handleToken(String content);
方法,该方法主要用来对客户端传入的内容进行一些额外的处理。TokenHandler
也是策略模式的一种体现,它定义了文本统一处理的接口,其子类负责提供不同的处理策略。具体到
VariableTokenHandler
中,该方法的作用就是替换传入的文本内容中的占位符。VariableTokenHandler
的构造方法需要一个Properties
类型的variables
参数,该参数中定义的变量将用于替换占位符。VariableTokenHandler
的占位符解析操作允许用户以${key:defaultValue}
的形式为指定的key
提供默认值,即如果变量上下文中没有匹配key
的变量值,则以defaultValue
作为key
的值。占位符中取默认值时使用分隔符默认是
:
,如果需要修改,可以通过在variables
参数中添加org.apache.ibatis.parsing.PropertyParser.default-value-separator="自定义分隔符"
进行配置。在占位符中使用默认值的操作默认是关闭的,如果需要开启,可以在
variables
参数中添加org.apache.ibatis.parsing.PropertyParser.enable-default-value=true
进行配置。下面是
VariableTokenHandler
的构造方法:private VariableTokenHandler(Properties variables) { this.variables = variables; // 是否允许使用默认值比如${key:aaaa} this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); // 默认值分隔符 this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); }
GenericTokenParser
是一个通用的占位符解析器,他的构造方法有三个入参,分别是占位符的开始标签,结束标签,以及针对占位符内容的处理策略对象。/** * GenericTokenParser * @param openToken 开始标签 * @param closeToken 结束标签 * @param handler 内容处理器 */ public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; }
GenericTokenParser
对外提供了一个parse(String text)
方法,该方法将会寻找匹配占位符的内容并调用TokenHandler
对其进行处理,如果未匹配到占位符对应的内容,则返回始原内容。
在完成XNode
对象的创建工作之后,就可以使用该对象调用parseConfiguration(XNode root)
方法来进行真正的配置文件解析操作了:
parseConfiguration(parser.evalNode("/configuration"));
在解析配置文件之前,我们先简单了解一下mybatis全局配置文件的DTD定义:
<!ELEMENT configuration (
properties?
, settings?
, typeAliases?
, typeHandlers?
, objectFactory?
, objectWrapperFactory?
, reflectorFactory?
, plugins?
, environments?
, databaseIdProvider?
, mappers?
)>
参考上面的DTD文件,我们可以发现configuration
节点下允许出现11种类型的子节点,这些节点都是可选的,这就意味着Mybatis的全局配置文件可以不配置任何子节点(参考单元测试:org.apache.ibatis.builder.XmlConfigBuilderTest#shouldSuccessfullyLoadMinimalXMLConfigFile
)。
回头继续看方法parseConfiguration
,在该方法中,对应着configuration
的子节点,解析配置的工作被拆分成了多个子方法来完成:
/**
* 解析Configuration节点
*/
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 加载资源配置文件,并覆盖对应的属性[properties节点]
propertiesElement(root.evalNode("properties"));
// 将settings标签内的内容转换为Property,并校验。
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 根据settings的配置确定访问资源文件的方式
loadCustomVfs(settings);
// 根据settings的配置确定日志处理的方式
loadCustomLogImpl(settings);
// 别名解析
typeAliasesElement(root.evalNode("typeAliases"));
// 插件配置
pluginElement(root.evalNode("plugins"));
// 配置对象创建工厂
objectFactoryElement(root.evalNode("objectFactory"));
// 配置对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 配置反射工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 通过settings配置初始化全局配置
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 加载多环境源配置,寻找当前环境(默认为default)对应的事务管理器和数据源
environmentsElement(root.evalNode("environments"));
// 数据库类型标志创建类,Mybatis会加载不带databaseId以及当前数据库的databaseId属性的所有语句,有databaseId的
// 语句优先级大于没有databaseId的语句
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 注册类型转换器
typeHandlerElement(root.evalNode("typeHandlers"));
// !!注册解析Dao对应的MapperXml文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
在上面代码中多次出现了root.evalNode(String)
方法,该方法的作用是:根据传入的表达式,获取到对应的XNode
节点对象。
public XNode evalNode(String expression) {
return xpathParser.evalNode(node, expression);
}
具体的实现实际上是委托给了xpathParser
解析器的XNode evalNode(Object root, String expression)
方法来完成。
/**
* 根据表达式获取节点
* @param root 根节点
* @param expression xpath表达式
* @return XNode
*/
public XNode evalNode(Object root, String expression) {
// 获取DOM节点
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
// 包装成XNode节点
return new XNode(this, node, variables);
}
该方法我们在上文中已经看过了,这里不再赘述,现在以propertiesElement(root.evalNode("properties"));
为例,解释一下xpath
表达式"properties"
的作用: 该表达式表示获取properties
元素及其所有子元素。