Mybatis源码之美:3.2.Mybatis中跨命名空间的缓存引用

1,138

Mybatis中跨命名空间的缓存引用

我们先看一下mapper元素的DTD定义:

<!ELEMENT mapper (cache-ref | cache | resultMap* | parameterMap* | sql* | insert* | update* | delete* | select* )+>
<!ATTLIST mapper
namespace CDATA #IMPLIED
>

按照上面的DTD定义来看,mapper元素有九个可用的顶级子元素,除了用于配置缓存的cache-refcache两个元素只允许配置一个以外,其余的元素都可以配置多个。

下面是这九个元素的名称及其作用的简单描述:

  • cache-ref 配置引用其他命名空间的缓存.
  • cache 配置缓存.
  • resultMap 配置返回结果映射集合,定义如何将数据库的列值转换为java对象.
  • parameterMap 配置请求参数映射集合,该参数目前已经弃用了.
  • sql 定义SQL代码块,可以用来被其他语句引用.
  • insert 定义插入语句
  • update 定义更新语句
  • delete 定义删除语句
  • select 定义查询语句

接下来的一段时间,我们将会在梳理解析这些元素的过程中度过。

命名空间的解析

XMLMapperBuilderconfigurationElement()方法中,做的第一件事就是获取当前mapper文件的namespace命名空间,并将其交给MapperBuilderAssistant缓存起来。

// 获取当前配置文件的命名空间(工作空间),通常这个值我们会设置为DAO操作类的全限定名称
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
}
// 配置当前的命名空间(工作空间)
builderAssistant.setCurrentNamespace(namespace);

前面说过namespace的值非常重要,他用来在整个Mybatis环境中唯一标志一个mapper元素,同时mapper子元素唯一标志的生成也依赖于该值,正是因为namespace的存在,Mybatis才实现了跨Mapper引用的功能。

namespace属性的解析操作并不复杂只是简单的取值赋值操作而已。

缓存引用的解析

在获取namespace参数之后,XMLMapperBuilder将会调用cacheRefElement()方法来解析cache-ref元素:

/**
 * 解析cache-ref 节点
 *
 * @param context cache-ref节点
 */
private void cacheRefElement(XNode context) {
    if (context != null) {
        // 处理引用其他命名空间的缓存
        // 添加缓存引用
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        // 构建缓存引用对象
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            // 解析引用的缓存配置,为builderAssistant设置使用的缓存实例
            cacheRefResolver.resolveCacheRef();
        } catch (IncompleteElementException e) {
            // 如果引用的缓存不存在(可能是还未加载),那么将会添加一个未完成的引用标记,之后会调用该方法完成补偿
            // 添加一个尚未完成的缓存引用标记
            configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}

cache-ref元素在Mybatis中用于引用其他命名空间的缓存对象,关于缓存对象,我们会在后面的文章中详细介绍。

cache-ref元素的DTD定义如下:

<!ELEMENT cache-ref EMPTY>
<!ATTLIST cache-ref
namespace CDATA #REQUIRED
>

cache-ref只有一个必填的namespace属性,该属性的值就是一个常规意义上的namespace,他指向一个mapper文件,因为一个mapper中只能有一个生效的缓存配置,所以我们只需要通过namespace就可以定位到一个唯一的缓存配置上。

Configuration对象中有一个类型为Map<String,String>cacheRefMap属性,负责维护mappermapper之间的缓存引用关系。

需要注意的是,mybatis并没有针对缓存的循环引用做特殊处理,因此在mybatis中可以出现MapperA引用MapperB缓存的同时MapperB也在引用了MapperA的缓存这种场景,针对这种场景,在解析完cache元素后,会进行了解。

cacheRefElement()在拿到被引用缓存的namespace之后,就会调用ConfigurationaddCacheRef()方法注册当前命名空间和被引用缓存的命名空间之间的关系。

之后使用当前的映射器构建助手MapperBuilderAssistant和被引用缓存对应的namespace参数生成一个缓存引用解析器CacheRefResolver的实例,来完成后续的缓存引用工作。

因为被引用的缓存可能会晚于当前cache-ref元素加载,因此cacheRefElement()方法会将无法完成解析缓存引用工作的CacheRefResolver的实例,注册到Configuration对象的incompleteCacheRefs集合中,便于在后面再次解析和使用。

/**
 * 未完成处理的缓存引用
 */
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();

缓存引用解析器CacheRefResolver是一个相对比较简单的对象:

/**
 * 缓存引用解析器
 *
 * @author Clinton Begin
 */
public class CacheRefResolver {
    /**
     * 映射器构建助手
     */
    private final MapperBuilderAssistant assistant;
    /**
     * 被引用的缓存命名空间
     */
    private final String cacheRefNamespace;

    public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
        // Mapper文件解析助手
        this.assistant = assistant;
        // 引入的缓存名称
        this.cacheRefNamespace = cacheRefNamespace;
    }
    /**
     * 解析引用的缓存
     *
     * @return 缓存
     */
    public Cache resolveCacheRef() {
        // 委托给缓存引用助手完成缓存引入的配置
        return assistant.useCacheRef(cacheRefNamespace);
    }
}

唯一值得一看的负责解析引用缓存的方法resolveCacheRef(),还把真正的解析操作委托给了MapperBuilderAssistantuseCacheRef(String namespace)方法来完成。

MapperBuilderAssistant中有两个关于缓存的属性定义,类型为CachecurrentCache属性用来记录当前命名空间中将会使用的缓存对象,类型为booleanunresolvedCacheRef属性则负责记录当前命名空间的缓存使用是否已经被解析,默认值是false

MapperBuilderAssistantuseCacheRef(String namespace)方法在实现上,会尝试从Configuration维护的缓存注册表caches中取出被引用命名空间的缓存配置对象赋值给currentCache属性。

/**
 * 为当前的映射器配置一个缓存引用
 *
 * @param namespace 被引用缓存的全局唯一标志
 * @return 缓存
 */
public Cache useCacheRef(String namespace) {
    // 校验
    if (namespace == null) {
        throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
        unresolvedCacheRef = true;
        // 获取待引用的缓存
        Cache cache = configuration.getCache(namespace);
        if (cache == null) {
            // 引用解析失败
            throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
        }
        currentCache = cache;
        unresolvedCacheRef = false;
        // 返回缓存
        return cache;
    } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
}

这里有一点需要注意的是unresolvedCacheRef属性,该属性默认是false,当调用useCacheRef()方法之后,该属性被刷新为true,直到成功获取被引用的缓存对象,不然抛出IncompleteElementException这个操作会一直中断将unresolvedCacheRef刷新为false的操作。

整个cache-ref元素的解析过程:

@startuml
hide footbox
participant XMLMapperBuilder as x
participant Configuration as c
participant MapperBuilderAssistant as mba
[-> x: cacheRefElement() \n 解析缓存引用
activate x
x->c++:addCacheRef() \n注册缓存引用关系
return
create participant CacheRefResolver as crr
x->crr++:resolveCacheRef() \n解析引用的缓存配置
crr->mba++:useCacheRef() \n委托给缓存引用助手完成缓存引入的配置
mba->c++:getCache() \n获取待引用的缓存
return 待引用的缓存
return  待引用的缓存
return

opt 无法获取待引用的缓存
x->c++:addIncompleteCacheRef() \n添加一个尚未完成的缓存引用标记
return
end
[<-x: 完成解析工作
@enduml

cache-ref元素的解析过程

关注我,一起学习更多知识

关注我