摘要
上篇文章主要讲解了加载mybaits主配置文件的过程。本篇文章会接下去分析,Myabtis是如何加载mapper.xml配置文件的。
加载mapper配置文件
进入源码之前我们先认识几个类,先看下这个类图
- SqlSouce: 接口,提供获取boundSql的方法,一条完整的sql语句,对应一个SqlSource对象
- RawSqlSource:用来封装解析只包含#{}或者纯sql的sql标签内容
- DynamicSqlSource: 用来封装和解析带有${}和动态sql标签的sql语句
- PrivoderSqlSource: 用来封装和解析注解类型的sql语句
- StaticSqlSource: 用来封装可以让JDBC直接执行的sql语句,上面几种SqlSource解析完成后就会生成一个StaticSqlSource对象
还有一个接口需要说明-SqlNode,在解析一个sql的过程中可能会有很多个动态标签,而每个标签会封装成一个对应的SqlNode添加到SqlSource中.
接着通过上节课说的XMLMapperBuilder 的parse方法解析mapper.xml配置文件,我们来看下具体的parse方法.
// mapper映射文件是否已经加载过
if (!configuration.isResourceLoaded(resource)) {
// 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕
configurationElement(parser.evalNode("/mapper"));
// 解析过的配置文件会添加到一个set集合中,避免重复解析
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
这里面configurationElement方法就行用来解析文件的具体方法
/**
* 解析mapper.xml映射文件
* @param context 映射文件根节点<mapper>对应的XNode
*/
private void configurationElement(XNode context) {
try {
// 获取<mapper>标签的namespace值
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置当前命名空间namespace的值
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql标签-就是对动态sql的重用,将写好的动态sql提取出来,然后在需要的地方进行调用
sqlElement(context.evalNodes("/mapper/sql"));
// 解析<select>\<insert>\<update>\<delete>子标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
其中buildStatementFromContext方法才是我们想看的去解析各个sql标签的具体方法
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// MappedStatement解析器
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析select等4个标签,创建MappedStatement对象
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
这里又有一个专门的解析类去将这个Sql解析成MappedStatement对象,MappedStatement对象封装着整个标签的信息,包括我们后面解析出来的SqlSource 具体的解析细节过程,大家点进去可以自己看到,我们拿几个点讲下
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
这个过程是判断当前的数据源跟配置的数据源是否一直,不一致就不继续解析,而后面最主要的是需要看下SqlSource的解析过程
// 创建SqlSource,解析SQL
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
其中具体的解析过程在XMLLanguageDriver中通过XMLScriptBuilder的parseScriptNode方法解析
public SqlSource parseScriptNode() {
// 获取解析过的SQL信息(解析动态SQL标签和${}),但是并没有对#{}进行处理
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
// 如果包含${}和动态SQL语句,就是dynamic的
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 否则是RawSqlSource的(带有#{}的SQL语句)
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
接下来我们看下parseDynamicTags方法是怎么做的
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
//获标签的子节点,子节点包括元素节点和文本节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
// 判断是否为文本节点
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
// 将文本内容封装到SqlNode中
TextSqlNode textSqlNode = new TextSqlNode(data);
// ${}是dynamic的
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 除了${}都是static的,包括下面的动态SQL标签
contents.add(new StaticTextSqlNode(data));
}
//处理元素节点
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
// 动态SQL标签处理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
// 动态SQL标签是dynamic的
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
TextSqlNode封装含有${}的Sql文本,StaticTextSqlNode封装只有#{}或者纯sql的文本信息,其他动态标签会封装到对应的动态标签节点的SqlNode中,而contents是所有SqlNode的节点集合,而解析动态标签的Sql信息的时候会通过对应的标签从nodeHandlerMap获取对应标签的解析器去解析动态标签内的Sql信息.而nodeHandlerMap实在构造函数中调用initNodeHandlerMap方法进行初始化.
现在我们只看其中的一个动态标签处理器(IfHandler),其他的大概类似。
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
//递归解析该动态标签下的Sql语句
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//获取If标签中的Ognl表达式
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
在处理过程中就是递归调用解析方法去解析动态标签下面的Sql信息,然后将对应的SqlNode集合和if标签中的Ognl表达式封装成一个IfSqlNode。其他动态标签的解析夜大概类似。 最终解析完将对应的SqlSource返回然后封装成一个MappedStatement对象,添加到Configuration的Map容器中
// 通过构建者助手,创建MappedStatement对象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
而最后这边Mybatis通过构建者助手,里面使用构造方法是创建MappedStatement对象,里面具体的构建过程,这里就不赘述,感兴趣的同学可以去点进去看下具体的构建过程.至此映射文件的解析已经全部完毕,所有信息都封装在Configuration的mappedStatements中,在后面执行的时候就会通过这个id(最终在构建的时候会id的拼接方式是(currentNamespace + "." + id)也就是加上空间名称用于区分)获取对应的MappedStatement拿到对应下信息进行数据库查询。
下一节,我们再一起去看下mybatis具体的执行过程,希望大家多多关注,或者有什么问题也可以留言探讨。