Mybatis源碼閱讀 之 玩轉Executor

  • 2019 年 10 月 26 日
  • 筆記

承接上篇部落格, 本文探究MyBatis中的Executor, 如下圖: 是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體系

小結

其實上面創建這個Cache對象才是二級快取者, 前面說的那個CachingExecutor中的TransactionalCacheManager不過是擁有從這個Cache中獲取數據的能力而已

我有調試他是如何從Cache中獲取出快取,事實證明,二級快取中存放的不是對象,而是被序列化後存儲的數據,需要反序列化出來

下圖是Mybatis反序列化數據到新創建的對象中的截圖

反序列化

下圖是TransactionalCacheManager是如何從Cache中獲取數據的調用棧的截圖

從caching中獲取數據調用棧

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

第一點: 通過以上程式碼的調用順序也能看出,二級快取在一級快取之前優先被執行, 也就是說二級快取不存在,則查詢一級快取,一級快取再不存在,就查詢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-賜我白日夢,如果有錯誤歡迎指出,也歡迎您點贊支援…