Executor 执行链

Executor 执行链:doQuery、StatementHandler 与三种执行器

Executor 是 SqlSession 和 JDBC 之间的执行引擎:SqlSession 只是门面,SQL 怎么变成 Statement、参数怎么设、结果怎么映射,全在这条链上。
核心思路是三段分工——BaseExecutor 用模板方法定死流程骨架(缓存、事务、延迟加载),子类只实现 doQuery/doUpdate 决定 Statement 的生命周期策略,StatementHandler 再把「建 Statement、设参数、执行、映射结果」封装成对 JDBC 的最后一层适配。

// 基于 MyBatis 3.5.x (本地 JavaSourceReadingLab 仓), org.apache.ibatis.session.defaults.DefaultSqlSession
// 入口:selectList 只做一件事——用语句 id 换出 MappedStatement,然后甩给 executor
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);   // id -> 这条 SQL 的全部静态信息
    dirty |= ms.isDirtySelect();
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
// 基于 MyBatis 3.5.x (本地 JavaSourceReadingLab 仓), org.apache.ibatis.executor.BaseExecutor
// 模板方法层:query 定流程(缓存判断、嵌套查询计数、延迟加载),把真正落库的 doQuery 留给子类
public abstract class BaseExecutor implements Executor {

  protected Transaction transaction;
  protected Executor wrapper;          // 指向最外层装饰器(通常是 CachingExecutor),见原理串讲

  protected PerpetualCache localCache; // 一级缓存,02 已详讲,本篇只看它在链路里的位置
  protected int queryStack;            // 嵌套查询深度:>0 说明当前在处理子查询/延迟加载

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
      throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);                     // 动态 SQL 在这里解析成最终 SQL + 参数映射
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // id+分页+SQL+参数值+环境 五要素拼 key
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {   // 只有最外层查询才允许清缓存,嵌套查询清了会把外层正在用的结果清掉
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;                                       // 进入一层查询(可能是子查询)
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;  // 自定义 handler 时跳过缓存
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);        // 只对存储过程 OUT 参数有意义
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {                                // 回到最外层:所有子查询都已入缓存,才能安全做延迟加载
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();                                // STATEMENT 级:一级缓存只活一次查询
      }
    }
    return list;
  }

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
      ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);     // 先占位:告诉嵌套查询「这个 key 正在查,别递归进来」
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);  // 钩子,子类各显神通
    } finally {
      localCache.removeObject(key);                       // 无论成败先摘掉占位符,失败时不能留脏占位
    }
    localCache.putObject(key, list);                      // 真结果落一级缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

  // 四个抽象钩子:SIMPLE/REUSE/BATCH 的差别全部体现在这里的实现
  protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

  protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;

  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
      ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

  // ...
}
// 基于 MyBatis 3.5.x (本地 JavaSourceReadingLab 仓), org.apache.ibatis.executor.SimpleExecutor
// 默认执行器:一次查询 = 新建 Statement -> 执行 -> 立刻关闭,最简单也最不容易出错
public class SimpleExecutor extends BaseExecutor {

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
          boundSql);                             // 注意传的是 wrapper 不是 this,嵌套查询要从最外层装饰器重新进
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler); // 执行 + 结果映射
    } finally {
      closeStatement(stmt);                      // SIMPLE 的标志:用完即关,不复用
    }
  }

  @Override
  public List<BatchResult> doFlushStatements(boolean isRollback) {
    return Collections.emptyList();              // 没有攒任何 Statement,flush 无事可做
  }

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);   // 从 Transaction 拿连接,开 DEBUG 会包一层日志代理
    stmt = handler.prepare(connection, transaction.getTimeout());  // 建 Statement + 设超时/fetchSize
    handler.parameterize(stmt);                            // 给 ? 占位符设值
    return stmt;
  }

  // ...
}
// 基于 MyBatis 3.5.x (本地 JavaSourceReadingLab 仓), org.apache.ibatis.executor.statement 包
// RoutingStatementHandler:构造时按 statementType 一次性选好真实 handler,之后所有方法纯转发
public class RoutingStatementHandler implements StatementHandler {

  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
      ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:                             // 默认值,绝大多数场景走这里
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    return delegate.prepare(connection, transactionTimeout);   // 之后全是这种一行转发
  }

  // ...
}

// BaseStatementHandler:prepare 的公共骨架,建 Statement 的动作留给子类
public abstract class BaseStatementHandler implements StatementHandler {

  protected final ResultSetHandler resultSetHandler;   // 结果集 -> Java 对象
  protected final ParameterHandler parameterHandler;   // 参数 -> ? 占位符

  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);    // 子类决定建哪种 Statement
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);                       // 建到一半失败必须关,否则漏 Statement
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

  // ...
}

// PreparedStatementHandler:终点站,这里之下就是纯 JDBC
public class PreparedStatementHandler extends BaseStatementHandler {

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();                    // 此时 SQL 里动态部分已解析完,只剩 ? 占位符
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);  // useGeneratedKeys 的底层
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    }
    if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);         // 最常见的一行:MyBatis 到 JDBC 的交接点
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(),
          ResultSet.CONCUR_READ_ONLY);
    }
  }

  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);  // 逐个走 TypeHandler 调 ps.setXxx(i, value)
  }

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();                                      // 真正发 SQL 到数据库
    return resultSetHandler.handleResultSets(ps);      // ResultSet -> List<E>,另一条大链路
  }

  // ...
}

