源码地址:https://github.com/nieandsun/mybatis-study


1 现象

如下图所示:
【Mybatis+spring整合源码探秘】--- Mybatis整合Spring后在非事务情况下一级缓存失效的底层原理_Mybatis
在非事务情况下连续两次调用userMapper的同一个查询方法,第二次却没有像Mybatis单独开发时一样走缓存 —》本文将从源码的角度来探索一下其具体的原因。


2 必备前置知识

通过文章《【Mybatis+spring整合源码探秘】— 创建Mapper动态代理类核心源码解读》和《【Mybatis源码探索】 — Mybatis查询过程核心源码解读 — mapper调用方式》已经知道Mapper对象其实是利用动态代理机制生成的动态代理对象,但是纯Mybatis开发和Mybatis/Spring框架整合开发还是有一些区别的:

(1)纯Mybatis开发生成的动态代理对象实际代理的SqlSession对象是DefaultSqlSession

(2)Mybatis/Spring框架整合开发时生成的动态代理对象实际代理的SqlSession对象是SqlSessionTemplate


在《【Mybatis源码探索】 — Mybatis查询过程核心源码解读 — mapper调用方式》那篇文章里我已经分析过纯Mybatis开发时mapper调用方法的底层原理 — 》从中可知Mybatis的一级缓存是默认开启的,且手动关闭SqlSession时,才会清空一级缓存,所以在纯Mybatis开发过程中如果连续使用同一mapper调用同一个接口,第二次会走一级缓存。

回看一下1中对应的代码,除了它没走一级缓存,还应该注意到的一点是,并不需要像纯Mybatis开发时一样,手动关闭SqlSession —> 这就有点意思了,不让手动关闭,那难道不需要管了??? 应该不会!!!那它肯定应该与第二次调用同一个接口没走缓存有一定的关系吧。。。带着这个问题,我们去源码中探寻Mybatis整合spring后一级缓存失效的底层原理。


3 源码探寻入口

本篇文章的入口如下:

userMapper.selectByPrimaryKey(id);

首先应该知道这里的userMapper是一个动态代理对象,进行上面的方法调用实际上会调用该对象对应的InvocationHandler对象里的invoke(…)方法,而这块源码我其实在《【Mybatis源码探索】 — Mybatis查询过程核心源码解读 — mapper调用方式》那篇文章的第3部分进行过具体的分析。但由于mybatis/spring整合开发时生成的动态代理对象和Mybatis单独开发时生成的动态代理对象代理的SqlSession并不是一个,所以两者的底层源码还是有一定区别的,而区别开始的地方正是利用SqlSession对象进行具体的数据库操作的地方。即下面真正拿着SqlSession对象进行具体增删改查的地方:
所在类MapperMethod

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: { //如果方法类型是“INSERT”的话调用sqlSession的insert方法
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: { //如果方法类型是“UPDATE”的话调用sqlSession的UPDATE方法
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {//如果方法类型是“DELETE”的话调用sqlSession的DELETE方法
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT: //如果方法类型是“SELECT”的话调用sqlSession的SELECT方法
      if (method.returnsVoid() && method.hasResultHandler()) { //没有返回值的情况
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) { //返回值有多个的情况
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) { //返回值为Map的情况
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) { //返回值为Cursor的情况
        result = executeForCursor(sqlSession, args);
      } else { //返回值为一个的情况
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param); //调用sqlSession的selectOne方法
        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());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result; //返回结果
}

【Mybatis源码探索】 — Mybatis查询过程核心源码解读 — mapper调用方式》那篇文章已经讲过,userMapper.selectByPrimaryKey(id);实际上会调用上面源码中对应的selectOne方法,即如下代码:

result = sqlSession.selectOne(command.getName(), param);

如果此时的sqlSession是DefaultSqlSession的话,其具体执行流程可参看我的这篇文章《【Mybatis源码探索】 — Mybatis查询过程核心源码解读 — 先聊聊selectOne方法》。

接下来将分析sqlSession为SqlSessionTemplate时源码的具体执行流程。


4 SqlSessionTemplate的selectOne(…)底层源码

SqlSessionTemplate对象里的selectOne(…)方法的源码如下:
所在类SqlSessionTemplate

@Override
public <T> T selectOne(String statement, Object parameter) {
  //statement为方法的全额限定名如:com.nrsc.mybatis.mapper.TUserMapper.selectByPrimaryKey
  //parameter正是方法的参数
  //但是这里的this.sqlSessionProxy却又是一个新的动态代理对象
  return this.sqlSessionProxy.selectOne(statement, parameter);
}

上面的this.sqlSessionProxy其实又是一个新的动态代理对象,它的生成就在SqlSessionTemplate的构造方法里,接下来我们看一下生成该对象的具体源码:
所在类SqlSessionTemplate

//由此可知,this.sqlSessionProxy是SqlSession类型的动态代理对象,
//且它对应的InvocationHandler对象是SqlSessionInterceptor
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class }, new SqlSessionInterceptor());

