前言
上一篇博客【Mybatis-Spring源码分析(二) Mapper接口代理的生成】主要说了Mybatis
的注解是怎么使用代理去调用Mapper
接口中的查询方法的。本篇则会侧重讲解调用接口的方法的执行过程。为什么是血案呢,因为Mybatis
的一级缓存在Mybatis-Spring
中是失效的,虽然笔者之前已经阐述过一级缓存十分的鸡肋,本篇也会源码角度探究一下为什么会导致失效的。更多Spring内容进入【Spring解读系列目录】。
代理对象如何执行SQL语句
我们上篇说到执行SQL语句的是一个Mapper
接口的代理的invoke()
方法。也就是用MapperProxy.invoke()
里面执行的excute()
方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
既然要执行sql,我们就选一个例子讲解,比如下面的CityMapper
。
public interface CityMapper {
@Select("select * from city")
public List<Map<String,Object>> list();
}
准备好以后点进execute()
方法,看看里面写了什么:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//这里的command是在MapperMethod的构造方法里初始化的,就是获取了接口方法上的注解里面的内容
// this.command = new SqlCommand(config, mapperInterface, method);
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
//。。。。。。无关紧要略
return result;
}
可以看到里面其实就是一个Switch
语句,用来判断是哪个注解用的,例子里是@Select
,所以就走到这个分支里去case SELECT
。由于要查的是select *
,因此一定会走到if (method.returnsMany())
的分支里,接着往下进入executeForMany(sqlSession, args)
方法:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
//。。。。。。无关紧要略
return result;
}
这里的if判断method.hasRowBounds()
是为了看我们的select *
语句是不是有条件,没有,所以走else
语句块,接着进入sqlSession.selectList(command.getName(), param)
:
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.sqlSessionProxy.selectList(statement, parameter);
}
到了这里可以停住了。因为再往下进入就发现这已经再用一个代理对象去执行了。
//SqlSessionTemplate#selectList。注意这里使用的已经是sqlSessionProxy了
// 原生Mybatis这里使用的则是defaultSqlSession.selectList()
public <E> List<E> selectList(String statement, Object parameter) {
//注意这个代理方法,此时selectList将会被下文的invoke()调用起来。
return this.sqlSessionProxy.selectList(statement, parameter);
}
从理论上分析这个sqlSessionProxy
应该是DefaultSqlSession
对象,但是从这个对象的名字上看这是一个代理对象,事实上也就是一个代理对象。因此代理对象的代码是看不到的,怎么执行的呢?一定是用的代理类的invoke()
方法执行的。因此程序在运行时往下走一定不是走的DefaultSqlSession
,而是一个代理类的invoke()
方法。笔者接了一个运行时的图展示一下,可以看到这个对象实际是$Proxy17
。
它的值是org.apache.ibatis.session.defaults.DefaultSqlSession@13f95696
。如果点击下一步就会跳到SqlSessionInterceptor
里面:
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//暂时略
}
这个SqlSessionInterceptor
是什么先不说,这里需要先解释一下为什么变成了DefaultSqlSession
。
DefaultSqlSession和SqlSessionTemplate
要解释这个问题需要先把这两个类的关系弄清楚。DefaultSqlSession
是原版Mybatis
执行SQL语句用的SqlSession
,SqlSessionTemplate
是Mybatis
为了和Spring
结合,而给DefaultSqlSession
生成代理类用的一个工具类,因此叫做Template
。上面说到调用selectList()
是一个代理类调用的invoke()
方法实现的,那么代理是什么时候做的呢?这就要打开SqlSessionTemplate
的构造方法了:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
//。。。。。。不重要,略
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//看这里生成代理对象
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
在构造方法里面一眼就能看到SqlSessionTemplate
在被实例化的时候就会对SqlSession
进行一个代理,并且如果需要执行方法就用SqlSessionInterceptor
去代理执行,看到这里是不是就接上了。
SqlSessionInterceptor代理
那么剩下的就交给了SqlSessionInterceptor#invoke()
方法了,省略去异常:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
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)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) { //。。。。。。略去
} finally {
if (sqlSession != null) { //看这里关闭session
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
首先一开始还是拿到了一个SqlSession
,然后使用了另一个invoke()
方法执行,最后返回result
。
执行SQL
通篇我们发现没有sql
执行,那么sql
在哪里执行的呢?其实就是method.invoke(sqlSession, args)
。这里拿出来的SqlSession
就是DefaultSqlSession
,而method
代理的方法就是selectList()
。因此这里的invoke()
真正执行的代码行就是DefaultSqlSession#selectList()
,只不过这里是由代理执行的,我们无法看到代码而已。而外面写的则是this.sqlSessionProxy.selectList(statement, parameter);
。
//下面是代理执行的,无法调试到代码,只是贴出来做一个演示。
public <E> List<E> selectList(String statement, Object parameter) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
血案
那么SQL
语句执行完以后,关注finally{}
代码块,大家看到了什么?closeSqlSession()
方法。也就是说Mybatis-Spring
在每一次执行完SQL
语句以后把SqlSession
关了。如果把SqlSession
关闭了,那么缓存中就不可能再有相关的数据,因此Mybatis
的一级缓存当然会失效了。
代理中的SqlSession为什么要关闭
既然Mybatis
把它关了,就必然有它的道理。其实想想十分的简单:原生的Mybatis
是把SqlSession
暴露了出来,因此用户想要关闭的时候就可以关闭Session
。但是Mybatis-Spring
不可以这样,因为暴露出来的是代理,如果不在代理的invoke()
里关闭,Spring
就再也无法拿到这个SqlSession
,一旦错过了就无法关闭了。但是如果暴露出来这个代理给用户,那么Mybatis
就和Spring
绑定的过于紧密,这肯定是Mybatis
的开发人员不愿意做的,毕竟笔者再第一篇Mybatis
帖子里已经说了,这些人为了对Spring
解耦做了相当大的改动。这就是前言里说的,一级缓存失效的原因。
总结
那么本篇博客到此就把一个完整的Mybatis-Spring
执行SQL
语句的流程说完了,画个图总结一下,下一篇我们会说一下Mybatis-Spring
的流程。