1. 程式人生 > >Mybatis原始碼閱讀 之 玩轉Executor

Mybatis原始碼閱讀 之 玩轉Executor

承接上篇部落格, 本文探究MyBatis中的Executor, 如下圖: 是Executor體系圖

本片部落格的目的就是探究如上圖中從頂級介面Executor中拓展出來的各個子執行器的功能,以及進一步瞭解Mybatis的一級快取和二級快取

預覽:

  • BaseExecutor :實現了Executor的全部方法,包括對快取,事務,連線提供了一系列的模板方法, 這寫模板方法中留出來了四個抽象的方法等待子類去實現如下
protected abstract int doUpdate(MappedStatement ms, Object parameter)
 throws SQLException;

protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
 throws SQLException;

protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
 throws SQLException;

protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
 throws SQLException;
  • SimpleExecutor: 特點是每次執行完畢後都會將創建出來的statement關閉掉,他也是預設的執行器型別
  • ReuseExecutor: 在它在本地維護了一個容器,用來存放針對每條sql創建出來的statement,下次執行相同的sql時,會先檢查容器中是否存在相同的sql,如果存在就使用現成的,不再重複獲取
  • BatchExecutor: 特點是進行批量修改,她會將修改操作記錄在本地,等待程式觸發提交事務,或者是觸發下一次查詢時,批量執行修改

建立執行器

當我們通過SqlSessionFactory建立一個SqlSession時,執行openSessionFromDataBase()

方法時,會通過newExecutor()建立執行器:

    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) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

通過這個函式,可以找到上面列舉出來的所有的 執行器, MyBatis預設建立的執行器的型別的是SimpleExecutor,而且MyBatis預設開啟著對mapper的快取(這其實就是Mybatis的二級快取,但是,不論是註解版,還是xml版,都需要新增額外的配置才能使新增這個額外配置的mapper享受二級快取,二級快取被這個CachingExecutor維護著)

BaseExecutor 的模板方法

在BaseExecutor的模本方法之前,其實省略了很多步驟,我們上一篇博文中有詳細的敘述,感興趣可以去看看,下面我就簡述一下: 程式設計師使用獲取到了mapper的代理物件,呼叫物件的findAll(), 另外獲取到的sqlSession的實現也是預設的實現DefaultSqlSession,這個sqlSession通過Executor嘗試去執行方法,哪個Executor呢? 就是我們當前要說的CachingExecutor,呼叫它的query(),這個方法是個模板方法,因為CachingExecutor只知道在什麼時間改做什麼,但是具體怎麼做,誰取做取決於它的實現類

如下是BaseExecutorquery()方法

  @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());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      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--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

BaseExecutor維護的一級快取

從上面的程式碼中,其實我們就跟傳說中的Mybatis的一級快取無限接近了,上面程式碼中的邏輯很清楚,就是先檢查是否存在一級快取,如果存在的話,就不再去建立statement查詢資料庫了

那問題來了,什麼是這個一級快取呢? 一級快取就是上面程式碼中的localCache,如下圖:

再詳細一點就看下面這張圖:

嗯! 原來傳說中的一級快取叫localCache,它的封裝類叫PerpetualCache 裡面維護了一個String 型別的id, 和一個hashMap 取名字也很講究,perpetual意味永不間斷,事實上確實如此,一級快取預設存在,也關不了(至少我真的不知道),但是在與Spring整合時,Spring把這個快取給關了,這並不奇怪,因為spring 直接幹掉了這個sqlSession

一級快取什麼時候被填充的值呢?填充值的操作在一個叫做queryFromDataBase()的方法裡面,我截圖如下:

其中的key=1814536652:3224182340:com.changwu.dao.IUserDao.findAll:0:2147483647:select * from user:mysql

其實看到這裡,平時聽到的為什麼大家會說一級快取是屬於SqlSession的啊,諸如此類的話就是從這個看原始碼的過程中的出來的結果,如果你覺的印象不深刻,我就接著補刀,每次和資料庫打交道都的先建立sqlSession,建立sqlSession的方法會在創建出DefaultSqlSession之前,先為它建立一個Executor,而我們說的一級快取就是這個Executor的屬性

