1. 前言
前面近一個月去寫自己的mybatis框架了,對mybatis源碼分析止步不前,此文繼續(xù)前面的文章。開始分析mybatis一,二級緩存的實現(xiàn)。
附上自己的項目github地址:https://github.com/xbcrh/simple-ibatis
對mybatis感興趣的同學(xué)可關(guān)注下,全手寫的一個orm框架,實現(xiàn)了sql的基本功能和對象關(guān)系映射。
廢話不說,開始解析mybatis緩存源碼實現(xiàn)。
2. mybatis中緩存的實現(xiàn)方式
見mybatis源碼包 org.apache.ibatis.cache
2.1 mybatis緩存實現(xiàn)接口類:cache
public?interface?Cache?{
//?獲取緩存的ID
String?getId();
//?放入緩存
void?putObject(Object?key,?Object?value);
//?從緩存中獲取
Object?getObject(Object?key);
//?移除緩存
Object?removeObject(Object?key);
//?清除緩存
void?clear();
//?獲取緩存大小
int?getSize();
//?獲取鎖
ReadWriteLock?getReadWriteLock();
}
mybatis自定義了緩存接口類,提供了基本的緩存增刪改查的操作。在此基礎(chǔ)上,提供了基礎(chǔ)緩存實現(xiàn)類PerpetualCache。源碼如下:
2.2 mybatis緩存基本實現(xiàn)類:PerpetualCache
public?class?PerpetualCache?implements?Cache?{
//?緩存的ID
private?String?id;
//?使用HashMap充當(dāng)緩存(老套路,緩存底層實現(xiàn)基本都是map)
private?Map?cache?=?new?HashMap();
//?唯一構(gòu)造方法(即緩存必須有ID)
public?PerpetualCache(String?id)?{
this.id?=?id;
}
//?獲取緩存的唯一ID
public?StringgetId()?{
returnid;
}
//?獲取緩存的大小,實際就是hashmap的大小
public?intgetSize()?{
returncache.size();
}
//?放入緩存,實際就是放入hashmap
public?void?putObject(Object?key,?Object?value)?{
cache.put(key,?value);
}
//?從緩存獲取,實際就是從hashmap中獲取
public?Object?getObject(Object?key)?{
returncache.get(key);
}
//?從緩存移除
public?Object?removeObject(Object?key)?{
returncache.remove(key);
}
//?hashmap清除數(shù)據(jù)方法
public?voidclear()?{
cache.clear();
}
//?暫時沒有其實現(xiàn)
public?ReadWriteLockgetReadWriteLock()?{
returnnull;
}
//?緩存是否相同
public?boolean?equals(Object?o)?{
if(getId()?==?null)?throw?new?CacheException("Cache?instances?require?an?ID.");
if(this?==?o)returntrue;?//?緩存本身,肯定相同
if(!(o?instanceof?Cache))returnfalse;?//?沒有實現(xiàn)cache類,直接返回false
Cache?otherCache?=?(Cache)?o;?//?強(qiáng)制轉(zhuǎn)換為cache
returngetId().equals(otherCache.getId());?//?直接比較ID是否相等
}
//?獲取hashCode
public?inthashCode()?{
if(getId()?==?null)?throw?new?CacheException("Cache?instances?require?an?ID.");
returngetId().hashCode();
}
}
PerpetualCache 類其實是對HashMap的封裝,通過對map的put和get等操作實現(xiàn)緩存的存取等功能。mybatis中除了基本的緩存實現(xiàn)類外還提供了一系列的裝飾類(此處是用到裝飾者模式),此處拿較為重要的裝飾類LruCache進(jìn)行分析。
2.3 Lru淘汰策略實現(xiàn)分析
Lru是一種緩存淘汰策略,其核心思想是”如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高“,LruCache 是基于LinkedHashMap實現(xiàn),LinkedHashMap繼承自HashMap,來分析下為什么LinkedHashMap可以當(dāng)做Lru緩存實現(xiàn)。
public?class?LinkedHashMap
extends?HashMap
implements?Map
LinkedHashMap繼承HashMap類,實際上就是對HashMap的一個封裝。
//?內(nèi)部維護(hù)了一個自定義的Entry,集成HashMap中的node類
static?class?Entry?extends?HashMap.Node?{
//?linkedHashmap用來連接節(jié)點的字段,根據(jù)這兩個字段可查找按順序插入的節(jié)點
Entry?before,?after;
Entry(inthash,?K?key,?V?value,?Node?next)?{
super(hash,?key,?value,?next);
}
}
構(gòu)造方法見如下:
public?LinkedHashMap(int?initialCapacity,
floatloadFactor,
boolean?accessOrder)?{
//?調(diào)用HashMap的構(gòu)造方法
super(initialCapacity,?loadFactor);
//?訪問順序維護(hù),默認(rèn)false不開啟
this.accessOrder?=?accessOrder;
}
引入兩種圖來理解HashMap與LinkedHashMap
?以上是HashMap的結(jié)構(gòu),采用拉鏈法解決沖突。LinkedHashMap在HashMap基礎(chǔ)上增加了一個雙向鏈表來表示節(jié)點插入順序。
如上,節(jié)點上多出的紅色和藍(lán)色箭頭代表了Entry中的before和after。在put元素時,會自動在尾節(jié)點后加上該元素,維持雙向鏈表。了解LinkedHashMap結(jié)構(gòu)后,在看看究竟什么是維護(hù)節(jié)點的訪問順序。先說結(jié)論,當(dāng)開啟accessOrder后,在對元素進(jìn)行g(shù)et操作時,會將該元素放在雙向鏈表的隊尾節(jié)點。源碼如下:
public?V?get(Object?key)?{
Node?e;
//?調(diào)用HashMap的getNode方法,獲取元素
if((e?=?getNode(hash(key),?key))?==?null)
returnnull;
//?默認(rèn)為false,如果開啟維護(hù)鏈表訪問順序,執(zhí)行如下方法
if(accessOrder)
afterNodeAccess(e);
returne.value;
}
//?方法實現(xiàn)(將e放入尾節(jié)點處)
void?afterNodeAccess(Node?e)?{?//?move?node?to?last
LinkedHashMap.Entry?last;
//?當(dāng)節(jié)點不是雙向鏈表的尾節(jié)點時
if(accessOrder?&&?(last?=?tail)?!=?e)?{
LinkedHashMap.Entry?p?=
(LinkedHashMap.Entry)e,?b?=?p.before,?a?=?p.after;?//?將待調(diào)整的e節(jié)點賦值給p
p.after?=?null;
if(b?==?null)?//?說明e為頭節(jié)點,將老e的下一節(jié)點值為頭節(jié)點
head?=?a;
else
b.after?=?a;//?否則,e的上一節(jié)點直接指向e的下一節(jié)點
if(a?!=?null)
a.before?=?b;?//?e的下一節(jié)點的上節(jié)點為e的上一節(jié)點
else
last?=?b;
if(last?==?null)
head?=?p;
else{
p.before?=?last;???//?last和p互相連接
last.after?=?p;
}
tail?=?p;???//?將雙向鏈表的尾節(jié)點指向p
++modCount;?//?修改次數(shù)加以
}
}
代碼很簡單,如上面的圖,我訪問了節(jié)點值為3的節(jié)點,那木經(jīng)過get操作后,結(jié)構(gòu)變成如下:
?經(jīng)過如上分析我們知道,如果限制雙向鏈表的長度,每次刪除頭節(jié)點的值,就變?yōu)橐粋€lru的淘汰策略了。舉個例子,我想限制雙向鏈表的長度為3,依次put 1 2 3,鏈表為 1 -> 2 -> 3,訪問元素2,鏈表變?yōu)?1 -> 3-> 2,然后put 4 ,發(fā)現(xiàn)鏈表長度超過3了,淘汰1,鏈表變?yōu)? -> 2 ->4;
那木linkedHashMap是怎樣知道自定義的限制策略,看代碼,因為LinkedHashMap中沒有提供自己的put方法,是直接調(diào)用的HashMap的put方法,查看hashMap代碼如下:
//?hashMap
final?V?putVal(inthash,?K?key,?V?value,?boolean?onlyIfAbsent,
boolean?evict)?{
Node[]?tab;?Node?p;?int?n,?i;
if((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
n?=?(tab?=?resize()).length;
if((p?=?tab[i?=?(n?-?1)?&hash])?==?null)
tab[i]?=?newNode(hash,?key,?value,?null);
else{
Node?e;?K?k;
if(p.hash?==hash&&
((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
e?=?p;
elseif(p?instanceof?TreeNode)
e?=?((TreeNode)p).putTreeVal(this,?tab,hash,?key,?value);
else{
for(int?binCount?=?0;?;?++binCount)?{
if((e?=?p.next)?==?null)?{
p.next?=?newNode(hash,?key,?value,?null);
if(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1for1st
treeifyBin(tab,hash);
break;
}
if(e.hash?==hash&&
((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
break;
p?=?e;
}
}
if(e?!=?null)?{?//?existing?mappingforkey
V?oldValue?=?e.value;
if(!onlyIfAbsent?||?oldValue?==?null)
e.value?=?value;
afterNodeAccess(e);
returnoldValue;
}
}
++modCount;
if(++size?>?threshold)
resize();
//?看這個方法
afterNodeInsertion(evict);
returnnull;
}
//?linkedHashMap重寫了此方法
void?afterNodeInsertion(boolean?evict)?{?//?possibly?remove?eldest
LinkedHashMap.Entry?first;
//?removeEldestEntry默認(rèn)返回fasle
if(evict?&&?(first?=?head)?!=?null?&&?removeEldestEntry(first))?{
K?key?=?first.key;
//?移除雙向鏈表中的頭指針元素
removeNode(hash(key),?key,?null,false,true);
}
}
原來只需要重新實現(xiàn)removeEldestEntry就可以自定義實現(xiàn)lru功能了。了解基本的lru原理后,開始分析LruCache。
2.4 緩存包裝類 - LruCache
public?class?LruCache?implements?Cache?{
//?被裝飾的緩存類,即真實的緩存類,提供真正的緩存能力
private?final?Cache?delegate;
//?內(nèi)部維護(hù)的一個linkedHashMap,用來實現(xiàn)LRU功能
private?Map?keyMap;
//?待淘汰的緩存元素
private?Object?eldestKey;
//?唯一構(gòu)造方法
public?LruCache(Cache?delegate)?{
this.delegate?=?delegate;?//?被裝飾的緩存類
setSize(1024);?//?設(shè)置緩存大小
}
....
}
經(jīng)分析,LruCache還是個裝飾類。內(nèi)部除了維護(hù)真正的Cache外,還維護(hù)了一個LinkedHashMap,用來實現(xiàn)Lru功能,查看其構(gòu)造方法。
//?唯一構(gòu)造方法
public?LruCache(Cache?delegate)?{
this.delegate?=?delegate;?//?被裝飾的緩存類
setSize(1024);?//?設(shè)置緩存大小
}
//?setSize()是構(gòu)造方法中方法
public?void?setSize(final?int?size)?{
//?初始化keyMap
keyMap?=?new?LinkedHashMap(size,?.75F,true)?{
private?static?final?long?serialVersionUID?=?4267176411845948333L;
//?什么時候自動刪除緩存元素,此處是根據(jù)當(dāng)緩存數(shù)量超過指定的數(shù)量,在LinkedHashMap內(nèi)部刪除元素
protected?boolean?removeEldestEntry(Map.Entry?eldest)?{
boolean?tooBig?=?size()?>?size;
if(tooBig)?{
//?將待刪除元素賦值給eldestKey,后續(xù)會根據(jù)此值是否為空在真實緩存中刪除
eldestKey?=?eldest.getKey();
}
returntooBig;
}
};
}
和上文分析一樣,重寫了removeEldestEntry方法。此方法返回一個boolean值,當(dāng)緩存的大小超過自定義大小,返回true,此時linkedHashMap中會自動刪除eldest元素。在真實緩存cache中也將此元素刪除。保持真實cache和linkedHashMap元素一致。其實就是用linkedHashMap的lru特性來保證cache也具有此lru特性。
分析put方法和get方法驗證此結(jié)論.。
@Override
public?Object?getObject(Object?key)?{
keyMap.get(key);?//?觸發(fā)linkedHashMap中g(shù)et方法,將key對應(yīng)的元素放入隊尾
returndelegate.getObject(key);?//?調(diào)用真實的緩存get方法
}
//?放入緩存時,除了在真實緩存中放一份外,還會在LinkedHashMap中放一份
@Override
public?void?putObject(Object?key,?Object?value)?{
delegate.putObject(key,?value);
//?調(diào)用LinkedHashMap的方法
cycleKeyList(key);
}
private?void?cycleKeyList(Object?key)?{
//?linkedHashMap中put,會觸發(fā)removeEldestEntry方法,如果緩存大小超過指定大小,則將雙向鏈表對頭值賦值給eldestKey
keyMap.put(key,?key);
//?檢查eldestKey是否為空。不為空,則代表此元素是淘汰的元素了,需要在真實緩存中刪除。
if(eldestKey?!=?null)?{
//?真實緩存中刪除
delegate.removeObject(eldestKey);
eldestKey?=?null;
}
}
介紹完Cache基本實現(xiàn)后,開始分析mybatis中一級緩存
3.?mybatis一級緩存使用源碼分析
此處是僅介紹mybatis的實現(xiàn),沒有涉及到與Spring整合,先介紹mybatis最基本的sql執(zhí)行語法。默認(rèn)大家掌握了SqlSessionFactoryBuilder,SqlSessionFactory,SqlSession用法。后面我會寫一篇博客分析SQL在mybatis中執(zhí)行的過程,會介紹到這些基礎(chǔ)知識。
InputStream?inputStream?=?Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml");?//?構(gòu)建字節(jié)流
SqlSessionFactoryBuilder?builder?=?new?SqlSessionFactoryBuilder();???//?構(gòu)建SqlSessionFactoryBuilder
SqlSessionFactory?factory?=?builder.build(inputStream);??//?構(gòu)建SqlSessionFactory
SqlSession?sqlSession?=?factory.openSession();?//?生成SqlSession
List?userList?=?sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser");?//?執(zhí)行SysUserMapper類的getSysUser方法
前文構(gòu)建SqlSession的內(nèi)容大家感興趣可自行查看,此處僅分析執(zhí)行過程。查看selectList方法,mybatis中sqlSession的默認(rèn)實現(xiàn)為DefaultSqlSession
public??List?selectList(String?statement,?Object?parameter,?RowBounds?rowBounds)?{
try?{
//?每個mapper文件會解析生成一個MappedStatement
MappedStatement?ms?=?configuration.getMappedStatement(statement);
//?調(diào)用真實的查詢方法,此處是調(diào)用executor的方法。executor采用了裝飾者模式,若該mapper文件未啟用二級緩存,則默認(rèn)為BaseExecutor。
//?若該mapper文件啟用了二級緩存,則使用的是CachingExecutor
List?result?=?executor.query(ms,?wrapCollection(parameter),?rowBounds,?Executor.NO_RESULT_HANDLER);
returnresult;
}?catch?(Exception?e)?{
throw?ExceptionFactory.wrapException("Error?querying?database.??Cause:?"+?e,?e);
}?finally?{
ErrorContext.instance().reset();
}
}
因為此處使用的是裝飾者模式,BaseExecutor是最基礎(chǔ)的執(zhí)行器,使用了一級緩存,CachingExecutor是對BaseExecutor進(jìn)行一次封裝,若打開二級緩存開關(guān),在使用一級緩存前,先使用二級緩存。后文介紹二級緩存會分析這兩個Executor生成地方。先分析BaseExecutor的一級緩存實現(xiàn)。
//?BaseExecutor.java
/**
*?查詢,并創(chuàng)建好CacheKey對象
*?@param?ms?Mapper.xml文件的select,delete,update,insert這些DML標(biāo)簽的封裝類
*?@param?parameter?參數(shù)對象
*?@param?rowBounds?Mybatis的分頁對象
*?@param?resultHandler?結(jié)果處理器對象
*/
public??List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler)?throws?SQLException?{
BoundSql?boundSql?=?ms.getBoundSql(parameter);?//?獲取boundSql對象
CacheKey?key?=?createCacheKey(ms,?parameter,?rowBounds,?boundSql);??//?生成緩存KEY
returnquery(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);?//?執(zhí)行如下方法
}
@SuppressWarnings("unchecked")
public??List?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());
if(closed)?throw?new?ExecutorException("Executor?was?closed.");
//如果將flushCacheRequired為true,則會在執(zhí)行器執(zhí)行之前就清空本地一級緩存
if(queryStack?==?0?&&?ms.isFlushCacheRequired())?{
clearLocalCache();
}
List?list;
try?{
queryStack++;?//?請求堆棧加一
//?如果此次查詢的resultHandler為null(默認(rèn)為null),則嘗試從本地緩存中獲取已經(jīng)緩存的的結(jié)果
list?=?resultHandler?==?null???(List)?localCache.getObject(key)?:?null;
if(list?!=?null)?{
//如果查到localCache緩存,處理localOutputParameterCache,即對存儲過程的sql進(jìn)行特殊處理
handleLocallyCachedOutputParameters(ms,?key,?parameter,?boundSql);
}else{
//?從數(shù)據(jù)庫中查詢,并將結(jié)果放入到localCache
list?=?queryFromDatabase(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);
}
}?finally?{
//?請求堆棧減一
queryStack--;
}
if(queryStack?==?0)?{
//?加載延遲加載List
for(DeferredLoad?deferredLoad?:?deferredLoads)?{
deferredLoad.load();
}
deferredLoads.clear();?//?issue#601
if(configuration.getLocalCacheScope()?==?LocalCacheScope.STATEMENT)?{
clearLocalCache();?//?issue#482
}
}
returnlist;
}
private??List?queryFromDatabase(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{
List?list;
localCache.putObject(key,?EXECUTION_PLACEHOLDER);?//?先放置一個占位符
try?{
list?=?doQuery(ms,?parameter,?rowBounds,?resultHandler,?boundSql);??//?從數(shù)據(jù)庫中查找
}?finally?{
localCache.removeObject(key);?//?移除占位符
}
localCache.putObject(key,?list);?//?放入緩存
if(ms.getStatementType()?==?StatementType.CALLABLE)?{
localOutputParameterCache.putObject(key,?parameter);??//?若是存儲過程,則放入存儲過程緩存中
}
returnlist;?//?返回查詢結(jié)果
}
mybatis一級緩存很好理解,對于同一個SqlSession對象(即同一個Executor),執(zhí)行同一條語句時,BaseExecutor會先從自己的緩存中查找,是否存在此條語句的結(jié)果,若能找到,則直接返回(暫且忽略存儲過程處理)。若沒有找到,則查詢數(shù)據(jù)庫,將結(jié)果放入此緩存,供下次使用。mybatis默認(rèn)打開一級緩存。
4.?mybatis二級緩存使用源碼分析
4.1 配置方式
在全局配置文件中mybatis-config.xml中加入如下設(shè)置
在具體mapper.xml中配置<cache/>標(biāo)簽或者<cache-ref/>標(biāo)簽
<cache></cache>或者<cache-ref/>
或者采用注解配置方式,在mapper.java文件上配置注解
@CacheNamespace 或者 @CacheNamespaceRef
4.1 mybatis解析二級緩存標(biāo)簽
還是采用上面sqlSession方式代碼來debug
InputStream?inputStream?=?Resources.getResourceAsStream("com/xiaobing/resource/mybatisConfig.xml");?//?構(gòu)建字節(jié)流
SqlSessionFactoryBuilder?builder?=?new?SqlSessionFactoryBuilder();???//?構(gòu)建SqlSessionFactoryBuilder
SqlSessionFactory?factory?=?builder.build(inputStream);??//?構(gòu)建SqlSessionFactory
進(jìn)入查看builder.build()方法
//?SqlSessionFactoryBuilder.java
/**根據(jù)流構(gòu)建SqlSessionFactory*/
public?SqlSessionFactory?build(InputStream?inputStream,?String?environment,?Properties?properties)?{
try?{
/**構(gòu)建XML文件解析器*/
XMLConfigBuilder?parser?=?new?XMLConfigBuilder(inputStream,?environment,?properties);
/**開始解析mybatis-config.xml文件并構(gòu)建全局變量Configuration*/
returnbuild(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.
}
}
}
進(jìn)入parser.parse()方法,,進(jìn)一步分析
public?Configurationparse()?{
if(parsed)?{
throw?new?BuilderException("Each?XMLConfigBuilder?can?only?be?used?once.");
}
parsed?=true;
parseConfiguration(parser.evalNode("/configuration"));
returnconfiguration;
}
private?void?parseConfiguration(XNode?root)?{
try?{
propertiesElement(root.evalNode("properties"));?//issue#117?read?properties?first?//?讀取properties配置
typeAliasesElement(root.evalNode("typeAliases"));?//?讀取別名設(shè)置
pluginElement(root.evalNode("plugins"));?//?讀取插件設(shè)置
objectFactoryElement(root.evalNode("objectFactory"));?//?讀取對象工廠設(shè)置
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));?//?讀取對象包裝工廠設(shè)置
settingsElement(root.evalNode("settings"));?//?讀取setting設(shè)置
environmentsElement(root.evalNode("environments"));?//readit?after?objectFactory?and?objectWrapperFactory?issue#631?//?讀取環(huán)境設(shè)置
databaseIdProviderElement(root.evalNode("databaseIdProvider"));?//?讀取數(shù)據(jù)庫ID提供信息
typeHandlerElement(root.evalNode("typeHandlers"));??//?讀取類型轉(zhuǎn)換處理器
mapperElement(root.evalNode("mappers"));??//?解析mapper文件
}?catch?(Exception?e)?{
throw?new?BuilderException("Error?parsing?SQL?Mapper?Configuration.?Cause:?"+?e,?e);
}
}
此處僅分析<cache/> 和 <cache-ref/>標(biāo)簽的解析,<cache/> 和 <cache-ref/>存在具體的mapper.xml文件中,分析mapperElement()方法。因為在mybatis-config.xml文件中關(guān)于<mapper>標(biāo)簽的值可配置package,resource,url,class等配置。如
分析mapperElement()方法
/**
*?映射文件支持四種配置,package,resource,url,class四種
*?如在mybatis-config.xml中配置
*?
*?*/
private?void?mapperElement(XNode?parent)?throws?Exception?{
if(parent?!=?null)?{
for(XNode?child?:?parent.getChildren())?{
if("package".equals(child.getName()))?{?//?若配置的是package,在講package下的所有mapper文件進(jìn)行解析
String?mapperPackage?=?child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
}else{
String?resource?=?child.getStringAttribute("resource");
String?url?=?child.getStringAttribute("url");
String?mapperClass?=?child.getStringAttribute("class");
if(resource?!=?null?&&?url?==?null?&&?mapperClass?==?null)?{??//?若配置的是resource,在解析resource對應(yīng)的mapper.xml
ErrorContext.instance().resource(resource);
InputStream?inputStream?=?Resources.getResourceAsStream(resource);?//?獲取xml文件字節(jié)流
XMLMapperBuilder?mapperParser?=?new?XMLMapperBuilder(inputStream,?configuration,?resource,?configuration.getSqlFragments());?//?構(gòu)建xml文件構(gòu)造器
mapperParser.parse();?//?解析xml文件
}elseif(resource?==?null?&&?url?!=?null?&&?mapperClass?==?null)?{?//?若配置的是url,在解析url對應(yīng)的mapper.xml
ErrorContext.instance().resource(url);
InputStream?inputStream?=?Resources.getUrlAsStream(url);
XMLMapperBuilder?mapperParser?=?new?XMLMapperBuilder(inputStream,?configuration,?url,?configuration.getSqlFragments());
mapperParser.parse();
}elseif(resource?==?null?&&?url?==?null?&&?mapperClass?!=?null)?{?//?若配置的是class,在解析class對應(yīng)的mapper文件
Class?mapperInterface?=?Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);?//?分析addMapper()方法
}else{
throw?new?BuilderException("A?mapper?element?may?only?specify?a?url,?resource?or?class,?but?not?more?than?one.");
}
}
}
}
}
因為我采用的是class配置,所以分析configuration.addMapper()方法
//?Configuration.java
public??void?addMapper(Classtype)?{
mapperRegistry.addMapper(type);
}
繼續(xù)進(jìn)入mapperRegistry.addMapper進(jìn)行分析
//?MapperRegistry.java
public??void?addMapper(Classtype)?{
if(type.isInterface())?{?//?mapper接口
if(hasMapper(type))?{?//?若mapper已被注冊
throw?new?BindingException("Type?"+type+"?is?already?known?to?the?MapperRegistry.");
}
boolean?loadCompleted?=false;
try?{
knownMappers.put(type,?new?MapperProxyFactory(type));??//?注冊映射接口
//?It's?important?that?the?type?is?added?before?the?parser?is?run
//?otherwise?the?binding?may?automatically?be?attempted?by?the
//?mapper?parser.?If?the?type?is?already?known,?it?won'
t?try.
MapperAnnotationBuilder?parser?=?new?MapperAnnotationBuilder(config,type);?//?生成注解構(gòu)造器
parser.parse();?//?解析mapper上的注解
loadCompleted?=true;
}?finally?{
if(!loadCompleted)?{
knownMappers.remove(type);
}
}
}
}
knownMappers.put(type, new MapperProxyFactory<T>(type));這里很重要,是注冊mapper文件代理對象。此處只做緩存的解釋,不做注冊詳解,后面在分析sql執(zhí)行流程時單獨去分析。
parser.parse()是對mapper文件進(jìn)行解析的關(guān)鍵,繼續(xù)分析
//?MapperAnnotationBuilder.java
//?解析配置文件
public?voidparse()?{
String?resource?=?type.toString();?//?接口的全限定名?class?com.test.userMapper
if(!configuration.isResourceLoaded(resource))?{??//?是否加載過
loadXmlResource();?//?在默認(rèn)路徑下(默認(rèn)和mapper接口同個包下),加載xml文件
configuration.addLoadedResource(resource);?//?設(shè)為該mapper配置文件已解析
assistant.setCurrentNamespace(type.getName());?//?設(shè)置構(gòu)建助力器當(dāng)前命名空間?com.test.userMapper
parseCache();?//?解析CacheNamespace注解,構(gòu)建一個Cache對象,并保存到Mybatis全局配置信息中
parseCacheRef();?//解析CacheNamespace注解,引用CacheRef對應(yīng)的Cache對象。
//?由此可知,當(dāng)引入了和后,該命名空間的緩存對象變?yōu)榱薈acheRef引用的緩存對象
Method[]?methods?=?type.getMethods();?//?獲取方法
for(Method?method?:?methods)?{
try?{
if(!method.isBridge())?{?//?issue#237?若該方法不是橋接方法
parseStatement(method);?//構(gòu)建MapperStatement對象,并添加到Mybatis全局配置信息中
}
}?catch?(IncompleteElementException?e)?{
//當(dāng)出現(xiàn)未完成元素時,添加構(gòu)建Method時拋出異常的MethodResolver實例,到下個Mapper的解析時再次嘗試解析
configuration.addIncompleteMethod(new?MethodResolver(this,?method));
}
}
}
parsePendingMethods();?//?解析未完成解析的Method
}
通過上面的代碼注釋,可知,當(dāng)解析mapper.java文件前,會先在同個文件夾下查看是否存在mapper.xml文件,若存在,則先解析mapper.xml文件。在解析mapper.xml文件時,若在mapper.xml中寫了緩存<cache/>或<cache-ref>,也會生成二級緩存。若同時還在mapper.java文件里寫了@CacheNamespace注解。則會進(jìn)行報錯,因為出現(xiàn)了兩個緩存。此時我們根據(jù)注解配置去分析。去分析parseCache()和parseCacheRef(),看配置了注解@CacheNamespace和CacheNamespaceRef之后緩存具體怎樣生成。
//?MapperAnnotationBuilder.java
private?voidparseCache()?{
//?獲取是否有@CacheNamespace?注解
CacheNamespace?cacheDomain?=?type.getAnnotation(CacheNamespace.class);
if(cacheDomain?!=?null)?{
/*
*?構(gòu)建一個緩存對象,具體分析
*?*/
assistant.useNewCache(cacheDomain.implementation(),?cacheDomain.eviction(),?cacheDomain.flushInterval(),?cacheDomain.size(),?cacheDomain.readWrite(),?null);
}
}
//?mapperBuilderAssistant.java
public?Cache?useNewCache(Class?typeClass,?//?基本緩存類
Class?evictionClass,??//?緩存裝飾類
Long?flushInterval,?//?緩存刷新間隔
Integer?size,?//?緩存大小
boolean?readWrite,?//?緩存可讀寫
Properties?props)?{
typeClass?=?valueOrDefault(typeClass,?PerpetualCache.class);?//?沒有設(shè)置則采用默認(rèn)的PerpetualCache
evictionClass?=?valueOrDefault(evictionClass,?LruCache.class);?//?沒有設(shè)置則采用默認(rèn)的LruCache
Cache?cache?=?new?CacheBuilder(currentNamespace)?//?命名空間作為緩存唯一ID
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.properties(props)
.build();
configuration.addCache(cache);?//?加入到全局緩存
currentCache?=?cache;?//?當(dāng)前緩存設(shè)為cache,由此可知,緩存是mapper級別
returncache;
}
此處是生成了二級緩存的地方,并設(shè)置當(dāng)前mapper文件的緩存為這個生成的二級緩存。若沒有配置@CacheNamespaceRef,那木此mapper文件就使用了這個自己生成的二級緩存。那@CacheNamespaceRef是用來干嘛的?回到上面代碼處進(jìn)行分析。
//?MapperAnnotationBuilder.java
private?voidparseCacheRef()?{
//?@CacheNamespaceRef?相當(dāng)于標(biāo)簽
CacheNamespaceRef?cacheDomainRef?=?type.getAnnotation(CacheNamespaceRef.class);
if(cacheDomainRef?!=?null)?{
assistant.useCacheRef(cacheDomainRef.value().getName());?//?構(gòu)建緩存引用,進(jìn)入分析
}
}
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;?//?設(shè)置當(dāng)前緩存對象為被引用的緩存對象
unresolvedCacheRef?=false;?//?標(biāo)志設(shè)置為false,代表有緩存引用。
returncache;
}?catch?(IllegalArgumentException?e)?{
throw?new?IncompleteElementException("No?cache?for?namespace?'"+?namespace?+"'?could?be?found.",?e);
}
}
由上文可知,當(dāng)配置了@CacheNamespaceRef和@CacheNamespace后,該mapper文件對應(yīng)的緩存以@CacheNamespaceRef引用的緩存為準(zhǔn)。這樣可是使得不同的mapper文件有相同的緩存。
4.2?緩存具體使用場景
上文說了,開啟二級緩存后,sqlSession中的Executor是CachingExecutor,查看生成CachingExecutor具體位置。繼續(xù)從那段測試代碼分析
SqlSession?sqlSession?=?factory.openSession();?//?生成SqlSession
List?userList?=?sqlSession.selectList("com.xiaobing.mapper.SysUserMapper.getSysUser");?//?執(zhí)行SysUserMapper類的getSysUser方法
debug進(jìn)入DefaultSqlSessionfactory.openSession()方法
//?DefaultSqlSessionfactory.java
public?SqlSessionopenSession()?{
returnopenSessionFromDataSource(configuration.getDefaultExecutorType(),?null,false);
}
...
private?SqlSession?openSessionFromDataSource(ExecutorType?execType,?TransactionIsolationLevel?level,?boolean?autoCommit)?{
Transaction?tx?=?null;
try?{
final?Environment?environment?=?configuration.getEnvironment();?//?獲取當(dāng)前配置設(shè)置的環(huán)境,有事務(wù)工廠,數(shù)據(jù)源
final?TransactionFactory?transactionFactory?=?getTransactionFactoryFromEnvironment(environment);?//?創(chuàng)建事務(wù)工廠
tx?=?transactionFactory.newTransaction(environment.getDataSource(),?level,?autoCommit);?//?事務(wù)類
final?Executor?executor?=?configuration.newExecutor(tx,?execType);?//?生成執(zhí)行器
returnnew?DefaultSqlSession(configuration,?executor,?autoCommit);
}?catch?(Exception?e)?{
closeTransaction(tx);?//?may?have?fetched?a?connection?so?lets?call?close()
throw?ExceptionFactory.wrapException("Error?opening?session.??Cause:?"+?e,?e);
}?finally?{
ErrorContext.instance().reset();
}
}
....
分析Executor executor = configuration.newExecutor(tx, execType);此段代碼
//?Configuration.java
public?Executor?newExecutor(Transaction?transaction,?ExecutorType?executorType)?{
executorType?=?executorType?==?null???defaultExecutorType?:?executorType;?//?默認(rèn)為SimpleExecutor
executorType?=?executorType?==?null???ExecutorType.SIMPLE?:?executorType;
Executor?executor;
.......
if(cacheEnabled)?{???//?若開啟二級緩存,則生成CachingExecutor
executor?=?new?CachingExecutor(executor);
}
.......
}
當(dāng)執(zhí)行查詢語句時,會執(zhí)行Executor的query()方法。分析CachingExecutor中query()方法究竟是怎樣使用二級緩存。
public??List?query(MappedStatement?ms,?Object?parameterObject,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)
throws?SQLException?{
//?mapper.xml設(shè)置了或者mapper.java使用了二級緩存注解
Cache?cache?=?ms.getCache();
if(cache?!=?null)?{
//?若該mapper文件中執(zhí)行的上一條語句是更新語句(增刪改),則會清空該mapper文件對應(yīng)的二級緩存
flushCacheIfRequired(ms);
if(ms.isUseCache()?&&?resultHandler?==?null)?{
ensureNoOutParams(ms,?parameterObject,?boundSql);
@SuppressWarnings("unchecked")
List?list?=?(List)?tcm.getObject(cache,?key);?//?從二級緩存中獲取
if(list?==?null)?{?//?若二級緩存中不存在
list?=?delegate.?query(ms,?parameterObject,?rowBounds,?resultHandler,?key,?boundSql);?//?調(diào)用后續(xù)的Executor執(zhí)行語句,后續(xù)的Executor會繼續(xù)使用一級緩存。
tcm.putObject(cache,?key,?list);?//?issue#578.?Query?must?be?not?synchronized?to?prevent?deadlocks??//?放入二級緩存中
}
returnlist;
}
}
returndelegate.?query(ms,?parameterObject,?rowBounds,?resultHandler,?key,?boundSql);?//?若沒開啟二級緩存,則調(diào)用后續(xù)的Executor執(zhí)行語句。后續(xù)的Executor會繼續(xù)使用一級緩存。
}
//?此處的update包括增刪改
public?int?update(MappedStatement?ms,?Object?parameterObject)?throws?SQLException?{
//?清空二級緩存
flushCacheIfRequired(ms);
returndelegate.update(ms,?parameterObject);
}
通過上面分析可知,二級緩存的實現(xiàn)是mapper級別的。只要對這個mapper文件使用@CacheNamespace注解或?qū)?yīng)的xml使用等標(biāo)簽,那木該mapper在生成時就會注冊一個mapper級別的緩存。在后續(xù)
對這一mapper文件任何查詢語句進(jìn)程操作的時候,都會使用到這個二級緩存。二級緩存就相當(dāng)于在一級緩存上在加入一個緩存。二級緩存Cache的實現(xiàn)是在LruCache上在封裝了一層TransactionCache,為了防止臟數(shù)據(jù)的產(chǎn)生。感興趣的可以自行去查看。以上便是關(guān)于mybatis緩存的內(nèi)容。
5. 總結(jié)驗證
我們知道,二級緩存是mapper級別的,在mybatis初始化時便生成了。當(dāng)此mapper文件中有更新語句時,才會刷新二級緩存。舉個例子,有MapperA.java和MapperB.java兩個文件,并都開啟了二級緩存,cacheA和cacheB。MapperA.java中有一條查詢語句select1,此查詢語句關(guān)聯(lián)了B的表。在第一次執(zhí)行MapperA.java中select1時,會從庫中取出數(shù)據(jù),并放入在cacheA中。當(dāng)mapperB.java中如果有一條更新語句update2,執(zhí)行update2,會刷新二級緩存cacheB。但不會刷新cacheA,因為update2并不在MapperA.java中。那此時cacheA中存在的數(shù)據(jù)便是臟數(shù)據(jù)了。
其實也有解決辦法,即在MapperA.java中使用@CacheNamespaceRef = "mapperB.java".讓兩個文件公用同一個二級緩存。這樣就OK啦