我曾跨入山巅,也曾步入低谷,两者都使我受益良多! 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源创计划”,欢迎正在阅读的你也加入,一块儿分享。