在上文中我们完成了 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
元素及其所有子元素。