Mybatis 都有哪些 Executor 执行器?

#Java#面试八股

Mybatis 都有哪些 Executor 执行器?

结论先行:MyBatis 有四种执行器——SimpleExecutor(默认,每次新建 Statement)、ReuseExecutor(按 SQL 复用 Statement)、BatchExecutor(攒批执行)、以及装饰性质的 CachingExecutor(二级缓存)。它们都继承 BaseExecutor,共用一级缓存与事务逻辑,差异集中在 doQuery / doUpdate / doFlushStatements 三个模板方法。

1. 继承结构

                 Executor(接口)
                     ▲
          ┌──────────┴───────────┐
   BaseExecutor(模板,一级缓存)  CachingExecutor(装饰器,二级缓存)
          ▲
   ┌──────┼──────────┬──────────────┐
SimpleExecutor  ReuseExecutor  BatchExecutor  ClosedExecutor(内部占位)

CachingExecutor 不是 BaseExecutor 的子类,它持有另一个 Executor 并在外层做二级缓存判断,是典型的装饰器模式。

2. 四种执行器对比

执行器Statement 策略适用场景注意点
SimpleExecutor每次执行新建 PreparedStatement,用完即关绝大多数场景(默认)无状态,最安全
ReuseExecutor以 SQL 为 key 缓存 Statement,同 SQL 复用同一会话内反复执行相同 SQL缓存不跨会话;Statement 未关前不能改结果集
BatchExecutor缓存 Statement,addBatch 攒批,flush 时 executeBatch批量插入/更新查询会强制 flush;自增主键要 flush 后才有值
CachingExecutor不直接执行,先查二级缓存开启 <cache/> 时自动包装只在 Executor 外层,不单独使用

3. 怎么指定

// 方式一:openSession 指定
try (SqlSession session = factory.openSession(ExecutorType.BATCH)) {
    OrderMapper mapper = session.getMapper(OrderMapper.class);
    for (Order o : orders) {
        mapper.insert(o);
    }
    session.flushStatements();
    session.commit();
}

// 方式二:全局默认执行器(mybatis-config.xml)
// <setting name="defaultExecutorType" value="REUSE"/>
mybatis:
  executor-type: reuse          # Spring Boot 配置项,等价于 defaultExecutorType

Spring 中若要某个 Mapper 走批量,通常单独定义一个 SqlSessionTemplate,而不是把全局执行器改成 BATCH——全局 BATCH 会影响所有查询,且容易忘记 flush。

4. BaseExecutor 的模板方法

// 简化后的骨架
public abstract class BaseExecutor implements Executor {
    protected PerpetualCache localCache;         // 一级缓存

    public <E> List<E> query(MappedStatement ms, Object parameter,
                             RowBounds rb, ResultHandler rh) {
        if (queryStack == 0 && ms.isUseCache()) {
            list = localCache.getObject(key);     // 命中直接返回
            if (list != null) return list;
        }
        if (closed) throw new ExecutorException("Executor was closed.");
        queryStack++;
        try {
            list = doQuery(ms, parameter, rb, rh); // 子类实现
        } finally {
            queryStack--;
        }
        if (queryStack == 0 && ms.isUseCache()) {
            localCache.putObject(key, list);
        }
        return list;
    }

    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter,
                                           RowBounds rb, ResultHandler rh);
}

doQuery 的差异就是四种执行器的全部差异:

  • SimpleExecutor:newStatementHandler → prepare → parameterize → query,最后 closeStatement。
  • ReuseExecutor:先从 statementMap 按 SQL 找 Statement,找不到才新建。
  • BatchExecutor:把 Statement 放进 statementList,攒够了一起 executeBatch。

5. 为什么查询会打断批处理

BatchExecutor.doQuery 的第一行就是:

public <E> List<E> doQuery(...) throws SQLException {
    flushStatements();     // 查询前必须先刷批,否则读不到未提交的批次
    ...
}

所以「边批量插入边查询」会让批次不断被切碎,性能反而下降。正确做法是把批处理与查询分成两个阶段。

6. 常见追问

追问一:CachingExecutor 和 <cache/> 是什么关系? <cache/> 只是声明「这个 namespace 要二级缓存」,真正的缓存读写由 CachingExecutor 完成。它在 query 里查 TransactionalCacheManager,提交时才把结果刷进 Cache。

追问二:ReuseExecutor 能跨会话复用 Statement 吗? 不能。Statement 绑定在 Connection 上,SqlSession 关闭时连接归还连接池,Statement 也随之关闭。复用只在同一个会话内有效。

追问三:BatchExecutor 能拿到自增 ID 吗? flush 之后能。executeBatch 后驱动通过 getGeneratedKeys 返回主键,MyBatis 在 Jdbc3KeyGenerator 里回填。flush 前读 id 是 null。

追问四:ExecutorType.BATCH 下 insert 返回什么? 返回 BATCH_UPDATE_RETURN_VALUE(一个负数常量)或 0,不是真实影响行数——因为语句还没执行。要看行数只能等 flushStatements() 的结果。

追问五:如何自定义执行器? 实现 Executor 接口或用 Configuration.newExecutor 的 ExecutorType 分支扩展,再通过插件包装。实际项目很少自定义,更多是用插件拦截 Executor.query/update。

小结

记法很简单:Simple 是「一次一关」,Reuse 是「同 SQL 复用」,Batch 是「攒够再发」,Caching 是「套在它们外面的二级缓存壳」。日常默认 Simple 足够,批量写入时显式开 Batch,并记住查询会强制 flush。

笔记加载中…