通过上面的代码分析可以知道this.sqlSessionProxy.selectOne(statement, parameter);这句代码最终会走SqlSessionInterceptor这个InvocationHandler对象的invoke(…)方法,接下来我们来看一下该invoke(…)方法的具体源码:
所在类SqlSessionInterceptor,该类实际上是SqlSessionTemplate的内部类

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  //生成真正访问数据库使用的SqlSession对象 ---> 其实这里生成的SqlSession正是DefaultSqlSession
  //底层源码暂时不细究了 --- 到讲整合spring事务的原理时再细究
  SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
      SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
  try {
    //使用DefaultSqlSession对象进行查询数据库 ---> 和之前的文章《【Mybatis源码探索】 --- 
    //Mybatis查询过程核心源码解读 --- 先聊聊selectOne方法》调用方式一样了
    Object result = method.invoke(sqlSession, args);
    
    //如果没有事务的话直接进行提交 --- 关于mybatis与spring事务我会再写一篇文章
    //这里是非事务情况下commit的地方,
    //事务的情况下会在哪里commit呢??? ---> 这肯定就与事务有关了,请待下篇讲解。
    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) { //报错的情况,暂时先不细究了=======================
    Throwable unwrapped = unwrapThrowable(t);
    if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
      // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
      closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      sqlSession = null;
      Throwable translated = SqlSessionTemplate.this.exceptionTranslator
          .translateExceptionIfPossible((PersistenceException) unwrapped);
      if (translated != null) {
        unwrapped = translated;
      }
    }
    throw unwrapped;
    //报错的情况,暂时先不细究了=======================
  } finally {
  	//无论怎样,都会强制关闭当前的sqlSession
    if (sqlSession != null) {
      closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
    }
  }
}

由上面的源码可知,SqlSessionTemplate底层还是用的DefaultSqlSession,而且mybatis使用事务的结合点应该就在下面这句话里:

SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
    SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

下篇文章会对其进行展开讲诉 — 》 可以进行测试,如果加上事务注解,一级缓存又会起作用了,具体原因相信大家肯定可以猜到,底层其实就是用到了ThreadLocal。

在没有事务时,getSqlSession(…)方法其实就是单纯的创建一个DefaultSqlSession对象,然后用该对象操作完数据库,就会进行commit —》清除当前DefaultSqlSession对象 —> 返回查询的结果。

而《【Mybatis源码探索】 — Mybatis查询过程核心源码解读 — 先聊聊selectOne方法》那篇文章里讲到过,一级缓存是放在SqlSession(即DefaultSqlSession)对象里的,在没有事务的情况下,DefaultSqlSession对象在用完就被清了 — 》 因此在Mybatis整合Spring后在非事务情况下,一级缓存其实是没用的。


5 为什么呢???

好好的一级缓存,为什么在非事务情况下就给弄没了呢??? 不知道大家有没有为此感到困惑。。。

首先相信大家肯定都知道Mybatis和Spring进行整合,其实是Mybatis方来主导的(据说Spring是想推自己的spring-data-jpa,所以不提供spring和mybatis的整合方式),也就是说这个事是Mybatis方做的。

那它为什么会把非事务情况下的一级缓存给干没了呢???我觉得应该与源码中的这段注释有关:

// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()

这句话的翻译过来的意思应该是:

由于某些数据库在调用close()之前需要进行提交/回滚,所以即使在非脏会话的情况下,也强制进行提交!!!

我觉得这应该就是在考虑分布式或多数据源的情况,既然你不用事务,应该也不会考虑回滚的事,所以干脆你就直接提交了吧,省的要是你不提交,影响到后续的操作呢??? — 当然这是我自己瞎猜的,欢迎评论交流,也欢迎批评指证。