何時清空一級快取

清空一級快取的方法就是BaseExecutorupdate()方法

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 清空本地快取
    clearLocalCache();
    // 呼叫子類執行器邏輯
    return doUpdate(ms, parameter);
  }

SimpleExecutor

SimpleExecutor是MyBatis提供的預設的執行器,他裡面封裝了MyBatis對JDBC的操作,但是雖然他叫XXXExecutor,但是真正去CRUD的還真不是SimpleExecutor,先看一下它是如何重寫BaseExecutordoQuery()方法的

詳細的過程在這篇博文中我就不往外貼程式碼了,因為我在上一篇博文中有這塊原始碼的詳細追蹤

  @Override
 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

建立StatementHandler

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

雖然表面上看上面的程式碼,感覺它只會建立一個叫RoutingStatementHandler的handler,但是其實上這裡面有個祕密,根據MappedStatement 的不同,實際上他會建立三種不同型別的處理器,如下:

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        // 早期的普通查詢,極其容易被sql注入,不安全
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
       //  處理預編譯型別的sql語句
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
       // 處理儲存過程語句
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

建立PreParedStatement

點選進入上篇博文,檢視如何建立PreparedStatement

執行查詢

點選進入上篇博文,裡面有記錄如何執行查詢

關閉連線

關於SimpleExecutor如何關閉statement,在上面一開始介紹SimpleExecutor時,我其實就貼出來了,下面再這個叫做closeStatement()的函式詳情貼出來

  protected void closeStatement(Statement statement) {
    if (statement != null) {
      try {
        statement.close();
      } catch (SQLException e) {
        // ignore
      }
    }
  }

ReuseExecutor

這個ReuseExecutor相對於SimpleExecutor來說,不同點就是它先來的對Statement的複用,換句話說,某條Sql對應的Statement創建出來後被放在容器中儲存起來,再有使用這個statement的地方就是容器中拿就行了

他是怎麼實現的呢? 看看下面的程式碼就知道了

public class ReuseExecutor extends BaseExecutor {
    private final Map<String, Statement> statementMap = new HashMap();

    public ReuseExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

嗯! 所謂的容器,不過是一個叫statementMap的HashMap而已

下一個問題: 這個容器什麼時候派上用場呢? 看看下面的程式碼也就知道了--this.hasStatementFor(sql)

    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();
        Statement stmt;
        if (this.hasStatementFor(sql)) {
            stmt = this.getStatement(sql);
            this.applyTransactionTimeout(stmt);
        } else {
            Connection connection = this.getConnection(statementLog);
            stmt = handler.prepare(connection, this.transaction.getTimeout());
            this.putStatement(sql, stmt);
        }

        handler.parameterize(stmt);
        return stmt;
    }

最後一點: 當MyBatis知道發生了事務的提交,回滾等操作時,ReuseExecutor會批量關閉容器中的Statement

BatchExecutor

這個執行器相對於SimpleExecutor的特點是,它的update()方法是批量執行的

執行器提交或回滾事務時會呼叫 doFlushStatements,從而批量執行提交的 sql 語句並最終批量關閉 statement 物件。

CachingExecutor與二級快取

首先來說,這個CachingExecutor是什麼? 那就得看一下的屬性,如下:

public class CachingExecutor implements Executor {
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

讓我們回想一下他的建立時機,沒錯就是在每次建立一個新的SqlSession時創建出來的,原始碼如下,這就出現了一個驚天的大問號!!!,一級快取和二級快取為啥就一個屬於SqlSession級別,另一個卻被所有的SqlSession共享了? 這不是開玩笑呢? 我當時確實也是真的蒙,為啥他倆都是隨時用隨時new,包括上面程式碼中的TransactionalCacheManager也是隨時用隨時new,憑什麼它維護的二級快取就這麼牛? 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) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

先說一下,我是看到哪行程式碼後意識到二級快取是這麼特殊的,如下:大家也看到了,下面程式碼中的tcm.getObject(cache, key);,是我們上面新創建出來的TransactionalCacheManager,然後通過這個空白的物件的getObject()竟然就將快取中的物件給獲取出來了,(我當時忽略了入參位置的cache,當然現在看,滿眼都是這個cache)

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          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);
  }

