相关博文:
你尝试过在mybatis某个mapper上同时配置<cache/>和<cache-ref/>吗?
mybatis全局配置文件实例与详解
Mybatis中一级缓存和二级缓存使用详解
这里我们手动根据mybatis-config.xml来创建sqlsesionFactory,观察mybatis中二级缓存的创建过程。
【1】创建Cache的完整过程
mybatis创建二级缓存的时序图如下
① 获取sqlsesionFactorynew SqlSessionFactoryBuilder().build(reader)
我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始:
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
//这里会进入代码2片段
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
② SqlSessionFactoryBuilder.build
代码2片段new SqlSessionFactoryBuilder().build(reader);的实现
,方法属于SqlSessionFactoryBuilder类
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//这里会进入代码3片段
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
代码解释如下:
这里会根据mybatis的配置文件、其他环境配置等创建XMLConfigBuilder。XMLConfigBuilder是用来解析mybatis的配置文件,解析过程中会将“解析结果”放到Configuration的对应成员变量中
,然后返回Configuration实例。
这里需要注意将“解析结果”放到Configuration的对应成员变量中
,mybatis框架的基础就是Configuration实例中的一系列成员,后面会具体分析。
另外,创建XMLConfigBuilder实例时也创建了Configuration实例并进行了初始化
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
//这里创建Configuration实例
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
Configuration实例初始化代码如下:
public Configuration(Environment environment) {
this();
this.environment = environment;
}
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
③ XMLConfigBuilder.parse
源码如下:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//这个实现在下面方法
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
代码解释如下:
- 判断是否解析过
- 如果解析过,抛出异常
- 如果未解析则设置
parsed = true;
然后调用parseConfiguration(parser.evalNode("/configuration"));
- 返回configuration实例
④ XMLConfigBuilder.parseConfiguration
这是解析mybatis配置文件的具体方法,会根据拿到的mybatis配置文件解析每个结点。在解析mappers
结点时会获取每一个XXXXMapper.xml或者XXXMapper类进行解析获取一个个MappedStatement。
//解析mybatis-config.xml
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//处理系统中的Mapper.xml--⑤ XMLConfigBuilder.mapperElement
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
解释如下:
- 解析properties结点
- 解析settings结点
- 解析typeAliases结点
- 解析plugins结点
- 解析objectFactory结点
- 解析objectWrapperFactory结点
- 解析reflectorFactory结点
- 根据settings结点为configuration设置对应的成员变量
- 解析environments结点
- 解析databaseIdProvider结点
- 解析typeHandlers结点
- 解析mappers结点-这个也是我们着重关注的
⑤ XMLConfigBuilder.mapperElement
源码如下:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
//这里会遍历循环解析每一个mapper结点
for (XNode child : parent.getChildren()) {
//判断是否包注入
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//否则使用resource
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//创建mapperParser ,解析mapper xml
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
//是否使用url配置
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
//是否配置具体类
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
如下图所示,mappers中每一个mapper结点可以有如下方式配置。
这里我们实践的背景是mapper结点中使用resource配置了xml路径。那么我们继续跟踪如下代码:
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
解析如下:
- 创建XMLMapperBuilder实例,这里source就是你配置的XXXMapper.xml文件包路径
- 创建XMLMapperBuilder实例时也创建了MapperBuilderAssistant实例
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { super(configuration); this.builderAssistant = new MapperBuilderAssistant(configuration, resource); this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; }
- 创建XMLMapperBuilder实例时也创建了MapperBuilderAssistant实例
mapperParser.parse();
方法解析具体的xxxxMapper.xml文件
需要注意的是,XMLConfigBuilder、XMLMapperBuilder以及MapperBuilderAssistant拥有一个共同的抽象父类BaseBuilder:
⑥ XMLMapperBuilder.parse
源码如下:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
//下面代码可以理解为mybatis"后手",对前面不能确定的ResultMap、ChacheRef、Statement再次处理
parsePendingResultMaps();
//若是在某个地方看到了parsePendingChacheRefs(),也是正确的。这是mybatis框架开发工程师写错单词了;不过已经修正
parsePendingCacheRefs();
parsePendingStatements();
}
代码解释如下
- ① 判断当前资源是否被加载。通过判断configuration实例的成员属性
Set<String> loadedResources = new HashSet<>()
是否contains当前resource来判断 - ② 如果没有加载,执行以下步骤
- ③ 解析mapper结点
- ④ 将当前resource放到configuration实例中loadedResources属性里
- ⑤ bindMapperForNamespace();
- 判断当前resource对应的namespace是否已经在configuration实例的MapperRegistry成员实例的knownMappers集合中。
Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>()
- 如果不在,则执行以下步骤
- 将
"namespace:" + namespace
添加到configuration实例的成员loadedResources中 configuration.addMapper(boundType)
;knownMappers.put(type, new MapperProxyFactory<>(type))
- 创建MapperAnnotationBuilder实例,解析命名空间对应的mapper接口(也就是解析接口上面的注解)
- 将
- 判断当前resource对应的namespace是否已经在configuration实例的MapperRegistry成员实例的knownMappers集合中。
- ⑥ 尝试处理待解决的ResultMaps
- ⑦ 尝试处理待解决的CacheRefs
- ⑧ 尝试处理待解决的Statements
假设你有一个EmployeeMapper.xml,那么loadedResources最终会保存什么呢?会保存三种格式
- EmployeeMapper.xml
- namespace:com.mybatis.dao.EmployeeMapper
- interface com.mybatis.dao.EmployeeMapper
6.1 XMLMapperBuilder.configurationElement
源码如下:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
代码解释如下:
- ① 获取namespace
- ② 为当前mapper的builderAssistant(MapperBuilderAssistant实例,是XMLMapperBuilder成员变量)设置namespace
- ③ 解析cache-ref结点,处理缓存引用
- ④ 解析cache结点,为当前namespace创建缓存实例
- ⑤ 解析parameterMap结点,参数映射
- ⑥ 解析resultMap结点,结果映射
- ⑦ 解析sql结点,SQL片段
- ⑧ 解析select|insert|update|delete结点
6.1.1 XMLMapperBuilder解析缓存引用结点
源码如下:
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);
}
}
}
解释如下:
-
① 向configuration实例的成员cacheRefMap添加引用关系
cacheRefMap.put(namespace, referencedNamespace);
-
② 创建缓存引用解析器实例CacheRefResolver,其包含了当前builderAssistant与引用的namespace。
public class CacheRefResolver { private final MapperBuilderAssistant assistant; private final String cacheRefNamespace; public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) { this.assistant = assistant; this.cacheRefNamespace = cacheRefNamespace; } public Cache resolveCacheRef() { return assistant.useCacheRef(cacheRefNamespace); } }
-
③ 尝试解析缓存引用
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); } } }
6.1.2 XMLMapperBuilder解析cache结点
源码如下:
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
解释如下:
- ① 获取cache的实现类类型
- ② 解析得到缓存回收策略处理类类型
- ③ 解析flushInterval(缓存刷新间隔)属性配置
- ④ 解析size属性(缓存存放多少元素)
- ⑤ 解析readOnly属性(是否只读)
- ⑥ 解析blocking属性(是否阻塞)
- ⑦ 获取子属性
- ⑧ 创建cache实例
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;
}
解释如下
- ① 根据命名空间、实现类、缓存过期策略…等使用CacheBuilder创建Cache实例
- ② 以namespace:cache实例这样的键值对放入configuration的成员变量中
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
- ③ 设置当前MapperBuilderAssistant实例的currentCache为①中获取到的cache实例
6.2 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集合中移除当前元素;否则不移除。
【2】使用Cache过程
在系统中,使用Cache的地方在CachingExecutor中:
CachingExecutor.query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)
方法源码如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
代码解释如下:
- ① 判断是否有二级缓存,如果缓存存在则执行如下步骤
获取cache后,先判断是否有二级缓存。 只有通过
<cache/>,<cache-ref/>
或@CacheNamespace,@CacheNamespaceRef
标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。- ② 根据sql配置(
<insert>,<select>,<update>,<delete>
的flushCache属性来确定是否清空缓存。 - ③ 如果当前ms配置使用了缓存也就是useCache=true,并且resultHandler ==null,则执行如下代码
-
④ 确保没有输出参数,这里是针对带有out类型的存储过程,也就是
StatementType==CALLABLE并且ParameterMode!=IN
。否则直接抛出异常
-
⑤ 从缓存里面获取数据。 如果数据为空则查询并将查询结果放入缓存, 如果缓存不为空则直接返回。
-
- ② 根据sql配置(
如果使用了二级缓存,那么查询结果首先会放在一级缓存中,当sqlsession关闭时,会将数据放到二级缓存里面。
【3】Cache使用时的注意事项
① 什么时候使用二级缓存
① 只能在只有单表操作
的表上使用缓存
不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下。
② 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存
这一点不需要多说,所有人都应该清楚。记住,这一点需要保证在①的前提下才可以!
② 避免使用二级缓存
可能会有很多人不理解这里,二级缓存带来的好处远远比不上他所隐藏的危害。
缓存是以namespace为单位的,不同namespace下的操作互不影响。
insert,update,delete操作会清空所在namespace下的全部缓存。
通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。
为什么避免使用二级缓存
在符合① 什么时候使用二级缓存
的要求时,并没有什么危害。其他情况就会有很多危害了。
针对一个表的某些操作不在他独立的namespace下进行。
例如在UserMapper.xml中有大多数针对user表的操作。但是在一个XXXMapper.xml中,还有针对user单表的操作。
这会导致user在两个命名空间下的数据不一致。如果在UserMapper.xml中做了刷新缓存的操作,在XXXMapper.xml中缓存仍然有效,如果有针对user的单表查询,使用缓存的结果可能会不正确。
更危险的情况是在XXXMapper.xml做了insert,update,delete操作时,会导致UserMapper.xml中的各种操作充满未知和风险。
有关这样单表的操作可能不常见。但是你也许想到了一种常见的情况。
多表操作一定不能使用缓存
首先不管多表操作写到那个namespace下,都会存在某个表不在这个namespace下的情况。
例如两个表:role和user_role,如果我想查询出某个用户的全部角色role,就一定会涉及到多表的操作。
<select id="selectUserRoles" resultType="UserRoleVO">
select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>
像上面这个查询,你会写到那个xml中呢??
不管是写到RoleMapper.xml还是UserRoleMapper.xml,或者是一个独立的XxxMapper.xml中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。
如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。
在我看来,就以MyBatis目前的缓存方式来看是无解的。多表操作根本不能缓存。
如果你让他们都使用同一个namespace(通过<cache-ref>
)来避免脏数据,那就失去了缓存的意义。
③ 挽救二级缓存**
想更高效率的使用二级缓存是解决不了了,但是解决多表操作避免脏数据还是有法解决的。
解决思路就是通过拦截器判断执行的sql涉及到那些表(可以用jsqlparser解析),然后把相关表的缓存自动清空。但是这种方式对缓存的使用效率是很低的。
设计这样一个插件是相当复杂的,还是建议,放弃二级缓存,在业务层使用可控制的缓存代替更好。