Mybatis 緩存 源碼分析

1. 結論

老規矩,先說結論。給各位大兄弟來點總結。

mybatis有兩級緩存,分別是:

  • SqlSession級別
  • Mapper級別

想必大家都對這個結論不陌生,但是有許多人其實並不明白具體原因。所以今天就和各位大兄弟一起來探討一下具體代碼。

2. Show code

這個緩存就得從創建執行器開始,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先不說,賣個關子)

2.1 SimpleExecutor

直接點進SimpleExecutor,發現沒啥東西,直接點進父類構造函數。(BaseExecutor有三個默認實現,分別是:BatchExecutorReuseExecutorSimpleExecutor。所以小夥伴不要糾結爲什麼選擇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來緩存的。

2.2 CacheExecutor

從上面代碼看,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爲單位的。

自此,我們搞清楚了一條大概的邏輯。

imp
imp
imp
imp
imp
imp
namespace1
namespace2
MappedStatement1
Mapper1
MappedStatement2
MappedStatement4
Mapper2
MappedStatement5
Cache1
Cache2
builderAssistant

3 細說

現在大體邏輯是搞懂了,但是代碼可能各位大兄弟看着有些糊塗,建議在電腦自己看看,加深一下印象。 畢竟看別人千遍,不如自己來實現一遍。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章