通过源码分析MyBatis如何完成XML配置文件的解析
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/77/ )。
MyBatis的配置文件与映射文件均是XML文件,那MyBatis是如何解析这些XML文件的呢?而且,XML文件中还存在“${ }
”等包裹的变量名,这些变量又是怎么被替换的呢?
本文我们就通过MyBatis的源码详细分析这一过程。
相关内容参考《通用源码阅读指导书——MyBatis源码详解》一书。
MyBatis中的parsing
包就是用来进行XML文件解析的包。在解析XML文件的过程,XPathParser
类与XNode
类是两个最为关键的类,下图给出了这两个类主要关系的类图。
通过图可以看出,XPathParser类中封装了“javax.xml.xpath.XPath
”类的对象。《通用源码阅读指导书——MyBatis源码详解》的11.1.2 XPath章节分析了XPath,XPath是XML解析的利器,因此XPathParser
类便具有了XML解析的能力。
下面代码给出了XPathParser
类的带注释的属性。
// 代表要解析的整个XML文档
private final Document document;
// 是否开启验证
private boolean validation;
// EntityResolver,通过它可以声明寻找DTD文件的方法,例如通过本地寻找,而不是只能通过网络下载DTD文件
private EntityResolver entityResolver;
// MyBatis配置文件中的properties节点的信息
private Properties variables;
// javax.xml.xpath.XPath工具
private XPath xpath;
有必要说明一下,上述“private Properties variables
”属性存储的内容就是MyBatis配置文件中properties
节点的信息。properties
节点会在解析配置文件的最开始就被解析,然后相关信息会被放入“private Properties variables
”属性并在解析后续节点时发挥作用,在《通用源码阅读指导书——MyBatis源码详解》的11.3 文档解析中的变量替换章节详细介绍了这一点。
XPathParser
存在多个重载的构造方法,它们均是根据传入的参数完成属性的初始化并构造出XML文档对应的Document对象。除去构造方法外,便是大量提供XML文档中节点解析功能的“eval*
”方法,这些方法最后都调用了如下所示的evaluate
方法。
/**
* 进行XML节点的解析
* @param expression 解析的语句
* @param root 解析根
* @param returnType 返回值类型
* @return 解析结果
*/
private Object evaluate(String expression, Object root, QName returnType) {
try {
// 对指定节点root运行解析语法expression,获得returnType类型的解析结果
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
在evaluate
方法中,使用“javax.xml.xpath.XPath
”对象进行了节点的解析。因此,整个XPathParser
类本质就是对“javax.xml.xpath.XPath
”的封装和调用,可以把XPathParser
类看作是javax.xml.xpath.XPath
类的包装类。
同样地,parsing
包中的XNode
类可以看作是“org.w3c.dom.Node
”类的包装类。“org.w3c.dom.Node
”类是用来表示DOM中节点的类,而XNode
类只是在“org.w3c.dom.Node
”类的基础上提取和补充了几个属性。下面给出了XNode
对象的属性。
// org.w3c.dom.Node 表示是XML中的一个节点
private final Node node;
// 节点名,可以从org.w3c.dom.Node中获取
private final String name;
// 节点体,可以从org.w3c.dom.Node中获取
private final String body;
// 节点的属性,可以从org.w3c.dom.Node中获取
private final Properties attributes;
// MyBatis配置文件中的properties信息
private final Properties variables;
// XML解析器XPathParser
private final XPathParser xpathParser;
XNode
对象的上述属性中,name、body、attributes这三个属性是从“org.w3c.dom.Node
”对象中提取出来的,variables、xpathParser这两个属性补充的。而我们知道XPathParser
类具有解析XML节点的能力,也就是说,XNode
类中封装了自身了的解析器。在一个类中封装自己的解析器,这是一种非常常见的做法,如此一来这个类不需要外界的帮助便可以解析自身,即获得了自解析能力。
大家可能会有过这样的经历:新安装的电脑上没有解压软件,于是从网络或者朋友那里得到了一份解压软件。可是,拿到手的解压软件安装包却是一个压缩文件。尚未安装解压软件的你必然没法打开压缩文件获得安装包。而自解压文件(SelF-eXtracting,简称SFX)能够帮助你摆脱这个困境。自解析类也有类似的有点,它减少了对外部类的依赖,具有更高的内聚性,也更为易用。
正是得益于XNode
类的自解析特性,它本身提供了一些“eval*
”方法,从而能够解析自身节点内的信息。
MyBatis配置文件中properties
节点会在解析配置文件的最开始就被解析,并在解析后续节点时发挥作用。可是如何才能让这些信息在XML文件的解析中发挥作用呢?
回到XPathParser
类,其“evalString(Object, String)
”方法如下所示。
/**
* 解析XML文件中的字符串
* @param root 解析根
* @param expression 解析的语句
* @return 解析出的字符串
*/
public String evalString(Object root, String expression) {
// 解析出字符串结果
String result = (String) evaluate(expression, root, XPathConstants.STRING);
// 对字符串中的属性进行处理
result = PropertyParser.parse(result, variables);
return result;
}
我们发现在解析字符串时,通过“PropertyParser.parse”方法对解析出来的结果进行了进一步处理。而这一步处理中,properties
节点的信息便发挥了作用。
PropertyParser类是属性解析器,与之关系密切的几个类的类图如图所示。
我们以GenericTokenParser
类为入口阅读图中所示的几个类。
GenericTokenParser
类是通用的占位符解析器,一共有三个属性,相关注释如下所示:
// 占位符的起始标志
private final String openToken;
// 占位符的结束标志
private final String closeToken;
// 占位符处理器
private final TokenHandler handler;
GenericTokenParser
类中有唯一的一个parse方法,该方法主要完成了占位符的定位工作,然后把占位符的替换工作交给了与其关联的TokenHandler
处理。我们通过一个例子对parse方法的功能进行介绍。
假设“openToken
=#{
”,“closeToken
=}
”,向GenericTokenParser
中的parse
方法传入的参数为“jdbc:mysql://127.0.0.1:3306/${dbname}?serverTimezone=UTC
”。则parse
方法会将被“#{
”和“}
”包围的dbname
字符串解析出来,作为入参传入handler
中的handleToken
方法,然后用handleToken
方法的返回值替换“${dbname}
”字符串。
GenericTokenParser
提供的占位符定位功能应用非常广泛,而不仅仅局限在XML解析中,毕竟它的名称是“通用的”占位符解析器。SQL语句的解析也离不开它的帮助。SQL语句中使用“#{ }
”或“${ }
”来设置的占位符也是依靠GenericTokenParser
来完成解析的,流程与本节介绍一样。
TokenHandler
是一个接口,如下面代码所示,它只定义了一个抽象方法handleToken。handleToken方法要求输入一个字符串,然后返回一个字符串。例如可以输入一个变量的名称,然后返回该变量的值。
public interface TokenHandler {
String handleToken(String content);
}
PropertyParser
类的内部类VariableTokenHandler
便继承了该接口。下面代码展示了VariableTokenHandler类的属性。
// 输入的属性变量,是HashTable的子类
private final Properties variables;
// 是否启用默认值
private final boolean enableDefaultValue;
// 如果启用默认值的化,键和默认值之间的分割符
private final String defaultValueSeparator;
了解了VariableTokenHandler类的属性后,我们阅读其handleToken方法,如下所示。向handleToken方法中传入入参后,该方法会以入参为键尝试从variables属性中寻找对应的值返回。在这个由键寻值的过程中还可以支持默认值。
/**
* 根据一个字符串,给出另一个字符串。多用在字符串替换等处
* 具体实现中,会以content作为键,从variables找出并返回对应的值
* 由键寻值的过程中支持设置默认值
* 如果启用默认值,则content形如"key:defaultValue"
* 如果没有启用默认值,则content形如"key"
* @param content 输入的字符串
* @return 输出的字符串
*/
@Override
public String handleToken(String content) {
if (variables != null) { // 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);
}
}
// 如果variables为null。不发生任何替换,直接原样还回
return "${" + content + "}";
}
最后我们再看PropertyParser中的静态方法parse,如下面代码所示。它做了以下几个工作将GenericTokenParser
提供的占位符定位功能和TokenHandler
提供的字符串替换功能串接在了一起:
- 创建一个VariableTokenHandler对象(TokenHandler接口子类的对象)。该对象能够从一个Properties对象(这里传入的是
properties
节点信息)中根据键索引一个值。 创建了一个属性解析器。只要设置了该属性解析器的要匹配的模式,它就能将指定模式的属性值定位出来,然后将其替换为TokenHandler接口中handleToken方法的返回值。
/** * 进行字符串中属性变量的替换 * @param string 输入的字符串,可能包含属性变量 * @param variables 属性映射信息 * @return 经过属性变量替换的字符串 */ public static String parse(String string, Properties variables) { // 创建负责字符串替换的类 VariableTokenHandler handler = new VariableTokenHandler(variables); // 创建通用的占位符解析器 GenericTokenParser parser = new GenericTokenParser("${", "}", handler); // 开展解析,即替换占位符中的值 return parser.parse(string); }
这样一来,只要在XML文件中使用“${
”和“}
”包围一个变量名,则该变量名就会被替换成properties
节点中对应的值。
通过以上过程,XML文件就被解析了,而且其中的变量名也会被替换为变量值。按照这种方式,我们也可以编写自己的XML解析器,用XML来配置自己开发的应用。
以上内容均参考《通用源码阅读指导书——MyBatis源码详解》一书。
这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,并且附带有示例项目源码,MyBatis的全中文注释。书籍还总结了大量的编程知识和架构经验,对提升编程和架构能力十分有用。推荐给大家。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。