目錄
- 1. 結論
- 2. Show code
- 2.1 SimpleExecutor
- 2.2 CacheExecutor
- 3 細說
老規矩,先說結論。給各位大兄弟來點總結。
mybatis有兩級緩存,分别是:
- SqlSession級别
- Mapper級别
想必大家都對這個結論不陌生,但是有許多人其實并不明白具體原因。是以今天就和各位大兄弟一起來探讨一下具體代碼。
這個緩存就得從建立執行器開始,
org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
,建立執行器是從建立SqlSession開始的, 這個看過上一篇文章的大兄弟應有了解,就不一一說啦。先上代碼。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) { // 是否開啟二級緩存,這個屬性是從配置檔案中解析出來的。二級緩存預設是true.
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
這段代碼,我們可以看出,如果在配置檔案中,沒有開啟二級緩存,則會直接建立一個Executor, 如果開啟了二級緩存,則會把建立的執行器進行包裝。 那我們就從沒有開啟二級緩存檢視。(為什麼要直接分析SimpleExecutor先不說,賣個關子)
直接點進SimpleExecutor,發現沒啥東西,直接點進父類構造函數。(BaseExecutor有三個預設實作,分别是:
BatchExecutor
,
ReuseExecutor
SimpleExecutor
。是以小夥伴不要糾結為什麼選擇SimpleExecutor了)代碼如下。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
點進父類的構造函數,我們大概也看出來了,在父類的構造函數中,的确存在名字叫localCache。 那不行,我們隻看到了Cahce,得找到使用的地方。緩存一般都是用來緩存查詢内容的,那我們就去找select,query方法咯。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 用來延遲加載,表示目前key已經被緩存
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 放入緩存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
從一個query方法中,點到最後。我們不用關注其中細節,在最終query方法中,的确是放入緩存中。我們看一下localCache的具體類型。代碼如下。
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
}
很簡單,就是一個id 和 一個map。 ,這也就是說明了,mybatis是用map來緩存資料的。
我們看到現在,算是明白了,mybatis中的一級緩存是自動開啟的,不需要什麼配置檔案,也隻是用map來緩存的。
從上面代碼看,CacheExecutor是對SimpleExecutor進行了包裝。那就進代碼看看。
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
// 進行包裝, 從實作類字面了解,就是延遲加載
delegate.setExecutorWrapper(this);
}
我們不關注這麼多, 我們就一個目的,搞清楚這個cache。 老規矩,查緩存從select或query方法找起來。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 從mappedStatement中擷取緩存。
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 把cache和key傳遞進去,檢視是否有緩存資料
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);
}
我們基本看出來,這個
ms.getCache()
是至關重要的,因為在代碼中,接下來的操作都和這個cache有關系。 那麼唯一的一件事就是搞清楚這個cache是從哪來的。我們就從MappedStatement的方方法開始看,這個cache是如何生成的。
public final class MappedStatement {
....
public Builder cache(Cache cache) {
mappedStatement.cache = cache;
return this;
}
...
}
我們不難看出來,這個Cache通過傳遞進來,然後直接指派給了MappedStatement的變量。 那麼我們就點選一下這個方法,看被哪調用。
org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement(...){
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
...
.useCache(valueOrDefault(useCache, isSelect))
// 傳入cache
.cache(currentCache);
}
我們發現被這個方法,這個方法有點熟悉,就是我們前一篇文章講到的,在解析mapper,建構MappedStatement的方法。這個傳入的currentCache不是當期方法的,是目前類的,那我們就要看當那類是怎麼建構出來的。我們直接看這個方法被哪調用,然後順藤摸瓜,找到建立的地方。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
...
// 通過使用builderAssistant添加mapper. 我們要接着看builderAssistant是如何建構的
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
note: 看到這些代碼并不陌生,我們現在反着推斷了。 有的小夥伴肯定好奇,我是如何找到這些方法的, 如何你有這個疑問,就需要看一下前一篇mybatis的文章啦!!
因為builderAssistant是類成員變量,那我們接着看parseStatementNode()方法被哪調用,然後找到建構builderAssistant的地方
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
到這仍發現builderAssistant是傳遞過來的,那我們就接着往上翻代碼。找到目前類是如何建構的。
....
一直往上翻,有很多代碼,道理相同,我們最後找到了如下代碼。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 設定namespace
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
// 建構cache
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 解析mappedStatement
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
...
}
}
這個熟悉的感覺。 我們看大了給builderAssistant設定namespace。并且有關于cache的代碼,這個時候就不要點走啦,得進去看看了,因為已經到了我們關心的點。點進
cacheElement(context.evalNode("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;
}
發現最終調用了
org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
的方法,我們也如願看到了cache的産生,并且是以namespace為機關的。
自此,我們搞清楚了一條大概的邏輯。
graph TD
MappedStatement1 --> |imp|Mapper1
MappedStatement2 --> |imp|Mapper1
MappedStatement4 --> |imp|Mapper2
MappedStatement5 --> |imp|Mapper2
Mapper1 --> |imp|Cache1
Mapper2 --> |imp|Cache2
Cache1 --> |namespace1|builderAssistant1((builderAssistant))
Cache2 --> |namespace2|builderAssistant1((builderAssistant))
現在大體邏輯是搞懂了,但是代碼可能各位大兄弟看着有些糊塗,建議在電腦自己看看,加深一下印象。 畢竟看别人千遍,不如自己來實作一遍。
你的每一個點贊,我都當做喜歡