Mybatis-Spring源码分析(三) 执行SQL导致的血案

本文深入剖析了Mybatis-Spring中SQL语句的执行过程,从Mapper接口的代理执行到SqlSession的生命周期,揭示了为何在Mybatis-Spring中一级缓存会失效。通过分析`SqlSessionTemplate`和`SqlSessionInterceptor`的角色,展示了代理对象如何调用`invoke()`方法执行SQL,并最终在每次执行后关闭SqlSession,导致一级缓存无法保持。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

上一篇博客【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语句用的SqlSessionSqlSessionTemplateMybatis为了和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的流程。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值