承接上篇博客, 本文探究MyBatis中的Executor, 以下圖: 是Executor體系圖html
本片博客的目的就是探究如上圖中從頂級接口Executor中拓展出來的各個子執行器的功能,以及進一步瞭解Mybatis的一級緩存和二級緩存java
預覽:mysql
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;
當咱們經過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維護着)spring
在BaseExecutor的模本方法以前,其實省略了不少步驟,咱們上一篇博文中有詳細的敘述,感興趣能夠去看看,下面我就簡述一下: 程序員使用獲取到了mapper的代理對象,調用對象的findAll()
, 另外獲取到的sqlSession的實現也是默認的實現DefaultSqlSession
,這個sqlSession經過Executor嘗試去執行方法,哪一個Executor呢? 就是咱們當前要說的CachingExecutor
,調用它的query()
,這個方法是個模板方法,由於CachingExecutor
只知道在什麼時間改作什麼,可是具體怎麼作,誰取作取決於它的實現類sql
以下是BaseExecutor
的query()
方法數據庫
@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; }
從上面的代碼中,其實咱們就跟傳說中的Mybatis的一級緩存無限接近了,上面代碼中的邏輯很清楚,就是先檢查是否存在一級緩存,若是存在的話,就再也不去建立statement查詢數據庫了緩存
那問題來了,什麼是這個一級緩存呢? 一級緩存就是上面代碼中的localCache
,以下圖: 安全
再詳細一點就看下面這張圖:app
嗯! 原來傳說中的一級緩存叫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的屬性
清空一級緩存的方法就是BaseExecutor
的update()
方法
@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
是MyBatis提供的默認的執行器,他裏面封裝了MyBatis對JDBC的操做,可是雖然他叫XXXExecutor
,可是真正去CRUD的還真不是SimpleExecutor
,先看一下它是如何重寫BaseExecutor
的doQuery()
方法的
詳細的過程在這篇博文中我就不往外貼代碼了,由於我在上一篇博文中有這塊源碼的詳細追蹤
@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); } }
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
關於SimpleExecutor
如何關閉statement,在上面一開始介紹SimpleExecutor
時,我其實就貼出來了,下面再這個叫作closeStatement()
的函數詳情貼出來
protected void closeStatement(Statement statement) { if (statement != null) { try { statement.close(); } catch (SQLException e) { // ignore } } }
這個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
這個執行器相對於SimpleExecutor的特色是,它的update()
方法是批量執行的
執行器提交或回滾事務時會調用 doFlushStatements,從而批量執行提交的 sql 語句並最終批量關閉 statement 對象。
首先來講,這個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
建立出來的對象給誰了?
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-賜我白日夢,若是有錯誤歡迎指出,也歡迎您點贊支持...