原理串讲

一次 selectList 的完整链路:DefaultSqlSession.selectList 用语句 id 从 Configuration 取出 MappedStatement,调 executor.query
四参数版 query 先做两件准备:ms.getBoundSql(parameter) 把动态 SQL 解析成带 ? 的最终 SQL,createCacheKey 用语句 id、RowBounds、SQL 文本、每个参数值、环境 id 拼出缓存 key——参与执行结果的每个变量都进了 key,这是缓存能安全命中的前提。
六参数版 query 查一级缓存,未命中则进 queryFromDatabase,占位、调 doQuery、回填缓存。
SimpleExecutor.doQuery 通过 configuration.newStatementHandler 拿到 RoutingStatementHandler(内部按 statementType 选出 PreparedStatementHandler),然后三步走:

代码块JAVA · 2 行收起展开
`handler.prepare` 经 `BaseStatementHandler.prepare` 调子类 `instantiateStatement`,落到 `connection.prepareStatement(sql)`;
`handler.parameterize` 委托 `ParameterHandler.setParameters` 给占位符设值;

handler.queryps.execute() 后把 ResultSet 交给 ResultSetHandler 映射成对象。
finally 里 closeStatement 关掉 Statement。

第一个值得问为什么的地方是 queryFromDatabase 里的 EXECUTION_PLACEHOLDER
MyBatis 支持嵌套查询:映射结果时某个属性可能触发另一条 select,而那条 select 可能又指回当前正在查的 key(循环引用)。
先往一级缓存放占位符,DeferredLoad.canLoad() 就能识别「这个 key 正在查、结果还没出来」,把加载推迟到 queryStack 归零之后统一执行,而不会递归发起重复查询甚至死循环。
queryStack 计数同理:flushCache="true" 的清缓存和延迟加载的触发都只在最外层(queryStack == 0)进行,否则子查询会破坏外层查询正依赖的缓存状态。

第二个是 doQuery 里传给 newStatementHandler 的为什么是 wrapper 而不是 this
开启二级缓存时真实 Executor 被 CachingExecutor 装饰,wrapper 保存的就是这个最外层引用(由 setExecutorWrapper 设置)。
StatementHandler 里的 ResultSetHandler 处理嵌套查询时要再次发起 executor.query——如果拿到的是被装饰的内层 this,子查询就会绕过二级缓存,造成同一条语句主查询走缓存、作为子查询却直查库的不一致。
装饰器模式的代价就在这:被装饰者内部的「自调用」必须显式持有最外层引用才能不丢装饰逻辑,这和 Spring AOP 里 this 调用不走代理是同一类问题。

RoutingStatementHandler 也值得一句:它在构造函数里做一次 switch 就把 delegate 定死,之后所有方法纯转发,没有任何逻辑。
这层看似多余的壳让 Configuration.newStatementHandler 只需要 new 一个类型,也给插件拦截 StatementHandler 提供了统一的代理点——拦截器只用包一个 Routing,不用关心底下三种实现。

三种 Executor 的差别全部落在 doQuery/doUpdate/doFlushStatements 这三个钩子上。
SIMPLE 如上,每次新建 Statement、用完即关,doFlushStatements 直接返回空列表。
REUSE(ReuseExecutor)维护一个 statementMap(SQL 文本 → Statement),prepareStatement 先查 map,同一条 SQL 第二次执行就复用已 prepare 的 Statement 只重新 parameterize,省掉重复的 prepare 往返;这些 Statement 攒到 doFlushStatements 时统一关闭清空。
BATCH(BatchExecutor)只对写生效:
doUpdate 不执行 SQL,只调 handler.batch(stmt)(底层是 ps.addBatch())把参数攒进当前 Statement,返回假行数 BATCH_UPDATE_RETURN_VALUE(Integer.MIN_VALUE + 1002),真正的执行推迟到 doFlushStatements 里逐个 stmt.executeBatch();
而且只有「连续且相同」的语句(sql.equals(currentSql) && ms.equals(currentStatement))才共享一个 Statement,写操作交错会切出多个批次。
BATCH 的 doQuery 会先调 flushStatements() 把攒着的写全部刷出去,否则同一会话里查询会读不到自己刚「写」的数据。

设计取舍

  • SIMPLE 是默认值不是性能最优:MyBatis 选它因为语义最简单——没有跨语句共享的 Statement 状态,任何一步失败影响面都只有当前这一次执行。
  • REUSE 的收益依赖「同一 SqlSession 内反复执行同一条 SQL」这个前提,Web 应用里 SqlSession 通常一请求一个,命中机会有限;连接池 + 驱动层 PreparedStatement 缓存往往已经覆盖了这个优化。
  • BATCH 的 update 返回值是假的(BATCH_UPDATE_RETURN_VALUE),拿它判断影响行数是经典误区,真实行数要等 flushStatements 返回的 BatchResult。
  • BATCH 只有连续相同的语句才进同一批次,循环里交替 insert 两张表会退化成逐条切换 Statement,批量收益归零。
  • Executor 钩子(doQuery)和 StatementHandler 分成两层不是过度设计:前者管 Statement 的生命周期策略(建几个、何时关),后者管单个 Statement 的操作细节(怎么建、怎么设参),三种 Executor 才能共享同一套 StatementHandler。

延伸阅读