myBatis源碼解析-二級緩存的實現(xiàn)方式

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啦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容