在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前节点解析工作无法进行下去。对于这种情况,MyBatis的做法是抛出IncompleteElementException异常。外部逻辑会捕捉这个异常,并将节点对应的解析器放入 incomplet*集合中。

额外逻辑

1
2
3
4
5
6
7
8
9
10
11
// -☆- XMLMapperBuilder
public void parse() {
// 省略部分代码
// 解析 mapper 节点
configurationElement(parser.evalNode("/mapper"));

// 处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

从上面的源码中可以知道有三种节点在解析过程中可能会出现不能完成解析的情况。由于上面三个以 parsePending 开头的方法逻辑一致,所以下面只会分析其中一个方法的源码

实例:配置映射文件会导致cache-ref节点无法完成解析

1
2
3
4
5
6
7
8
9
10
11
<!-- 映射文件 1 -->
<mapper namespace="xyz.coolblog.dao.Mapper1">
<!-- 引用映射文件 2 中配置的缓存 -->
<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
</mapper>


<!-- 映射文件 2 -->
<mapper namespace="xyz.coolblog.dao.Mapper2">
<cache/>
</mapper>

MyBatis 先解析映射文件 1,然后再解析映射文件 2。按照这样的解析顺序,映射文件 1 中的cache-ref节点就无法完成解析,因为它所引用的缓存还未被解析

当映射文件 2 解析完成后,MyBatis会调用parsePendingCacheRefs方法处理在此之前未完成解析的cache-ref节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void parsePendingCacheRefs() {
// 获取 CacheRefResolver 列表
Collection<CacheRefResolver> incompleteCacheRefs =
configuration.getIncompleteCacheRefs();

// 线程安全
synchronized (incompleteCacheRefs) {
Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
// 通过迭代器遍历列表
while (iter.hasNext()) {
try {
// 尝试解析 <cache-ref> 节点,若解析失败,则抛出
// IncompleteElementException,此时下面的删除操作不会被执行
iter.next().resolveCacheRef();
// 移除 CacheRefResolver 对象。如果代码能执行到此处,
// 表明已成功解析了 <cache-ref> 节点
iter.remove();
} catch (IncompleteElementException e) {
// 如果再次发生 IncompleteElementException 异常,表明当前
// 映射文件中并没有<cache-ref>所引用的缓存。有可能所引用的缓存
// 在后面的映射文件中,所以这里不能将解析失败的 CacheRefResolver
// 从集合中删除
}
}
}
}

上面代码不是很长,逻辑也比较简单,这里简单总结一下:

  1. 获取获取 CacheRefResolver 列表,并进行遍历
  2. 尝试解析节点,若解析失败再次抛出异常
  3. 若解析成功则列表中移除相关节点