我當時出現這個問題完全是我忽略了一部分前面解析配置檔案部分的原始碼,下面我帶大家看看這部分原始碼是怎麼執行的

一開始MyBatis會建立一個XMLConfigBuilder用這個builder去解析配置檔案(因為我們環境是單一的MyBatis,並沒有和其他框架整,這個builder就是用來解析配置檔案的)

我們關注什麼呢? 我們關注的是這個builder解析<mapper>標籤的,原始碼入下:

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      ...
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));

關注這個方法中的configuration.addMapper(mapperInterface);方法,如下: 這裡面存在一個物件叫做,MapperRegistry,這個物件叫做mapper的註冊器,其實我覺得這是個需要記住的物件,因為它出現的頻率還是挺多的,它幹什麼工作呢? 顧名思義,解析mapper唄? 我的當前是基於註解搭建的環境,於是它這個MapperRegistry為我的mapper生成的物件就叫MapperAnnotationBuilder見名知意,這是個基於註解的構建器

 public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

所以說我們就得去看看這個解析註解版本mapper的builder,到底是如何解析我提供的mapper的,原始碼如下:

  public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }

方法千千萬,但是我關注的是它的parseCache();方法,為什麼我知道來這裡呢? (我靠!,我找了老半天...)

接下來就進入了一個高潮,相信你看到下面的程式碼也會激動, 為什麼激動呢? 因為我們發現了Mybatis處理@CacheNamespace註解的細節資訊

private void parseCache() {
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
      Properties props = convertToProperties(cacheDomain.properties());
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
    }
  }

再往下跟進這個assistant.useNewCache()方法,就會發現,MyBatis將創建出來的一個Cache物件,這個Cache的實現類叫BlockingCache

創建出來的物件給誰了?

  • Configuration物件自己留了一份 (放在了 caches = new StrictMap<>("Caches collection");中)
  • 當前類MapperBuilderAssistant也保留一了一份
  • 最主要的是MappedStatement物件中也保留了一份mappedStatement.cache

說了這麼多了,附上一張圖,用來紀念建立這個Cache的成員

小結

其實上面建立這個Cache物件才是二級快取者, 前面說的那個CachingExecutor中的TransactionalCacheManager不過是擁有從這個Cache中獲取資料的能力而已

我有除錯他是如何從Cache中獲取出快取,事實證明,二級快取中存放的不是物件,而是被序列化後儲存的資料,需要反序列化出來

下圖是Mybatis反序列化資料到新建立的物件中的截圖

下圖是TransactionalCacheManager是如何從Cache中獲取資料的呼叫棧的截圖

二級快取與一級快取的互斥性

第一點: 通過以上程式碼的呼叫順序也能看出,二級快取在一級快取之前優先被執行, 也就是說二級快取不存在,則查詢一級快取,一級快取再不存在,就查詢DB

第二點: 就是說,對於二級快取來說,無論我們有沒有開啟事務的自動提交功能,都必須手動commit()二級快取才能生效,否則二級快取是沒有任何效果的

第三點: CachingExecutor提交事務時的原始碼如下:

  @Override
  public void commit(boolean required) throws SQLException {
    // 代理執行器提交
    delegate.commit(required);
    // 事務快取管理器提交
    tcm.commit();
  }

這就意味著,TransactionalCacheManager和BaseExecutor的實現類的事務都會被提交

為什麼說二級快取和以及快取互斥呢?可以看看BaseExecutor的原始碼中commit()如下: 怎麼樣? 夠互斥吧,一個不commit()就不生效,commit()完事把一級快取幹掉了

  @Override
  public void commit(boolean required) throws SQLException {
    if (closed) {
      throw new ExecutorException("Cannot commit, transaction is already closed");
    }
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

到這裡本文又行將結束了,總體的節奏還是挺歡快挺帶勁的,我是bloger-賜我白日夢,如果有錯誤歡迎指出,也歡迎您點贊支援...