mybatis源码解析系列源码基于 3.5.2-SNAPSHOT 版本,截至笔者开始写第一篇源码解析时这是官方的最新master分支。
写文章的过程中参考了很多大佬的文章和思路,以及一些为了解释清楚所必须的图。如有侵权,请联系删除。
参考内容:芋道源码博客 《MyBatis技术内幕》以及其他很多文章及书籍,以及开源项目,不一一列举。
引言:在之前的项目结构概览里提到了初始化项目的时候解析 mybatis-config.xml
文件,其中解析操作就是解析器模块来实现的。解析器模块还可以为映射配置文件提供支持,以及处理动态SQL语句中的占位符。
先看一下 pasring
包下的文件:
前面说了mybatis解析xml 是基于 XPath 封装的,所以很显然XPathParser就是解析 xml 文件的了,先拿这个开始入手。
贴一下XPathParser的属性:
// XML 文档对象,例如 mybatis-config.xml *Mapper.xml 等
private final Document document;
//是否校验 XML ,一般为 true
private boolean validation;
// XML 实体解析器
private EntityResolver entityResolver;
//变量 properties 对象
private Properties variables;
//JDK XPath 对象
private XPath xpath;
这里面不太好理解的就是 EntityResolver
了,不是说XPathParser就是解析 xml 的吗?怎么这里又来了一个什么鬼实体解析器。XML是有一套约束用来校验文件的,而校验XML文件时会基于开始位置指定的 DTD / XSD 文件来校验。而这里指定的都是网络位置。当服务部署在网络环境不好或或者内网环境下时,就会无法下载约束文件,导致校验失败。所以 MyBatis 自定义了 EntityResovler 实现,用于加载本地的DTD文件。
veriables 主要是用于记录 mybtais-config.xml
中的变量的键值对,比如在 xml 文件中定义了 ${username} 等变量,就由此字段来保存具体值。
xml的构造方法有16个,不过除了参数不太一样之外,其他基本上是差不多的,这里拿下面的构造方法做一个示例讲解:
/**
* 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);
//解析XML文件到 document 对象
this.document = createDocument(new InputSource(new StringReader(xml)));
}
公用构造方法 org.apache.ibatis.parsing.XPathParser#commonConstructor
如下:
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
//赋值
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
//创建XPathFactory实例
XPathFactory factory = XPathFactory.newInstance();
//实例化xpath对象
this.xpath = factory.newXPath();
}
创建 document 对象方法 org.apache.ibatis.parsing.XPathParser#createDocument
如下:
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
//创建 DocumentBuilderFactory
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//设置是否校验xml
factory.setValidating(validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
//创建 DocumentBuilder
DocumentBuilder builder = factory.newDocumentBuilder();
//设置 xml 实体解析器
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 {
}
});
//解析 xml 文件
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
以上就是 XPathParser 的构造方法了,虽然有贴代码的嫌疑,但其实底层都是java解析xml的代码,和mybatis的关系不大,所以就不详解了,后续有可能的话去写一篇博客,其实很多框架都涉及xml解析校验这些,比如 Spring。
eval*
方法有很多个,用于解析 Boolean , String,Integer,Node等类型的元素或节点值的值。
eval* 方法最后调用的都是 org.apache.ibatis.parsing.XPathParser#evaluate
。下面对方法做一个简单解析:
/**
* 获取指定元素或值
* @param expression 表达式
* @param root 指定节点
* @param returnType 返回类型
* @return
*/
private Object evaluate(String expression, Object root, QName returnType) {
try {
//xpath api获取指定元素节点或值
//javax.xml.xpath.XPath.evaluate(java.lang.String, java.lang.Object, javax.xml.namespace.QName)
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
这个方法和其他方法不太相同的是在调用完 evaluate()
方法之后会调用 org.apache.ibatis.parsing.PropertyParser#parse
处理节点中相应的默认值。
public String evalString(Object root, String expression) {
String result = (String) evaluate(expression, root, XPathConstants.STRING);
//如果result是动态值,则基于 variables 替换动态值,也就是mybatis如何替换动态值的实现方式
result = PropertyParser.parse(result, variables);
return result;
}
调用
evaluate
方法获取到值,然后调用org.apache.ibatis.parsing.PropertyParser#parse
基于 variables 替换动态值。在 parse 方法中会创建 GenericTokenParser 解析器,将默认值委托给org.apache.ibatis.parsing.GenericTokenParser#parse
方法来处理,方法会顺序查找openToken和closeToken,解析得到其占位符的字面值,并将其交给TokenHadler处理,然后将解析结果拼装成字符串并返回。因为代码略长,就不贴代码了。
org.apache.ibatis.parsing.PropertyParser#parse
public static String parse(String string, Properties variables) {
//创建 VariableTokenHandler
VariableTokenHandler handler = new VariableTokenHandler(variables);
//创建 GenericTokenParser,并指定其处理的占位符格式为 ${}
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
//执行解析
return parser.parse(string);
}
XPathParser还有一个属性就是 EntityResolver了,前面说到这个是用来加载本地的DTD文件的。但是这是一个接口,里面只有一个 resolveEntity()
。按照惯例,需要找其默认实现来看看里面到底有什么套路。默认实现是XMLMapperEntityResolver 。类上面的注释说这个是 MyBatis DTD的离线实体解析器,嗯,应该就是它了。
属性值如下,制定了配置文件的 DTD 和 mapper.xml文件的 DTD 位置。文件就在builer包下,感兴趣的可以去看一下。
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";
//mybatis-3-config.dtd 文件本地位置
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
//mybatis-3-mapper.dtd 文件本地位置
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
解析的代码相对比较简单,可以使用 org.apache.ibatis.autoconstructor.AutoConstructorTest#fullyPopulatedSubject
debug看一下怎么解析的,其实前面提到的createDocument()方法里 builder.parse()
方法就有调用这个方法。
前面的代码里提到了token这个东西,这里解释一下token是个啥。在《MyBatis技术内幕》里,给的解释是占位符。debug
org.apache.ibatis.parsing.GenericTokenParser#parse
时,openToken和closeToken的值分别是 ${ 和 } 。是不是很熟悉?其实就是占位符而已。
GenericTokenParser 这个看名字是通用Token解析器,那不通用的呢?不通用的由TokenHandler来处理。GenericTokenParser主要做的事情就是将openToken和closeToken之前的字符串取出来,然后交由handler处理,再拼接到一块。这个处理的handler就是由 org.apache.ibatis.parsing.PropertyParser.VariableTokenHandler#handleToken
来完成的。
org.apache.ibatis.parsing.PropertyParser.VariableTokenHandler#handleToken
前面说到这个方法可以替换动态值,实现逻辑是什么样的呢?
public String handleToken(String content) {
//variables 不为空
if (variables != null) {
//key为content
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());
}
//有默认值,在variables中查找指定的占位符
if (defaultValue != null) {
//在variables中查找指定的占位符
return variables.getProperty(key, defaultValue);
}
}
//未开启默认值,直接替换
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
//variables 为空,在content的前后加上${和}
return "${" + content + "}";
}
}
TokenHandler与 parsing
包相关的实现只有 org.apache.ibatis.parsing.PropertyParser.VariableTokenHandler。这个主要是用来替换动态值的,是 PropertiesParser 的内部类,其中的handleToken方法逻辑已经在上面写了详细的注释,这里不再重复。
parsing包的解析差不多就到这里了,其实还有部分代码和方法没有去写解析,但是主线和比较重要的部分这里已经大致说了一些。比如动态值替换是怎么实现的。源码解析也不一定是每一部分都要实现,如果每一步都去解析一遍,就可以考虑自己造个轮子了。