你需要下载Mybatis的源码,跟着本篇文章来一起阅读。
缓存源码分析
二级缓存构建在一级缓存之上,在收到查询请求时,mybatis首先会查询二级缓存,若二级缓存未命中,再去查询一级缓存,一级缓存没有,再查询数据库。 二级缓存与一级缓存不同,二级缓存和具体的命名空间绑定,一个Mapper中有一个Cache,相同Mapper中的MappedStatement共用一个Cache,一级缓存则是和SqlSession绑定。
:::tips SqlSession提交或关闭之后二级缓存才会生效。 :::
启动二级缓存:
- 开启全局二级缓存配置
<settings>
<!--开启全局的二级缓存配置-->
<setting name="cacheEnabled" value="true"/>
</settings>
- 在需要使用二级缓存的mapper配置文件中配置标签
<cache></cache>
- 在具体CURD标签上配置useCache=true
<select id="findById" resultMap="userMap" useCache="true" >
select * from user where id = #{id}
</select>
标签原理解析
标签是在Mapper.xml文件中,所以在解析Mapper.xml中会处理标签。
根据我们之前的分析Mapper.xml的解析会在XMLMapperBuilder 类的parse方法完成
public void parse() {
// 判断当前 Mapper 是否已经加载过
if (!configuration.isResourceLoaded(resource)) {
// 解析 `<mapper />` 节点
configurationElement(parser.evalNode("/mapper"));
// 标记该 Mapper 已经加载过
configuration.addLoadedResource(resource);
// 绑定 Mapper
bindMapperForNamespace();
}
// 解析待定的 <resultMap /> 节点
parsePendingResultMaps();
// 解析待定的 <cache-ref /> 节点
parsePendingCacheRefs();
// 解析待定的 SQL 语句的节点
parsePendingStatements();
}
在解析标签的configurationElement方法中,看到了解析<cache/>节点的方法cacheElement
// 解析 `<mapper />` 节点
private void configurationElement(XNode context) {
try {
// 获得 namespace 属性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置 namespace 属性
builderAssistant.setCurrentNamespace(namespace);
// 解析 <cache-ref /> 节点 当前的mapper中引入其他缓存时会用到这个标签
cacheRefElement(context.evalNode("cache-ref"));
// 解析 <cache /> 节点
cacheElement(context.evalNode("cache"));
// 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 <resultMap /> 节点们
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 <sql /> 节点们
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 <select /> <insert /> <update /> <delete /> 节点们
// 这里会将生成的Cache包装到对应的MappedStatement
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);
}
}
下面我们看cacheElement方法中做了些什么,其实创建Cache对象
// 解析 <cache /> 标签
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//解析<cache/>标签的type属性,这里我们可以自定义cache的实现类,比如redisCache,如果没有自定义,这里使用和一级缓存相同的PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 获得负责过期的 Cache 实现类
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 清空缓存的频率。0 代表不清空
Long flushInterval = context.getLongAttribute("flushInterval");
// 缓存容器大小
Integer size = context.getIntAttribute("size");
// 是否序列化
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 是否阻塞
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获得 Properties 属性
Properties props = context.getChildrenAsProperties();
// 创建 Cache 对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
Cache对象的创建最终调用了MapperBuilderAssistant.useNewCache()方法:
只会解析一次Mapper.xml,也就是会只创建一次Cache对象,并放进Configuration中。
/**
* 创建 Cache 对象
*
* @param typeClass 负责存储的 Cache 实现类
* @param evictionClass 负责过期的 Cache 实现类
* @param flushInterval 清空缓存的频率。0 代表不清空
* @param size 缓存容器大小
* @param readWrite 是否序列化
* @param blocking 是否阻塞
* @param props Properties 对象
* @return Cache 对象
*/
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 1.生成Cache对象
Cache cache = new CacheBuilder(currentNamespace)
//这里如果我们定义了<cache/>中的type,就使用自定义的Cache,否则使用和一级缓存相同的PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 2.添加到Configuration中
configuration.addCache(cache);
// 3.并将cache赋值给MapperBuilderAssistant.currentCache
currentCache = cache;
return cache;
}
OK,代码执行到这里我们知道了Cache对象已经存储到了Configuration中,并且并将cache赋值给MapperBuilderAssistant.currentCache。
我们继续回到解析节点的方法XMLMapperBuilder.configurationElement
// 解析 `<mapper />` 节点
private void configurationElement(XNode context) {
try {
// 获得 namespace 属性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置 namespace 属性
builderAssistant.setCurrentNamespace(namespace);
// 解析 <cache-ref /> 节点 当前的mapper中引入其他缓存时会用到这个标签
cacheRefElement(context.evalNode("cache-ref"));
// 解析 <cache /> 节点
cacheElement(context.evalNode("cache"));
// 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 <resultMap /> 节点们
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 <sql /> 节点们
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 <select /> <insert /> <update /> <delete /> 节点们
// 这里会将生成的Cache包装到对应的MappedStatement
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);
}
}
最终会调用到buildStatementFromContext将Cache包装到MappedStatement中,该方法最终会调用到
XMLStatementBuilder.parseStatementNode()方法解析每一个StatementNode,最终会调用addMappedStatement方法创建MappedStatement并且添加到Configuration中
/**
* 执行解析
*/
public void parseStatementNode() {
//....获取各种属性
//清空二级缓存默认值为false
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//使用二级缓存 默认值为true
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//.....
// 创建 MappedStatement 对象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
需要注意的是addMappedStatement方法是MapperBuilderAssistant类中的一个方法,而在上述的解析标签中会创建Cache对象,并且会将该对象赋值给MapperBuilderAssistant.currentCache属性中。 如下代码,可以清楚的看到将currentCache赋值给MappedStatement对象中。
// 构建 MappedStatement 对象
public MappedStatement addMappedStatement(......) {
// 如果只想的 Cache 未解析,抛出 IncompleteElementException 异常
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
// 获得 id 编号,格式为 `${namespace}.${id}`
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 创建 MappedStatement.Builder 对象
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id)) // 获得 ResultMap 集合
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache); // 在这里将之前生成的Cache对象封装到MappedStatement
// 获得 ParameterMap ,并设置到 MappedStatement.Builder 中
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// 创建 MappedStatement 对象
MappedStatement statement = statementBuilder.build();
// 添加到 configuration 中
configuration.addMappedStatement(statement);
return statement;
}
:::tips 注意:在解析Mapper.xml会生成一个Cache对象,之后解析NodeStatement:select | insert| update | delete标签,并且每个标签会为其生成一个MappedStatement对象,并且将之前生成的Cache对象封装到MappedStatement中,也就是说一个Mapper.xml对应着一个Cache对象。这也就是为什么二级缓存作用域是namespce。 :::
需要注意,当我们开启二级缓存后使用的执行器是CachingExecutor,我们断点在执行器的位置可以看到具体的执行器的实例
通过上述的截图我们还可以看到在CachingExecutor的构造方法中传递了一个delegate被委托的Executor对象
SimpleExecutor
CachingExecutor源码分析
下面我们看缓存执行的query方法的实现:
可以看到主要经历了几个步骤:
- 从MappedStatement中获取缓存对象,完美对接了我们上述的分析思路
- 判断cache对象是否为空
- 如果为空通过
delegate(SimpleExecutor)执行query方法,如果有一级缓存则走一级缓存查询,没有一级缓存则查询数据库 - 如果不为空继续往下走
- 如果为空通过
- 如果设置了flushCache=true 则清空缓存
- 如果useCache = true 则获取缓存
- 如果获取结果为空则
delegate.query如果有一级缓存则走一级缓存查询,如果没有一级缓存则查询数据库 - 如果获取结果不为空,则返回结果即可 :::tips 证实:通过源码证实了,会先走二级缓存,如果没有二级缓存则走一级缓存,如果没有一级缓存则走数据库查询。 :::
- 如果获取结果为空则
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 从 MappedStatement 中获取 Cache,注意这里的 Cache 是从MappedStatement中获取的
// 也就是我们上面解析Mapper中<cache/>标签中创建的,它保存在Configration中
// 我们在初始化解析xml时分析过每一个MappedStatement都有一个Cache对象,就是这里
Cache cache = ms.getCache();
// 如果配置文件中没有配置 <cache>,则 cache 为空
if (cache != null) {
//如果需要刷新缓存的话就刷新:flushCache="true"
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 暂时忽略,存储过程相关
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从二级缓存中,获取结果,这里为什么使用的tcm获取缓存呢?
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的话,则进行DB查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 缓存查询结果 并不是真正的向二级缓存存储数据
tcm.putObject(cache, key, list); // issue #578 and #116
}
// 如果存在,则直接返回结果
return list;
}
}
// 不使用缓存,则从数据库中查询(会查一级缓存)
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
/**
* 如果需要清空缓存,则进行清空
*
* @param ms MappedStatement 对象
*/
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) { // 是否需要清空缓存
//清空二级缓存
tcm.clear(cache);
}
通过看源码不知道大家有么有发现,无论是获取缓存还是清空缓存都会通过tcm处理而不是通过Cache对象直接处理,这是为什么呢?其实做后台开发我们都需要考虑并发的情况,其实java几乎所有的框架都会考虑并发问题,由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个
事务共⽤⼀个缓存实例,会导致脏读问题。⾄于脏读问题,需要借助其他类来处理,也就是上⾯代码中tcm 变量对应的类型。
原来如此,试想一下并发情况下访问同一个Mapper.xml中的方法,它使用同一个Cache对象,如果不加以控制确实会出现并发问题。
下面我们来看一下tcm它是如何处理并发问题的。tcm就是TransactionalCacheManager的实例,具体实现如下:
/**
* {@link TransactionalCache} 事务缓存管理器
*
* @author Clinton Begin
*/
public class TransactionalCacheManager {
/**
* // Cache 与 TransactionalCache 的映射关系表
*/
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
/**
* 清空缓存
*
* @param cache Cache 对象
*/
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
/**
* 获得缓存中,指定 Cache + K 的值。
*
* @param cache Cache 对象
* @param key 键
* @return 值
*/
public Object getObject(Cache cache, CacheKey key) {
// 直接从TransactionalCache中获取缓存
return getTransactionalCache(cache).getObject(key);
}
/**
* 添加 Cache + KV ,到缓存中
*
* @param cache Cache 对象
* @param key 键
* @param value 值
*/
public void putObject(Cache cache, CacheKey key, Object value) {
// 直接存入TransactionalCache的缓存中
getTransactionalCache(cache).putObject(key, value);
}
/**
* 提交所有 TransactionalCache
*/
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
/**
* 回滚所有 TransactionalCache
*/
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
/**
* 获得 Cache 对应的 TransactionalCache 对象
*
* @param cache Cache 对象
* @return TransactionalCache 对象
*/
private TransactionalCache getTransactionalCache(Cache cache) {
//如果 key 对应的 value 不存在,则使用获取 remappingFunction 重新计算后的值,并保存为该 key 的 value,否则返回 value。
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
从上述代码可以看出,TransactionalCacheManager内部维护了Cache实例与TransactionalCache实例之间的关系,该类仅仅是负责维护两者的映射关系,真正做事的是TransactionalCache是一种缓存装饰器,可以为Cache实例增加事务功能.
如下代码:
- 获取缓存getObject:直接从delegate代理对象中去查询也就是真实的缓存对象查询,如果不存在添加到缓存未命中集合中
entriesMissedInCache - 存储缓存putObject: 主要到这个方法只是把它存入到了
entriesToAddOnCommit集合中并没有存储Cache对象中,这一步是关键,缓存并没有立刻生效,如果直接存到delegate会导致脏数据的问题。 - 缓存生效commit:当sqlsession调用commit方法的时候,才会将
entriesToAddOnCommit中的数据存入到delegate中 - clear方法:会将clearOnCommit置为true,并且清空
entriesToAddOnCommit当再次调用commit方法如果clearOnCommit==true就会清空缓存
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
/**
* 委托的 Cache 对象。
*
* 实际上,就是二级缓存 Cache 对象。
*/
private final Cache delegate;
/**
* 提交时,清空 {@link #delegate}
*
* 初始时,该值为 false
* 清理后{@link #clear()} 时,该值为 true ,表示持续处于清空状态
*/
private boolean clearOnCommit;
/**
* // 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
*/
private final Map<Object, Object> entriesToAddOnCommit;
/**
* 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
*/
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public Object getObject(Object key) {
// 查询的时候是直接从delegate中去查询的,也就是从真正的缓存对象中查询
Object object = delegate.getObject(key);
// 如果不存在,则添加到 entriesMissedInCache 中
if (object == null) {
// 缓存未命中,则将 key 存入到 entriesMissedInCache 中
entriesMissedInCache.add(key);
}
// issue #146
// 如果 clearOnCommit 为 true ,表示处于持续清空状态,则返回 null
if (clearOnCommit) {
return null;
// 返回 value
} else {
return object;
}
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
public void putObject(Object key, Object object) {
// 将键值对存入到 entriesToAddOnCommit 这个Map中中,而非真实的缓存对象 delegate 中
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
// 标记 clearOnCommit 为 true
clearOnCommit = true;
// 清空 entriesToAddOnCommit
entriesToAddOnCommit.clear();
}
public void commit() {
// 如果 clearOnCommit 为 true ,则清空 delegate 缓存
if (clearOnCommit) {
delegate.clear();
}
// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate(cache) 中
flushPendingEntries();
// 重置
reset();
}
public void rollback() {
// 从 delegate 移除出 entriesMissedInCache
unlockMissedEntries();
// 重置
reset();
}
private void reset() {
// 重置 clearOnCommit 为 false
clearOnCommit = false;
// 清空 entriesToAddOnCommit、entriesMissedInCache
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
/**
* 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
*/
private void flushPendingEntries() {
// 将 entriesToAddOnCommit 中的内容转存到 delegate 中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 在这里真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,二级缓存才真正的生效
delegate.putObject(entry.getKey(), entry.getValue());
}
// 将 entriesMissedInCache 刷入 delegate 中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
}
当调用sqlsession.update方法,缓存会如何处理?
我们来看CachingExecutor的update方法:
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 如果需要清空缓存,则进行清空
flushCacheIfRequired(ms);
// 执行 delegate 对应的方法
return delegate.update(ms, parameterObject);
}
/**
* 如果需要清空缓存,则进行清空
*
* @param ms MappedStatement 对象
*/
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) { // 是否需要清空缓存
//清空二级缓存
tcm.clear(cache);
}
}
如上述代码在进行SQL的增、删、改操作时都会清空二级缓存,因此二级缓存不适用于经常进行更新的数据。
二级缓存的设计上大量的运用了装饰者模式,如CachingExecutor,以及各种Cache接口的装饰器
- 二级缓存实现了SqlSession之间的缓存数据共享,属于namespace级别
- 二级缓存具有丰富的缓存策略
- 二级缓存可由多个装饰器,与基础缓存组合而成
- 二级缓存工作由一个缓存装饰器执行CachingExecutor和一个事务型预缓存TransactionalCache完成
延迟加载源码分析
mybatis为我们提供了延迟加载策略,那么什么是延迟加载呢?就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称为懒加载
- 优点:先从单表查询,需要时再从关联表中去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张白表速度要快。
- 缺点:因为只有当需要用到数据时,才会进行数据库查询,这样大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
- 在多表中:一对多,多对多通常情况下采用延迟加载,其他情况通常采用立即加载。
- 延迟加载是基于嵌套查询来实现的
局部的延迟加载
在association和collection标签中都有一个fetchType属性,通过修改它的值,可以修改局部的加载策略
<resultMap id="userMap" type="com.lagou.pojo.User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<!-- fetchType 延迟加载策略
lazy : 懒加载策略
eager : 立即加载策略
-->
<collection property="orderList" ofType="com.lagou.pojo.Order"
select="com.lagou.mapper.IOrderMapper.findOrderByUid" column="id"
fetchType="lazy">
<id property="id" column="oid"/>
<result property="orderTime" column="ordertime"/>
<result property="total" column="total"/>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap" >
select u.*,o.id oid,o.ordertime,o.total,o.uid from user u left join orders o on o.uid = u.id
</select>
全局延迟加载 在mybatis的核心配置文件中可以使用setting标签修改全局的加载策略
<settings>
<!-- 开启全局延迟加载 默认为false -->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
一级缓存原理
至于一级缓存的实现非常简单,在BaseExecutor的类中,具体核心代码如下: 其实就是声明了一个变量,在commit和clear的时候包括执行update方法都会清空缓存
public abstract class BaseExecutor implements Executor {
/**
* 本地缓存,即一级缓存
*/
protected PerpetualCache localCache;
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// queryStack + 1
queryStack++;
// 从一级缓存中,获取查询结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// 获取到,则进行处理
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
// 获得不到,则从数据库中查询
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// queryStack - 1
queryStack--;
}
if (queryStack == 0) {
// 执行延迟加载
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 清空 deferredLoads
deferredLoads.clear();
// 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地缓存
clearLocalCache();
// 执行写操作
return doUpdate(ms, parameter);
}
@Override
public void commit(boolean required) throws SQLException {
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 清空本地缓存
clearLocalCache();
// 刷入批处理语句
flushStatements();
// 是否要求提交事务。如果是,则提交事务。
if (required) {
transaction.commit();
}
}
@Override
public void clearLocalCache() {
if (!closed) {
// 清理 localCache
localCache.clear();
// 清理 localOutputParameterCache
localOutputParameterCache.clear();
}
}
@Override
public void close(boolean forceRollback) {
try {
// 回滚事务
try {
rollback(forceRollback);
} finally {
// 关闭事务
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
// 置空变量
transaction = null;
deferredLoads = null;
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
}
延迟加载原理实现
它的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属 性的 getting ⽅法时,进⼊拦截器⽅法。⽐如调⽤a.getB().getName()⽅法,进⼊拦截器的 invoke(...)⽅法,发现a.getB()需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调⽤a.setB(b)⽅法,于是a对象b属性就有值了,接着完 成a.getB().getName()⽅法的调⽤。这就是延迟加载的基本原理。 总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。
具体的源码位置在DefaultResultSetHandler在处理结果集的类
Mybatis的查询结果是由ResultSetHandler接⼝的handleResultSets()⽅法处理的。ResultSetHandler
接⼝只有⼀个实现,DefaultResultSetHandler,接下来看下延迟加载相关的⼀个核⼼的⽅法。
如下代码,会判断是否开启延迟加载,在找到嵌套查询的标签会调用configuration.getProxyFactory()获取结果的代理对象resultObject,并返回。
// 创建映射后的结果对象
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
// useConstructorMappings ,表示是否使用构造方法创建该结果对象。此处将其重置
this.useConstructorMappings = false; // reset previous mapping result
final List<Class<?>> constructorArgTypes = new ArrayList<>(); // 记录使用的构造方法的参数类型的数组
final List<Object> constructorArgs = new ArrayList<>(); // 记录使用的构造方法的参数值的数组
// 创建映射后的结果对象
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
// 如果有内嵌的查询,并且开启延迟加载,则创建结果对象的代理对象 resultMap中的配置 会找到嵌套查询的标签
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// issue gcode #109 && issue #149 判断当前的属性是否设置fetchType="lazy" 属性
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
// 设置了延迟加载 走如下的方法 构建代理对象
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
// 判断是否使用构造方法创建该结果对象
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
默认的代理对象时JavassitProxyFactory
/**
* 默认的生成代理对象的方式是:JavassistProxyFactory
*/
protected ProxyFactory proxyFactory = new JavassistProxyFactory();
然后我们再去看看JavassitProxyFactory的具体实现,其中的createProxy()方法会调用到如下的核心逻辑
/**
* 代理对象实现的核心逻辑
*/
private static class EnhancedResultObjectProxyImpl implements MethodHandler {
private final Class<?> type;
private final ResultLoaderMap lazyLoader;
private final boolean aggressive;
private final Set<String> lazyLoadTriggerMethods;
private final ObjectFactory objectFactory;
private final List<Class<?>> constructorArgTypes;
private final List<Object> constructorArgs;
private EnhancedResultObjectProxyImpl(Class<?> type, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
this.type = type;
this.lazyLoader = lazyLoader;
this.aggressive = configuration.isAggressiveLazyLoading();
this.lazyLoadTriggerMethods = configuration.getLazyLoadTriggerMethods();
this.objectFactory = objectFactory;
this.constructorArgTypes = constructorArgTypes;
this.constructorArgs = constructorArgs;
}
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
final Class<?> type = target.getClass();
// 创建 EnhancedResultObjectProxyImpl 对象
EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
// 创建代理对象
Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
// 将 target 的属性,复制到 enhanced 中
PropertyCopier.copyBeanProperties(type, target, enhanced);
return enhanced;
}
@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
// 忽略 WRITE_REPLACE_METHOD ,和序列化相关
if (WRITE_REPLACE_METHOD.equals(methodName)) {
Object original;
if (constructorArgTypes.isEmpty()) {
original = objectFactory.create(type);
} else {
original = objectFactory.create(type, constructorArgTypes, constructorArgs);
}
PropertyCopier.copyBeanProperties(type, enhanced, original);
if (lazyLoader.size() > 0) {
return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
} else {
return original;
}
} else {
//
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
// 加载所有延迟加载的属性 aggressive 对应aggressiveLazyLoading属性 判断lazyLoadTriggerMethods属性设置的方法名
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
// 如果调用了 setting 方法,则不在使用延迟加载
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property); // 移除
// 如果调用了 getting 方法,则执行延迟加载
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
// 判断该属性是否进行了延迟加载
if (lazyLoader.hasLoader(property)) {
// 发送SQL语句进行SQL的执行
lazyLoader.load(property);
}
}
}
}
}
// 继续执行原方法
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
lazyLoader.load()最终会调用到LoadPair.load()方法,这个方法负责查询SQL并且将结果通过set方法设置进去,当调用原方法的get时就会取出对应的数据
class LoadPair{
....
public void load(final Object userObject) throws SQLException {
if (this.metaResultObject == null || this.resultLoader == null) {
if (this.mappedParameter == null) {
throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
+ "required parameter of mapped statement ["
+ this.mappedStatement + "] is not serializable.");
}
// 获得 Configuration 对象
final Configuration config = this.getConfiguration();
// 获得 MappedStatement 对象
final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
if (ms == null) {
throw new ExecutorException("Cannot lazy load property [" + this.property
+ "] of deserialized object [" + userObject.getClass()
+ "] because configuration does not contain statement ["
+ this.mappedStatement + "]");
}
// 获得对应的 MetaObject 对象
this.metaResultObject = config.newMetaObject(userObject);
// 创建 ResultLoader 对象
this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
metaResultObject.getSetterType(this.property), null, null);
}
/* We are using a new executor because we may be (and likely are) on a new thread
* and executors aren't thread safe. (Is this sufficient?)
*
* A better approach would be making executors thread safe. */
if (this.serializationCheck == null) {
final ResultLoader old = this.resultLoader;
this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
}
this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
}
:::tips 延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载. :::