我曾跨入山巔,也曾步入低谷,兩者都使我受益良多! I've been to the top, and I've fallen to the bottom, and I've learned a lot from both!
1、概述
最近總是據說Spring和MyBtis集成後,一級緩存就不可用了!java
我就納悶了,爲何一級緩存不可用呢?這難道是Spring的BUG?這引發了我極大的興趣,由於Spring做爲一個極其優秀的項目管理框架,它竟然也有BUG,我要一探究竟,知足個人好奇心!git
2、真的沒走緩存
爲了幫助我查看源碼,我把MyBatis和Spring集成後寫了以下代碼:github
AnnotationConfigApplicationContext annotationConfigApplicationContext;
@Before
public void init(){
annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
}
@Test
public void selectTest(){
TestMapper bean = annotationConfigApplicationContext.getBean(TestMapper.class);
List<User> users = bean.selectUser("週六");
System.out.println(users);
List<User> users1 = bean.selectUser("週六");
System.out.println(users == users1);
}
講道理,以上代碼在常規的環境下,是必定會走一級緩存的,由於他知足一級緩存命中的條件,即同一個SqlSession
、 StatementId
相同,參數
相同、分頁條件
相同、查詢語句
相同、環境名稱
相同 六大命中規則,因此理論上,一級緩存是必定會命中的!可是事實上日誌以下:web
他竟然沒有走緩存,而是去查詢了兩遍數據庫,一級緩存華麗麗的的失效了,但是這道理是爲何呢?sql
3、失效的緣由
Spring做爲一個頂級項目管理框架,對於如此明顯的BUG,他不可能發現不了,及時真的發現不了,那麼github上使用者也不可能不提BUG,因而,我打斷點調試調試,看下源碼就是是如何來操做的!數據庫
從哪裏下手呢?剛剛咱們說過一級緩存的命中規則,2,3,4,5,6條規則必定是同樣的,由於我只是單純的複製了兩遍查詢,代碼上沒有變更,因此他的查詢語句、參數之類的條件必定是相同的,那麼最可能出現的條件就是第一條:同一個SqlSession
,難道說Spring集成MyBatis後,每一次查詢都是用了不一樣的SqlSession? 之前看過我文章的都應該知道,我以前分析過一篇關於MyBatis設計模式的文章,關於門面模式中說到過:每個SqlSession都會有一個惟一的執行器(Executor)與之對應
,因此說若是想驗證是否是同一個SqlSession,只須要驗證兩次使用的執行器是否是一個就OK了,說作就作,我在BaseExecutor#query
方法上斷點,結果以下:設計模式
果真不出我所料,兩次查詢走的根本不是一個執行器,那麼也就必定不是一個SqlSession,這下只掉緣由了,可是爲何呢?緩存
4、罪魁禍首
經過上圖的斷點咱們能夠看出來,正常狀況下,咱們的Mapper代理裏面所包含的應該是DefaultSqlSession
對象,可是經過整合Spring後咱們發現,咱們的SqlSession對象被偷樑換柱了,換成了SqlSessionTemplate
類,咱們進入到這個類中:微信
public class SqlSessionTemplate implements SqlSession, DisposableBean {...}
發現這個類也繼承了SqlSession
接口,那就好辦了,那麼查詢的方法必定是通過Select方法來實現的,咱們進入到他的selectList
方法,看下他的實現邏輯:session
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.sqlSessionProxy.selectList(statement, parameter);
}
咱們發現,這個方法內部內部的查詢彷佛又交給了一層代理,由這一層代理去真正執行的查詢操做,咱們彷佛快找到緣由了:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
... 忽略沒必要要的代碼...
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
果不其然,這個對象在初始化的時候,將這個代理對象也連帶着初始化了,這個正是使用的JDK的動態代理來實現的,熟悉動態代理的同窗可能會知道,JDK動態代理的精髓也就是InvocationHandler
的子類,也就是SqlSessionInterceptor
,咱們進入到裏面看一下他的實現:
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//獲取SqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
//反射調用真正的處理方法
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
//提交數據
sqlSession.commit(true);
}
//返回查詢的數據
return result;
} catch (Throwable t) {
//。。。。忽略沒必要要代碼
} finally {
if (sqlSession != null) {
//關閉SqlSession的鏈接
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
既然SqlSession不一致,那麼確定是在獲取SqlSession的時候,裏面實現了一些邏輯,從而形成了 SqlSession的不一致,咱們進入到getSqlSession
方法中:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
//...忽略沒必要要代碼....
//從ThreadLocal變量裏面獲取當前的SqlSession的處理器
SqlSessionHolder holder =
(SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
//若是事務同步管理器處於活動狀態則從SqlSessionHolder獲取Session
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
//若是SqlSessionHolder中獲取的SqlSession爲空,則新建一個SqlSession
session = sessionFactory.openSession(executorType);
//若事務同步管理器處於活動狀態則將SqlSession設置到SqlSessionHolder中保存起來,以便下次使用
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
原來如此,原來並非說Spring使MyBatis的一級緩存失效了,而是由於Spring只有在開啓了事務以後,在同一個事務裏的SqlSession會被緩存起來,同一個事務中,屢次查詢是能夠命中緩存的!咱們回到SqlSessionInterceptor#invoke
方法裏面,他在關閉的SqlSession的時候一樣對 是否開啓事務作了處理,咱們看closeSqlSession
方法的源碼:
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
//........忽略沒必要要的代碼
SqlSessionHolder holder =
(SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
//查看事務同步管理器是否存在 session
if ((holder != null) && (holder.getSqlSession() == session)) {
holder.released();
} else {
//若是不存在就將該Session關閉掉
session.close();
}
}
那麼,既然致使一級緩存失效的罪魁禍首咱們找到了,如何解決呢?
5、解決方案
爲何一級緩存失效,由於兩次查詢沒有使用同一個事物,那麼咱們加上同一個事物,看看狀況如何:
@Test
public void selectTest(){
TestMapper bean = annotationConfigApplicationContext.getBean(TestMapper.class);
//添加事務
DataSourceTransactionManager dataSourceTransactionManager =
annotationConfigApplicationContext.getBean(DataSourceTransactionManager.class);
TransactionStatus transaction =
dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());
List<User> users = bean.selectUser("週六");
System.out.println(users);
List<User> users1 = bean.selectUser("週六");
System.out.println(users == users1);
}
咱們這個時候來看一下結果:
果真不出我所料,一級緩存又被成功的使用上了。
古人云:耳聽爲虛,眼見爲實!只有真正的經歷過,才知道哪些是真,哪些是假!這一次調試源碼,不光讓我對Spring整合MyBatis有了一個總體的認知,更是讓我對動態代理有了一個更加深刻的瞭解,後續我會整理一下,分享出來!
才疏學淺,若是文章中理解有誤,歡迎大佬們私聊指正!歡迎關注做者的公衆號,一塊兒進步,一塊兒學習!
❤️「轉發」 和 「在看」 ,是對我最大的支持❤️
本文分享自微信公衆號 - JAVA程序狗(javacxg)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。