Mybatis中跨命名空间的缓存引用
我们先看一下mapper
元素的DTD
定义:
<!ELEMENT mapper (cache-ref | cache | resultMap* | parameterMap* | sql* | insert* | update* | delete* | select* )+>
<!ATTLIST mapper
namespace CDATA #IMPLIED
>
按照上面的DTD
定义来看,mapper
元素有九个可用的顶级子元素,除了用于配置缓存的cache-ref
和cache
两个元素只允许配置一个以外,其余的元素都可以配置多个。
下面是这九个元素的名称及其作用的简单描述:
cache-ref
配置引用其他命名空间的缓存.cache
配置缓存.resultMap
配置返回结果映射集合,定义如何将数据库的列值转换为java
对象.parameterMap
配置请求参数映射集合,该参数目前已经弃用了.sql
定义SQL
代码块,可以用来被其他语句引用.insert
定义插入语句update
定义更新语句delete
定义删除语句select
定义查询语句
接下来的一段时间,我们将会在梳理解析这些元素的过程中度过。
命名空间的解析
在XMLMapperBuilder
的configurationElement()
方法中,做的第一件事就是获取当前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
属性,负责维护mapper
与mapper
之间的缓存引用关系。
需要注意的是,
mybatis
并没有针对缓存的循环引用做特殊处理,因此在mybatis
中可以出现MapperA
引用MapperB
缓存的同时MapperB
也在引用了MapperA
的缓存这种场景,针对这种场景,在解析完cache
元素后,会进行了解。
cacheRefElement()
在拿到被引用缓存的namespace
之后,就会调用Configuration
的addCacheRef()
方法注册当前命名空间和被引用缓存的命名空间之间的关系。
之后使用当前的映射器构建助手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()
,还把真正的解析操作委托给了MapperBuilderAssistant
的useCacheRef(String namespace)
方法来完成。
MapperBuilderAssistant
中有两个关于缓存的属性定义,类型为Cache
的currentCache
属性用来记录当前命名空间中将会使用的缓存对象,类型为boolean
的unresolvedCacheRef
属性则负责记录当前命名空间的缓存使用是否已经被解析,默认值是false
。
MapperBuilderAssistant
的useCacheRef(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