映射文件解析完成后,并不意味着整个解析过程就结束了。此时还需要通过命名空间绑定 mapper 接口,这样才能将映射文件中的 SQL 语句和 mapper 接口中的方法绑定在一起,后续即可通过调用 mapper 接口方法执行与之对应的 SQL 语句。下面我们来分析一下 mapper 接口的绑定过程。
// -☆- XMLMapperBuilder private void bindMapperForNamespace() { // 获取映射文件的命名空间 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { // 根据命名空间解析 mapper 类型 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { } if (boundType != null) { // 检测当前 mapper 类是否被绑定过 if (!configuration.hasMapper(boundType)) { configuration.addLoadedResource("namespace:" + namespace); // 绑定 mapper 类 configuration.addMapper(boundType); } } } } // -☆- Configuration public <T> void addMapper(Class<T> type) { // 通过 MapperRegistry 绑定 mapper 类 mapperRegistry.addMapper(type); } // -☆- MapperRegistry public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { /* * 将 type 和 MapperProxyFactory 进行绑定, * MapperProxyFactory 可为 mapper 接口生成代理类 */ knownMappers.put(type, new MapperProxyFactory<T>(type)); // 创建注解解析器。在 MyBatis 中,有 XML 和 注解两种配置方式可选 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 解析注解中的信息 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }以上就是 Mapper 接口的绑定过程。这里简单一下:
获取命名空间,并根据命名空间解析 mapper 类型
将 type 和 MapperProxyFactory 实例存入 knownMappers 中
解析注解中的信息
以上步骤中,第3步的逻辑较多。如果大家看懂了映射文件的解析过程,那么注解的解析过程也就不难理解了,这里就不深入分析了。好了,Mapper 接口的绑定过程就先分析到这。
2.3 处理未完成解析的节点在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前节点解析工作无法进行下去。对于这种情况,MyBatis 的做法是抛出 IncompleteElementException 异常。外部逻辑会捕捉这个异常,并将节点对应的解析器放入 incomplet* 集合中。这个我在分析映射文件解析的过程中进行过相应注释,不知道大家有没有注意到。没注意到也没关系,待会我会举例说明。下面我们来看一下 MyBatis 是如何处理未完成解析的节点。
// -☆- XMLMapperBuilder public void parse() { // 省略部分代码 // 解析 mapper 节点 configurationElement(parser.evalNode("/mapper")); // 处理未完成解析的节点 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }如上,parse 方法是映射文件的解析入口。在本章的开始,我贴过这个源码。从上面的源码中可以知道有三种节点在解析过程中可能会出现不能完成解析的情况。由于上面三个以 parsePending 开头的方法逻辑一致,所以下面我只会分析其中一个方法的源码。简单起见,这里选择分析 parsePendingCacheRefs 的源码。下面看一下如何配置映射文件会导致 <cache-ref> 节点无法完成解析。
<!-- 映射文件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> 节点。具体的逻辑如下:
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 从集合中删除 */ } } } }上面代码不是很长,我也做了比较多的注释,应该不难理解。好了,关于未完成解析节点的解析过程就分析到这。
3.总结