项目背景:mybatis3.4.1,mapper接口上没有任何注释,有两个对应的XXXMapper.xml和YYYMapper.xml文件,在xml文件中分别配置<cache/>和<cache-ref/>。
首先看下官方文档:<cache/>对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 <cache-ref/> 元素来引用另一个缓存。
那如果在同一个xxxMapper.xml文件同时配置<cache/>和<cache-ref/>呢?这时当前命名空间是独享缓存还是与引用的缓存共享?
这里先说结论:独享缓存还是共享缓存要根据mybatis解析XXXMapper.xml和YYYMapper.xml文件的次序!
【1】项目背景
① 两个mapper接口
接口有一些crud方法,没有任何注解
public interface DepartmentMapper { //... } public interface EmployeeMapper { //... }
② 两个xml配置文件
DepartmentMapper.xml文件缓存配置如下:
<mapper namespace="com.mybatis.dao.DepartmentMapper"> <!-- 引用缓存:namespace:指定和哪个名称空间下的缓存一样 --> <cache-ref namespace="com.mybatis.dao.EmployeeMapper"/> <cache/> //... </mapper>
这里可以看到同同时配置了<cache/>和<cache-ref/>
。
EmployeeMapper.xml缓存配置如下:
<mapper namespace="com.mybatis.dao.EmployeeMapper"> <cache /></cache> //... </mapper>
mybatis-config.xml中<mappers/>
标签如何配置呢?下面分析。
【2】流程解析
① 二级缓存创建过程
下图是创建SqlSessionFactory过程,这里主要描述了二级缓存的创建过程。
如上图流程图示,在解析mapper.xml文件时会先尝试解析<cache-ref/>
结点,然后解析<cache/>
。
MapperBuilderAssistant中解析<cache-ref/>
源码
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); } }
解释如下:
① 根据namespace尝试从configuration获取对应的cache实例
② 如果获取到,则设置当前MapperBuilderAssistant实例的currentCache为①中获取到的cache实例。否则走③
③ 如果没有获取到,则抛出异常IncompleteElementException 。该异常会被XMLMapperBuilder拦截,将未正常处理的CacheRefResolver放入configuration的Collection<CacheRefResolver> incompleteCacheRefs中
private void cacheRefElement(XNode context) { if (context != null) { configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try { cacheRefResolver.resolveCacheRef(); } catch (IncompleteElementException e) { configuration.addIncompleteCacheRef(cacheRefResolver); } } }
MapperBuilderAssistant中解析<cache/>
源码
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache = cache; return cache; }
解释如下
① 根据命名空间、实现类、缓存过期策略…等创建Cache实例
② 以namespace:cache实例这样的键值对放入configuration的成员变量中protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
③ 设置当前MapperBuilderAssistant实例的currentCache为①中获取到的cache实例
XMLMapperBuilder中parsePendingCacheRefs方法源码
private void parsePendingCacheRefs() { Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs(); synchronized (incompleteCacheRefs) { Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator(); while (iter.hasNext()) { try { iter.next().resolveCacheRef(); iter.remove(); } catch (IncompleteElementException e) { // Cache ref is still missing a resource... } } } }
解释如下:
① 获取configuration中Collection<CacheRefResolver> incompleteCacheRefs
② 循环遍历①中incompleteCacheRefs得到每一个CacheRefResolver,并调用resolveCacheRef方法
③ 走前面MapperBuilderAssistant中解析<cache-ref/>源码的过程
④ 如果③中没有正常处理,则从incompleteCacheRefs集合中移除当前元素;否则不移除。
OK,有了上面的分析,我们来对比不同<mappers>配置时缓存实例具体情况。
② EmployeeMapper在前
也就是说mybatis-config.xml中<mappers/>
标签配置如下:
<mappers> <mapper resource="com/mybatis/dao/EmployeeMapper.xml"/> <mapper resource="com/mybatis/dao/DepartmentMapper.xml"/> </mappers>
流程分析如下:
① 解析EmployeeMapper.xml,只有<cache/>结点
② 解析<cache-ref/>结点,不存在,则无影响
③ 解析cache结点,并将创建的cache实例放入configuration
④ 执行parsePendingCacheRefs方法,这时configuration中Collection<CacheRefResolver> incompleteCacheRefs为空,故而无影响。
⑤ 解析DepartmentMapper,有<cache/>和<cache-ref/>结点
⑥ 解析cache-ref结点,将DepartmentMapper的缓存实例引用指向③中创建的EmployeeMapper的缓存实例;
⑦ 解析<cache/>结点,DepartmentMapper为自己创建一个缓存实例不再引用③中创建的EmployeeMapper的缓存实例;
⑧ 执行parsePendingCacheRefs方法,这时configuration中Collection<CacheRefResolver> incompleteCacheRefs为空,故而无影响。
结论:DepartmentMapper与EmployeeMapper分别拥有自己的二级缓存Cache实例!
③ EmployeeMapper在后
也就是说mybatis-config.xml中<mappers/>
标签配置如下:
<mappers> <mapper resource="com/mybatis/dao/DepartmentMapper.xml"/> <mapper resource="com/mybatis/dao/EmployeeMapper.xml"/> </mappers>
流程分析如下:
① 解析DepartmentMapper,有<cache/>和<cache-ref/>结点
② 解析<cache-ref/>结点,此时不存在EmployeeMapper的缓存实例,故而抛出异常。将DepartmentMapper对应的CacheRefResolver放入configuration的incompleteCacheRefs集合中
③ 解析cache结点,并将创建的cache实例放入configuration
④ 执行parsePendingCacheRefs方法,这时configuration中Collection<CacheRefResolver> incompleteCacheRefs有一条数据,但是仍旧没有对应的EmployeeMapper的缓存实例。故而不做处理。
⑤ 解析EmployeeMapper.xml,只有<cache/>结点
⑥ 解析<cache-ref/>结点,不存在,则无影响
⑦ 解析cache结点,并将创建的cache实例放入configuration
⑧ 执行parsePendingCacheRefs方法,这时configuration中Collection<CacheRefResolver> incompleteCacheRefs有DepartmentMapper的CacheRefResolver,需要进行处理。会将DepartmentMapper的缓存引用指向EmployeeMapper的缓存实例。
结论:EmployeeMapper拥有自己的二级缓存Cache实例,DepartmentMapper的缓存引用指向EmployeeMapper的二级缓存Cache实例!
综上所述,永远不要在一个mapper.xml中同时配置<cache/>和<cache-ref/>标签(也永远不要在一个mapper接口类上面同时配置@CacheNamespace和@CacheNamespaceRef注解)