MyBatis 架构全解:SqlSession、Executor 与 StatementHandler

概述

前文《MyBatis 与 Spring 整合原理》详细剖析了 MapperScannerRegistrarMapperFactoryBeanSqlSessionTemplate 如何利用 Spring 扩展点将 MyBatis 无缝接入 Spring 容器。然而,当 Mapper 代理最终调用 SqlSession.selectOne() 时,MyBatis 内部到底发生了什么?SqlSession 如何将请求委托给 ExecutorStatementHandler 如何封装 JDBC 的 PreparedStatementInterceptorChain 如何在执行链路中织入插件逻辑?本文将正面拆解 MyBatis 的内部执行链路,揭示其架构设计的精妙之处。

MyBatis 作为 Java 生态中最受欢迎的持久化框架之一,其成功的核心在于精巧的架构设计。它不像 Hibernate 那样试图隐藏 SQL,而是将 SQL 的控制权交给开发者,同时通过 SqlSessionExecutorStatementHandlerResultSetHandler 等核心组件的精密协作,将 JDBC 的繁琐操作封装得严丝合缝。一条简单的 selectOne 调用,背后经历了门面委托、执行器选择、一级缓存检查、数据库查询、Statement 创建与参数设置、插件拦截、结果集映射等十余个步骤。本文将沿这条完整的执行链路,逐层拆解每个核心组件的源码,并结合模板方法、策略、装饰器、责任链等设计模式,揭示 MyBatis 架构设计的工程智慧。

  • 核心要点
  • 三层架构:接口层(SqlSession)、核心处理层(Executor → StatementHandler → ParameterHandler/ResultSetHandler)、基础支持层。
  • Executor 体系BaseExecutor 的模板方法骨架,「Simple/Reuse/Batch」的策略选择,「CachingExecutor」的装饰器增强。
  • StatementHandler 封装RoutingStatementHandler 的策略路由,三种 StatementHandler 对 JDBC 不同 Statement 的封装。
  • 参数映射与结果映射TypeHandler 体系如何桥接 Java 类型与 JDBC 类型,ResultMap 如何定义字段映射规则。
  • 插件拦截链InterceptorChain 如何利用 JDK 动态代理和责任链模式,对四大对象进行拦截增强。
  • 设计模式:门面、模板方法、策略、装饰器、责任链、工厂、建造者模式的集中体现。

文章组织架构图

flowchart TD subgraph A ["文章组织架构"] direction TB n1["1. MyBatis 总体架构分层与四大对象"] n2["2. SqlSession:门面模式下的统一入口"] n3["3. Transaction 接口与 Spring 事务的协作"] n4["4. Executor 体系:模板方法、策略与装饰器的三重奏"] n5["5. StatementHandler:JDBC Statement 的封装与路由"] n6["6. ParameterHandler 与参数映射"] n7["7. ResultSetHandler 与结果集映射"] n8["8. 插件拦截链:责任链模式在 MyBatis 中的应用"] n9["9. 一个 SQL 请求的完整执行时序"] n10["10. 设计模式总结与 Spring 核心容器对比"] n11["11. 生产事故排查专题"] n12["12. 面试高频专题"] n1 --> n2 --> n3 --> n4 --> n5 --> n6 --> n7 --> n8 --> n9 --> n10 --> n11 --> n12 end classDef topic fill:#f8f9fa,stroke:#333,stroke-width:2px,rx:5,color:#333; class n1,n2,n3,n4,n5,n6,n7,n8,n9,n10,n11,n12 topic;
  • 总览说明:全文 12 个模块从 MyBatis 总体架构出发,逐步深入 SqlSession、Transaction、Executor、StatementHandler、参数/结果映射、插件拦截,再到全链路时序、设计模式总结及与 Spring 的对比、事故与面试,形成完整闭环。
  • 逐模块说明:模块 1 建立三层架构与四大对象的整体认知;模块 2-3 讲解门面入口和事务协作;模块 4-7 逐层深入各核心处理组件;模块 8 揭示插件拦截原理;模块 9 串联全链路;模块 10 提炼设计思想并与 Spring 对比;模块 11-12 落地实践与应试。
  • 关键结论MyBatis 的架构设计是"模板方法 + 策略 + 装饰器 + 责任链"的教科书级应用,理解 BaseExecutor.query 的骨架和 RoutingStatementHandler 的路由逻辑,是掌握 MyBatis 源码的钥匙。

1. MyBatis 总体架构分层与四大对象

MyBatis 的整体架构可以分为三层,每一层都职责分明,共同支撑起这个半自动化的 ORM 框架。

  • 接口层 :这是开发者直接交互的部分。它对外提供统一的 API,如 SqlSession 接口。该层负责接收调用请求,并将它们分发给核心处理层。SqlSession 是所有数据库操作的统一门面。
  • 核心处理层 :这是 MyBatis 的大脑和心脏,负责完成所有核心的数据库操作流程。它包含四大核心对象:
    1. Executor(执行器):负责调度和执行 SQL。它管理一级缓存和事务,是整个执行流程的总指挥。
    2. StatementHandler(语句处理器) :负责封装 JDBC Statement 操作,如创建 StatementPreparedStatement,并调用 ParameterHandler 设置参数。
    3. ParameterHandler(参数处理器) :负责将用户传入的 Java 参数映射到 PreparedStatement 的占位符上。
    4. ResultSetHandler(结果集处理器) :负责将 JDBC 返回的 ResultSet 映射成 Java 对象集合。
  • 基础支持层 :提供最底层的通用能力,包括配置解析(XNodeXMLConfigBuilder)、类型转换(TypeHandler 体系)、缓存(Cache 接口及其实现)、日志(适配多种日志框架)、反射(ReflectorMetaObject)和资源加载等。这一层为上层提供坚实的基础支撑。

一条简单的 SQL 请求在 MyBatis 内部会经历如下流转路径: SqlSession 接收请求 → 委托给 ExecutorExecutor 检查缓存、获取连接 → 创建 StatementHandlerStatementHandler 创建 JDBC Statement → 委托 ParameterHandler 设置参数 → 执行 SQL → 委托 ResultSetHandler 处理结果集 → 返回最终结果。

下面这张类关系图,清晰地展示了各核心接口与类之间的结构关系。

classDiagram class SqlSessionFactory { <> +openSession() SqlSession } class SqlSession { <> +selectOne(statement, parameter) Object +selectList(statement, parameter) List +insert(statement, parameter) int +update(statement, parameter) int +delete(statement, parameter) int +commit() +rollback() } class DefaultSqlSession { -Configuration configuration -Executor executor -boolean autoCommit +selectOne() Object +selectList() List } class Executor { <> +query(ms, parameter, rowBounds, resultHandler) List +update(ms, parameter) int +commit(required) void +rollback(required) void +flushStatements() List~BatchResult~ } class BaseExecutor { <> #PerpetualCache localCache #int queryStack +query() List #queryFromDatabase() List #doQuery() List #doUpdate() int } class CachingExecutor { -Executor delegate -TransactionalCacheManager tcm +query() List +update() int } class SimpleExecutor class ReuseExecutor class BatchExecutor class StatementHandler { <> +prepare(connection) Statement +parameterize(statement) void +query(statement, resultHandler) List +update(statement) int } class RoutingStatementHandler { -StatementHandler delegate } class PreparedStatementHandler class CallableStatementHandler class SimpleStatementHandler class ParameterHandler { <> +setParameters(ps) void } class DefaultParameterHandler class ResultSetHandler { <> +handleResultSets(ps) List } class DefaultResultSetHandler SqlSessionFactory ..> SqlSession : creates SqlSession <|.. DefaultSqlSession DefaultSqlSession --> Executor : uses Executor <|.. BaseExecutor Executor <|.. CachingExecutor CachingExecutor o-- Executor : decorates BaseExecutor <|-- SimpleExecutor BaseExecutor <|-- ReuseExecutor BaseExecutor <|-- BatchExecutor DefaultSqlSession --> StatementHandler : creates StatementHandler <|.. RoutingStatementHandler RoutingStatementHandler o-- StatementHandler : routes to StatementHandler <|-- PreparedStatementHandler StatementHandler <|-- CallableStatementHandler StatementHandler <|-- SimpleStatementHandler StatementHandler --> ParameterHandler : uses ParameterHandler <|.. DefaultParameterHandler StatementHandler --> ResultSetHandler : uses ResultSetHandler <|.. DefaultResultSetHandler
  • 图表主旨概括 :本类图展示了 MyBatis 核心接口与类之间的静态结构关系。从 SqlSessionExecutor 再到 StatementHandlerParameterHandlerResultSetHandler,清晰地揭示了"四大对象"的依赖层次和具体实现。
  • 逐层/逐元素分解
    • 接口层 :以 SqlSessionDefaultSqlSession 为核心,它持有 Executor 引用,作为客户端调用的入口。
    • 核心处理层Executor 体系最为复杂,BaseExecutor 通过模板方法模式定义了执行骨架,SimpleExecutorReuseExecutorBatchExecutor 是具体策略实现,而 CachingExecutor 则作为装饰器,为所有执行器增加了二级缓存能力。RoutingStatementHandler 充当策略路由器,根据配置创建不同的 StatementHandler 实现。
    • 基础支持层(隐式)Configuration 对象贯穿始终,为所有组件提供运行时配置。
  • 设计原理映射
    • 门面模式SqlSession 接口和 DefaultSqlSession 实现。
    • 模板方法模式BaseExecutorquery()update() 方法。
    • 策略模式BaseExecutor 的不同子类和 RoutingStatementHandler
    • 装饰器模式CachingExecutorExecutor 接口的实现。
  • 工程联系与关键结论理解 DefaultSqlSession 只是一个"门面",它将所有工作委托给 Executor,而 Executor 又依赖于 StatementHandler 等组件,是深入 MyBatis 源码的第一步。这种分层解耦的设计,使得每一层都可以独立演进和测试。

2. SqlSession:门面模式下的统一入口

SqlSession 是 MyBatis 为开发者提供的顶层接口。无论是执行 SQL、获取 Mapper 代理还是管理事务,所有操作都必须通过它来完成。这完美体现了门面模式的思想:为一个子系统中的一组接口提供一个统一的高层接口,从而简化子系统的使用。

2.1 门面之下的复杂性

开发者只需调用 sqlSession.selectOne("..."),但这一行代码背后,隐藏着复杂的操作序列:

  1. Configuration 中查找 MappedStatement
  2. 决定使用哪种 Executor
  3. 检查一级/二级缓存。
  4. 从连接池获取数据库连接。
  5. 创建 StatementHandler 并创建 JDBC Statement
  6. 应用插件拦截链对 ExecutorStatementHandler 等方法的拦截。
  7. 处理参数映射。
  8. 执行 JDBC 操作。
  9. 处理结果集映射。
  10. 关闭资源和清理缓存。

DefaultSqlSession 作为默认实现,巧妙地将这些复杂性委托给了 ExecutorConfiguration

java 复制代码
// 代码位置:org.apache.ibatis.session.defaults.DefaultSqlSession
public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor; // 门面背后的真正执行者

  // ... 构造函数 ...

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 1. 从Configuration中获取MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 2. 将所有查询操作委托给Executor
      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();
    }
  }

  @Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true; // 标记为脏,用于事务提交判断
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 将所有更新操作委托给Executor
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  // ... 其他方法是类似的委托模式 ...
}

设计解读

  • DefaultSqlSession 中的 selectListselectOneupdateinsertdelete 等方法,其内部逻辑惊人地一致:获取 MappedStatement,然后调用 executor 的对应方法。
  • 它本身不包含任何数据库访问逻辑,纯粹是一个门面委托器 。它将 MyBatis 核心处理层(ExecutorStatementHandler 等)这个复杂的子系统,简化为一个易于使用的接口。
  • 这种设计使得核心处理层的任何内部变化(如更换 Executor 实现),都不会影响到调用方。

以下序列图展示了 DefaultSqlSession 作为门面的委托过程。

sequenceDiagram participant App as 应用程序 participant SqlSession as SqlSession(DefaultSqlSession) participant Executor as Executor(SimpleExecutor) participant StmtHandler as StatementHandler participant JDBC as JDBC App->>SqlSession: 1. selectOne(statement, parameter) SqlSession->>SqlSession: 2. 从Configuration获取MappedStatement SqlSession->>Executor: 3. query(ms, parameter, ...) activate Executor Executor->>Executor: 4. 检查一级缓存 Executor->>StmtHandler: 5. 创建StatementHandler并执行 activate StmtHandler StmtHandler->>JDBC: 6. 与JDBC交互 deactivate StmtHandler Executor-->>SqlSession: 7. 返回结果 deactivate Executor SqlSession-->>App: 8. 返回处理后的结果
  • 图表主旨概括 :本序列图旨在展现 SqlSession 的门面模式特性。它清晰地演示了一个来自应用层的 selectOne 调用,是如何被 DefaultSqlSession 完整地委托给 Executor,而不在自身中进行任何业务处理的。
  • 逐层/逐元素分解
    • 应用层 :发起对 SqlSession 接口的调用。
    • 门面层(DefaultSqlSession) :方法实现内部,完成了从 Configuration 获取元数据(MappedStatement)的工作,然后立即将请求和元数据都传递给 Executor
    • 核心处理层(Executor):接过请求,成为后续所有操作的发起者和协调者。
  • 设计原理映射门面模式DefaultSqlSession 封装了内部的 ExecutorConfiguration 等组件的交互细节,为外部提供了一个简单、统一的接口。
  • 工程联系与关键结论SqlSession 是每个数据库会话的边界,而 Executor 是每个操作的生命周期控制器。理解它们的委托关系,是定位性能瓶颈和异常根源的第一步。

2.2 工厂与建造者的协作

要获取一个 SqlSession 实例,必须先创建一个 SqlSessionFactory。而 SqlSessionFactory 的创建又依赖于 SqlSessionFactoryBuilder。这里体现了工厂模式建造者模式的协作。

  • 建造者模式(SqlSessionFactoryBuilder :用于构建复杂的 SqlSessionFactory 对象。它提供了多个重载的 build 方法,可以接收 ReaderInputStream 等不同来源的 MyBatis 配置文件,内部会调用 XMLConfigBuilder 解析 XML,最终生成一个 Configuration 对象,并用它来创建 SqlSessionFactorySqlSessionFactoryBuilder 的生命周期很短暂,一旦工厂构建完成,它就应该被销毁。
  • 工厂模式(SqlSessionFactory :一旦 SqlSessionFactory 被创建,它就应该在整个应用生命周期中存在。它提供了 openSession 方法,用于创建新的 SqlSession 实例。这个工厂方法封装了创建 SqlSession 的细节,包括从 Configuration 中获取 Environment(数据源和事务工厂配置),并通过 TransactionFactory 创建 Transaction,最终组装出一个可用的 DefaultSqlSession
java 复制代码
// 使用建造者模式创建工厂
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 1. 建造者生明周期开始
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 建造者生命周期结束

// 2. 使用工厂模式创建门面
try (SqlSession session = sqlSessionFactory.openSession()) {
    // ...
}

3. Transaction 接口与 Spring 事务的协作

Executor 在执行 SQL 时,并不直接管理连接,而是通过 Transaction 接口来获取连接、提交或回滚事务。

3.1 MyBatis 原生的 Transaction 设计

java 复制代码
// 代码位置:org.apache.ibatis.transaction.Transaction
public interface Transaction {
  Connection getConnection() throws SQLException;
  void commit() throws SQLException;
  void rollback() throws SQLException;
  void close() throws SQLException;
  Integer getTimeout() throws SQLException;
}

Transaction 接口定义了事务操作的基本契约。它主要有两个实现:

  • JdbcTransaction :直接基于 JDBC 的 Connection 对象管理事务。commitrollback 方法直接调用 java.sql.Connection 的对应方法。它适用于独立运行、不依赖外部事务管理器的 MyBatis 应用程序。
  • ManagedTransaction :它的 commitrollback 方法是空的!它不主动管理事务,而是将事务管理权交给外部容器(如 Web 容器或 Spring 容器)。getConnection 方法每次都会从数据源获取一个新的连接,这在非 Web 环境下需要谨慎处理连接关闭。

3.2 SpringManagedTransaction:与 Spring 事务的完美协作

在与 Spring 整合的环境下,MyBatis 提供了一个关键的实现:SpringManagedTransaction。这个类是 MyBatis 事务管理从"自力更生"到"融入生态"的桥梁。相关原理在《MyBatis 与 Spring 整合原理》和《Spring 事务管理抽象》中已有深入阐述,这里我们聚焦其核心机制。

java 复制代码
// 代码位置:org.mybatis.spring.transaction.SpringManagedTransaction
public class SpringManagedTransaction implements Transaction {

  private final DataSource dataSource;
  private Connection connection;
  private boolean isConnectionTransactional;
  private boolean autoCommit;

  @Override
  public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  private void openConnection() throws SQLException {
    // 1. 核心:通过Spring的DataSourceUtils获取连接,而非直接从数据源获取
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    // 2. 判断这个连接是否是Spring事务管理器管理的事务性连接
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
    // ... 日志 ...
  }

  @Override
  public void commit() throws SQLException {
    // 3. 如果连接是Spring事务管理的,则不执行任何操作,将事务管理权完全交给Spring
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      // ... 日志 ...
      this.connection.commit();
    }
  }

  @Override
  public void rollback() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      // ... 日志 ...
      this.connection.rollback();
    }
  }

  @Override
  public void close() throws SQLException {
    // 4. 将连接"归还"给Spring,而不是物理关闭。Spring会根据事务状态决定是否真正关闭
    DataSourceUtils.releaseConnection(this.connection, this.dataSource);
  }
}

设计解读

  • 连接获取 :通过 DataSourceUtils.getConnection(),MyBatis 获取到的是与当前 Spring 事务绑定的连接DataSourceUtils 的背后是 TransactionSynchronizationManager,它将连接存储在当前线程的 ThreadLocal 资源中。这确保了在整个 Spring 事务范围内,所有 MyBatis 操作使用的都是同一个数据库连接。
  • 事务提交与回滚commit()rollback() 方法会先检查 isConnectionTransactional 标志。如果为 true,说明当前连接是 Spring 事务的一部分,MyBatis 不会调用 connection.commit()connection.rollback(),而是将控制权完全交给 Spring 的 AbstractPlatformTransactionManager。这与 ManagedTransaction 的设计思想一脉相承 ,但 SpringManagedTransaction 的"容器"特指 Spring。
  • 连接关闭close() 方法通过 DataSourceUtils.releaseConnection() 将连接的"使用权"释放。如果当前连接是事务性的,它不会被物理关闭,而是被解除与线程的绑定;如果当前连接是非事务性的,它会被直接关闭或归还到连接池。这避免了连接的泄漏。

结论SpringManagedTransaction 完美诠释了"好莱坞原则"(Don't call us, we'll call you)。它放弃了对事务和连接生命周期的最终控制权,转而成为 Spring 事务生态中的一个忠实参与者。在 Spring 整合环境下,MyBatis 的事务行为完全由 Spring 的事务管理器决定。

4. Executor 体系:模板方法、策略与装饰器的三重奏

Executor 是 MyBatis 核心处理层中的核心。它不仅负责执行 SQL 语句,还管理着一级缓存和事务。MyBatis 的 Executor 体系是设计模式应用的集大成者,它融合了模板方法模式、策略模式和装饰器模式。

4.1 Executor 接口与模板方法骨架

java 复制代码
// 代码位置:org.apache.ibatis.executor.Executor (接口)
public interface Executor {
  ResultHandler NO_RESULT_HANDLER = null;
  int update(MappedStatement ms, Object parameter) throws SQLException;
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
  void commit(boolean required) throws SQLException;
  void rollback(boolean required) throws SQLException;
  void close(boolean forceRollback);
  Transaction getTransaction();
  // ...
}

BaseExecutor 作为 Executor 的抽象实现,定义了执行流程的骨架。这便是模板方法模式的体现。

java 复制代码
// 代码位置:org.apache.ibatis.executor.BaseExecutor
public abstract class BaseExecutor implements Executor {

  // 一级缓存,基于PerpetualCache的HashMap实现
  protected PerpetualCache localCache;
  // ... 其他字段 ...

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 1. 创建缓存键
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    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 {
    // ... 错误上下文 ...
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache(); // 2. 需要时清空缓存
    }
    List<E> list;
    try {
      queryStack++; // 3. 防止递归查询时反复清缓存
      // 4. 尝试从一级缓存获取
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 5. 一级缓存命中!
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 6. 一级缓存未命中,查询数据库。这是一个抽象方法,由子类实现
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    // ... 延迟加载等 ...
    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);
    try {
      // 调用子类实现的 doQuery 或 doUpdate
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // 查询完成后,从缓存中移除占位符
      localCache.removeObject(key);
    }
    // 将查询结果放入一级缓存
    localCache.putObject(key, list);
    return list;
  }

  // 以下两个方法是留给子类实现的"钩子方法"
  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
  protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
}

设计解读

  • BaseExecutor.query() 就是模板方法。它定义了查询算法的主干:创建 CacheKey、清缓存、查一级缓存、查库、存缓存。
  • queryFromDatabase()doQuery() 的分离非常巧妙。queryFromDatabase 负责缓存管理逻辑,而 doQuery 负责实际的数据库查询。doQuerydoUpdate 等抽象方法就是钩子方法 ,将具体执行逻辑的实现延迟到 SimpleExecutorReuseExecutorBatchExecutor 等子类中。
  • queryStack 是一个防止递归查询清缓存的精巧设计。当 MyBatis 进行关联查询(如嵌套结果映射)时,可能会递归调用 query 方法。queryStack > 0 时不会清空缓存,保证了缓存行为在递归场景下的正确性。

下面这张序列图生动展示了 BaseExecutor 的模板方法骨架和一级缓存的工作流程。

sequenceDiagram participant Client as 调用者(DefaultSqlSession) participant BaseEx as BaseExecutor participant SubEx as SimpleExecutor(子类) participant DB as 数据库 Client->>BaseEx: query(ms, param, ...) activate BaseEx BaseEx->>BaseEx: 1. 创建CacheKey BaseEx->>BaseEx: 2. 检查是否需要清缓存(flushCache) BaseEx->>BaseEx: 3. queryStack++ BaseEx->>BaseEx: 4. localCache.getObject(key) alt 一级缓存命中 BaseEx-->>Client: 5. 直接返回缓存中的list else 一级缓存未命中 BaseEx->>BaseEx: 6. queryFromDatabase(key) note right of BaseEx: 模板方法:调用抽象方法doQuery BaseEx->>SubEx: 7. doQuery(...) activate SubEx SubEx->>DB: 8. 执行真正的JDBC查询 DB-->>SubEx: 9. 返回数据 deactivate SubEx BaseEx->>BaseEx: 10. localCache.putObject(key, list) BaseEx-->>Client: 11. 返回从DB查询的list end BaseEx->>BaseEx: 12. queryStack-- deactivate BaseEx
  • 图表主旨概括 :本序列图展示了 BaseExecutor.query() 方法的完整执行流程,清晰地描绘了模板方法模式和一级缓存的交互逻辑。
  • 逐层/逐元素分解
    • BaseExecutor(模板类):负责流程控制,定义了查询的主流程:缓存检查、缓存写和清空逻辑。
    • SimpleExecutor 等子类(具体类) :负责实现doQuery钩子方法,封装了与 JDBC 交互的细节。
  • 设计原理映射模板方法模式BaseExecutor.query() 是模板,doQuery()/doUpdate() 是钩子。
  • 工程联系与关键结论一级缓存是 BaseExecutor 级别的,意味着同一个 SqlSession 内共享。这是 MyBatis 隐形数据行为最常见的来源,理解其工作机制对于避免"脏读"至关重要。

4.2 Executor 的三兄弟:策略模式的体现

BaseExecutor 的三个子类代表了三种不同的 Statement 管理策略,这是策略模式的典型应用。

  1. SimpleExecutor(简单执行器) 这是 MyBatis 的默认执行器。它的策略是:每次执行 updatequery,都会创建一个新的 Statement 对象,用完立即关闭。

    java 复制代码
    // 代码位置:org.apache.ibatis.executor.SimpleExecutor
    public class SimpleExecutor extends BaseExecutor {
      @Override
      public <E> List<E> doQuery(MappedStatement ms, Object parameter, ...) throws SQLException {
        Statement stmt = null;
        try {
          Configuration configuration = ms.getConfiguration();
          // 1. 创建StatementHandler,这是策略模式(RoutingStatementHandler)
          StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
          // 2. 创建Statement,设置参数
          stmt = prepareStatement(handler, ms.getStatementLog());
          // 3. 执行查询,处理结果集
          return handler.query(stmt, resultHandler);
        } finally {
          // 4. 重要:关闭Statement
          closeStatement(stmt);
        }
      }
      // ...
    }

    优点 :简单、线程安全。 缺点 :频繁创建和关闭 Statement,对数据库造成压力,性能较低。

  2. ReuseExecutor(可重用执行器) 它的策略是:缓存已经创建好的 Statement,以 SQL 字符串为 Key。执行时,如果缓存中存在,就复用。

    java 复制代码
    // 代码位置:org.apache.ibatis.executor.ReuseExecutor
    public class ReuseExecutor extends BaseExecutor {
      // Statement缓存池,Key是SQL,Value是Statement对象
      private final Map<String, Statement> statementMap = new HashMap<>();
    
      @Override
      public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        // ...
        Statement stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.update(stmt);
      }
    
      private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();
        if (hasStatementFor(sql)) {
          // 1. 复用策略:如果缓存中有此SQL的Statement,直接取用
          stmt = getStatement(sql);
          applyTransactionTimeout(stmt);
        } else {
          // 2. 否则,创建新的Statement,并放入缓存
          Connection connection = getConnection(statementLog);
          stmt = handler.prepare(connection, transaction.getTimeout());
          putStatement(sql, stmt);
        }
        // 3. 重要:每次复用都需要重新设置参数
        handler.parameterize(stmt);
        return stmt;
      }
      // ...
    }

    优点 :减少了 Statement 的创建次数,对相同 SQL 的多次调用性能更好。 缺点Statement 对象本身会占用数据库游标等资源。必须确保在事务提交或会话关闭时,清理缓存的 Statement

  3. BatchExecutor(批处理执行器) 它的策略是:只处理 update 操作。它将多个 Statement 添加到批处理队列中,通过 JDBC 的 addBatch()executeBatch() 来批量提交,从而大幅提升批量插入或更新的性能。

    java 复制代码
    // 代码位置:org.apache.ibatis.executor.BatchExecutor
    public class BatchExecutor extends BaseExecutor {
      // 存储Statement及其对应的BatchResult
      private final List<Statement> statementList = new ArrayList<>();
      private final List<BatchResult> batchResultList = new ArrayList<>();
    
      @Override
      public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        // ...
        final BoundSql boundSql = ms.getBoundSql(parameter);
        final String sql = boundSql.getSql();
        Statement stmt;
        // 找到当前SQL对应的BatchResult
        BatchResult batchResult = batchResultList.stream().filter(br -> br.getSql().equals(sql)).findFirst().orElseGet(() -> {
          // ... 创建新的Statement和BatchResult并缓存 ...
          BatchResult newBatchResult = new BatchResult(sql, new ArrayList<>());
          batchResultList.add(newBatchResult);
          return newBatchResult;
        });
        // 1. 核心:通过statement.addBatch()将当前操作加入批处理
        stmt = batchResult.getStatement();
        handler.parameterize(stmt);
        handler.batch(stmt); // 内部调用 stmt.addBatch()
        batchResult.addParameterObject(parameter);
        return BATCH_UPDATE_RETURN_VALUE; // 返回一个不可知的值
      }
      // ...
    }

    关键点 :调用 BatchExecutordoUpdate 后,SQL 并未立即执行,而是加入了队列。只有当调用 flushStatements() 方法或事务提交时,才会真正执行 stmt.executeBatch()忘记调用 flushStatements 是导致数据未持久化的常见原因。

4.3 CachingExecutor:为执行器穿上二级缓存的"马甲"

CachingExecutor 实现了 Executor 接口,但它不亲自执行 SQL,而是将请求委托给内部的 delegate 执行器。这是标准的装饰器模式应用,它在不改变原有执行器功能的前提下,动态地为执行器增加了二级缓存的能力。

java 复制代码
// 代码位置:org.apache.ibatis.executor.CachingExecutor
public class CachingExecutor implements Executor {

  private final Executor delegate; // 被装饰的真实执行器
  private final TransactionalCacheManager tcm = new TransactionalCacheManager(); // 事务缓存管理器

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 1. 从MappedStatement中获取二级缓存(映射文件级别的缓存)
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        // ... 处理存储过程输出参数 ...
        // 2. 装饰器增强:先从二级缓存中查
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 3. 缓存未命中,交给被装饰的真实执行器去查询
          list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
          // 4. 将结果存放到二级缓存(通过TransactionalCacheManager,暂存)
          tcm.putObject(cache, key, list);
        }
        return list;
      }
    }
    // 5. 如果没有二级缓存,则直接穿透到被装饰的执行器
    return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    flushCacheIfRequired(ms); // 更新操作会清空缓存
    // 直接交给被装饰的执行器执行
    return delegate.update(ms, parameter);
  }

  // commit时,将暂存在TransactionalCacheManager中的结果真正刷入二级缓存
  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit(); // 装饰器特有的后置处理
  }
  // ...
}

设计解读

  • CachingExecutor 和被它装饰的 delegate(例如 SimpleExecutor)都实现了 Executor 接口,这使得它们可以无缝替换。
  • query 方法在调用 delegate.query 前后,增加了"查询二级缓存"和"写入二级缓存"的功能。这是装饰器模式的精髓:功能增强
  • TransactionalCacheManager 是一个关键组件,它保证了在事务提交之前,数据暂时存放,只有事务成功提交,暂存的数据才会真正被刷新到二级缓存中。如果事务回滚,暂存的数据将被丢弃,避免了缓存脏数据。

下面的序列图清晰地展示了 CachingExecutor 的装饰过程。

sequenceDiagram participant Caller as DefaultSqlSession participant CacheEx as CachingExecutor(装饰器) participant RealEx as SimpleExecutor(被装饰者) participant DB as 数据库 participant TCM as TransactionalCacheManager Caller->>CacheEx: query(ms, param, ...) activate CacheEx CacheEx->>TCM: 1. getObject(cache, key) alt 二级缓存命中 TCM-->>CacheEx: 返回结果 CacheEx-->>Caller: 返回二级缓存结果 else 二级缓存未命中 CacheEx->>RealEx: 2. delegate.query(...) activate RealEx RealEx->>DB: 3. 查询数据库 DB-->>RealEx: 4. 返回数据 deactivate RealEx CacheEx->>TCM: 5. putObject(cache, key, list) note right of TCM: 暂存数据,等待事务提交 CacheEx-->>Caller: 6. 返回数据库结果 end deactivate CacheEx Caller->>CacheEx: commit() CacheEx->>RealEx: 7. delegate.commit() CacheEx->>TCM: 8. tcm.commit() note over TCM: 将暂存数据真正刷入二级缓存
  • 图表主旨概括 :本序列图揭示了 CachingExecutor 作为装饰器,如何在真实的 Executor 之前插入二级缓存查询,并在其后插入缓存写入逻辑。
  • 逐层/逐元素分解
    • CachingExecutor(装饰器) :持有 Executor 引用,其 query 方法在调用被装饰对象前后添加了缓存查询和暂存的逻辑。
    • SimpleExecutor(被装饰者):只负责纯粹的数据库查询逻辑,完全感知不到二级缓存的存在。
    • TransactionalCacheManager:作为缓存装饰上下文,管理事务性缓存,保证缓存的最终一致性。
  • 设计原理映射装饰器模式 。不改变原有 SimpleExecutor 的代码,通过组合方式动态地增加了二级缓存功能。
  • 工程联系与关键结论二级缓存是跨 SqlSession 的,是实现进程内缓存共享的关键。然而,其设计(尤其是与 TransactionalCacheManager 的集成)也要求开发者在设计查询语句和事务边界时必须格外小心,错误的配置极易导致数据不一致。

5. StatementHandler:JDBC Statement 的封装与路由

Executor 准备好所有执行要素后,它会把具体与 JDBC Statement 交互的细节委托给 StatementHandlerStatementHandler 屏蔽了 StatementPreparedStatementCallableStatement 三种 JDBC 接口的差异。

5.1 设计:封装、路由与模板

StatementHandler 接口定义了 prepareparameterizequeryupdate 等方法。其实现体系同样充满了设计智慧:

  • 策略模式(路由)RoutingStatementHandler 扮演路由角色。它根据 MappedStatement 中配置的 statementType,创建并委托给具体的 StatementHandler 实现。
  • 模板方法模式BaseStatementHandler 是抽象基类,它提取了所有 StatementHandler 的共性,如获取 ConfigurationBoundSql 等,并定义了后续操作的骨架。
  • 适配器模式(隐式) :三个具体实现类------SimpleStatementHandler(处理普通 SQL)、PreparedStatementHandler(处理预编译 SQL)、CallableStatementHandler(处理存储过程)------分别封装了不同类型 JDBC Statement 的差异,为上层 Executor 提供了统一的接口。
java 复制代码
// 代码位置:org.apache.ibatis.executor.statement.RoutingStatementHandler
public class RoutingStatementHandler implements StatementHandler {

  private final StatementHandler delegate; // 真实的StatementHandler

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 策略模式:根据StatementType创建不同的StatementHandler实现
    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);
  }

  @Override
  public void parameterize(Statement statement) throws SQLException {
    // 委托给具体的实现,例如PreparedStatementHandler
    delegate.parameterize(statement);
  }
  // ...
}

设计解读RoutingStatementHandler 是典型的策略模式 中的"上下文"角色。它将选择具体策略(哪种 StatementHandler)的逻辑集中在构造函数中。客户端(SimpleExecutor 等)只需与 RoutingStatementHandler 交互,完全不用关心底层的 Statement 类型是 StatementPreparedStatement 还是 CallableStatement

以下序列图演示了这个路由过程。

sequenceDiagram participant Executor as BaseExecutor子类 participant Routing as RoutingStatementHandler participant Prepared as PreparedStatementHandler participant JDBC as java.sql.PreparedStatement Executor->>Routing: new RoutingStatementHandler(ms, ...) activate Routing Routing->>Routing: switch (ms.getStatementType()) note right of Routing: 策略:PREPARED Routing->>Prepared: new PreparedStatementHandler(...) Routing->>Routing: this.delegate = PreparedStatementHandler deactivate Routing Executor->>Routing: 2. prepare(connection, ...) Routing->>Prepared: delegate.prepare(connection, ...) Prepared->>JDBC: connection.prepareStatement(sql) JDBC-->>Prepared: PreparedStatement实例 Prepared-->>Routing: PreparedStatement实例 Routing-->>Executor: PreparedStatement实例 Executor->>Routing: 3. parameterize(statement) Routing->>Prepared: delegate.parameterize(statement) Prepared->>Prepared: 委托ParameterHandler设置参数
  • 图表主旨概括 :本序列图演示了 RoutingStatementHandler 作为策略路由,在构造函数中根据 StatementType 创建具体 StatementHandler 的过程,以及后续所有方法调用都被透明地委托给这个具体实现的过程。
  • 逐层/逐元素分解
    • 路由层RoutingStatementHandler 根据 ms.getStatementType() 决定使用哪个具体的 StatementHandler
    • 实现层PreparedStatementHandler 封装了与 PreparedStatement 交互的所有细节。
  • 设计原理映射策略模式RoutingStatementHandler 是 Context,Simple/Prepared/CallableStatementHandler 是 ConcreteStrategy。委托模式也被广泛使用。
  • 工程联系与关键结论绝大多数情况下,我们使用 PREPARED 类型。RoutingStatementHandler 的存在使得 MyBatis 在框架层面能够透明地支持存储过程和普通 SQL,而不会让上层感知到复杂性。

6. ParameterHandler 与参数映射

StatementHandler 创建好 PreparedStatement 之后,就轮到 ParameterHandler 出场了。它的职责是唯一的:将 Java 方法参数设置到 PreparedStatement? 占位符中。

6.1 核心实现:DefaultParameterHandler

java 复制代码
// 代码位置:org.apache.ibatis.scripting.defaults.DefaultParameterHandler
public class DefaultParameterHandler implements ParameterHandler {

  private final TypeHandlerRegistry typeHandlerRegistry;
  private final MappedStatement mappedStatement;
  private final Object parameterObject;
  private final BoundSql boundSql;
  private final Configuration configuration;

  @Override
  public void setParameters(PreparedStatement ps) {
    // 1. 获取参数映射列表。ParameterMapping对象描述了?占位符的位置、Java类型、JdbcType、TypeHandler等
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        // 2. OUT模式的参数不处理
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          // 3. 从参数对象中取出对应属性的值
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) {
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            // 4. 如果参数是单一简单类型,直接取其值
            value = parameterObject;
          } else {
            // 5. 否则,通过反射从复杂对象中获取属性值(使用MetaObject)
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          // 6. 为这个参数选择合适的TypeHandler
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          // ... 设置默认JdbcType ...
          // 7. 核心:使用TypeHandler将值设置到PreparedStatement中
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        }
      }
    }
  }
}

设计解读

  • 解耦参数来源与设置ParameterHandler 不关心参数是从 @Param 注解、JavaBean 属性还是方法参数过来的。它只关心两件事:从 parameterObject 中拿到值,然后用 TypeHandler 设置到 ps 里。
  • @Param 的支持 :这是由 ParamNameResolver 在更早的阶段完成的。ParamNameResolver 会解析 Mapper 接口方法参数上的 @Param 注解,并将多个参数包装成一个 Map。这个 Map 最终成为 parameterObject。因此,DefaultParameterHandler 可以直接通过属性名(也就是 @Param 的值)从 Map 中获取参数值。
  • TypeHandler 作为桥梁TypeHandler 是 MyBatis 类型系统的基石,负责 Java 类型和 JDBC 类型之间的桥梁转换。setParameter 方法知道如何将 Java 对象(例如 Date)正确地设置到 PreparedStatement 的指定位置(例如作为 VARCHARTIMESTAMP)。TypeHandler 体系是 MyBatis 类型转换的核心,其扩展机制将在后续篇章详述。

7. ResultSetHandler 与结果集映射

JDBC 执行完成后,返回一个 ResultSetResultSetHandler 的职责就是将这个 ResultSet 转换为应用程序需要的 Java 对象列表。

7.1 DefaultResultSetHandler 的处理流程

java 复制代码
// 代码位置:org.apache.ibatis.executor.resultset.DefaultResultSetHandler
public class DefaultResultSetHandler implements ResultSetHandler {
  // ...
  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    // ...
    final List<Object> multipleResults = new ArrayList<>();
    int resultSetCount = 0;
    // 1. 获取第一个ResultSet。这通常是查询的主结果
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    // 2. 获取此ResultSet对应的ResultMap列表
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 3. 核心:处理单一ResultSet,根据ResultMap进行映射
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 4. 获取下一个ResultSet(处理存储过程或包含多个ResultSet的查询)
      rsw = getNextResultSet(stmt);
      // ...
      resultSetCount++;
    }
    // ...
    return collapseSingleResultList(multipleResults);
  }

  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      // ...
      if (resultHandler == null) {
        // 5. 创建默认的ResultHandler,用于聚合Object
        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
        // 6. 处理整行映射和嵌套映射
        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
        multipleResults.add(defaultResultHandler.getResultList());
      } else {
        handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
      }
    }
    // ...
  }

  // 处理行值,逻辑中会区分简单映射和嵌套映射
  private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      // 7. 处理嵌套结果映射(包含<association>或<collection>)
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      // 8. 处理简单结果映射
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }
}

设计解读

  • ResultMap 的重要性ResultMap 是 MyBatis 最强大的特性之一,它定义了数据库列名与 Java 对象属性名之间的映射规则,包括类型处理器、嵌套映射、辨别器等。DefaultResultSetHandler 的所有映射逻辑都围绕 ResultMap 展开。
  • 映射分离handleRowValuesForSimpleResultMap 处理简单映射,它遍历 ResultSet 的每一行,为每一行创建一个目标 Java 对象(createResultObject),然后根据 ResultMap 中定义的 ResultMapping 列表,调用 applyPropertyMappingsResultSet 中获取列值并设置到对象属性中。每一步都离不开 TypeHandler
  • 嵌套映射与延迟加载 :当遇到 <association><collection> 标签定义的嵌套映射时,MyBatis 采用截然不同的处理逻辑(handleRowValuesForNestedResultMap)。对于延迟加载,此时并不会立即执行关联查询,而是会创建一个目标对象的代理对象(通过 CGLIB 或 Javassist),并在代理对象中记录下关联查询所需的所有信息。只有当真正访问该属性时,才会触发代理对象去执行额外的 SQL 查询。延迟加载的详细机制将在《映射器原理》篇中深入。

8. 插件拦截链:责任链模式在 MyBatis 中的应用

MyBatis 的插件机制是其扩展性的核心。它允许开发者在 SQL 执行过程中的特定点进行拦截和增强,例如分页、加密、审计等功能。其底层实现巧妙地运用了 JDK 动态代理和责任链模式

8.1 InterceptorChain 的实现原理

插件拦截的核心类是 InterceptorChain

java 复制代码
// 代码位置:org.apache.ibatis.plugin.InterceptorChain
public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  // 1. 为目标对象应用所有插件,层层包装
  public Object pluginAll(Object target) {
    // 遍历所有拦截器
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
}

开发者的 Interceptor 实现的 plugin 方法通常会使用 MyBatis 提供的 Plugin.wrap 静态方法。

java 复制代码
// 代码位置:MyBatis Plugin类(用户实现 Interceptor 时常用的工具方法)
public static Object wrap(Object target, Interceptor interceptor) {
  // 1. 获取interceptor上的@Intercepts和@Signature注解定义的拦截点
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  Class<?> type = target.getClass();
  // 2. 获取目标对象的类型及其所有接口中,与拦截点匹配的接口
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    // 3. 如果目标对象有需要拦截的接口,则创建JDK动态代理
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  // 4. 否则,直接返回原对象
  return target;
}

// Plugin是InvocationHandler的实现
public class Plugin implements InvocationHandler {
  // ...
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // A. 如果当前调用的方法在拦截签名中,则调用Interceptor的intercept方法
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // B. 否则,直接调用目标原方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
}

8.2 责任链的形成

当一个目标对象(如 Executor)被创建时,MyBatis 会立刻调用 interceptorChain.pluginAll(executor)。这个过程形成了一个嵌套的代理链,这正是责任链模式的体现:

  1. 原始对象 Executor 是链尾。
  2. 拦截器A的 plugin 方法会创建一个代理 ProxyA,它持有 Executor 的引用。
  3. 拦截器B的 plugin 方法会再创建一个代理 ProxyB,它持有 ProxyA 的引用。
  4. ..。
  5. 最终返回的是最外层的代理对象。

当外部调用 executor.query() 方法时,调用流程如下: ProxyB.invoke → (匹配B的拦截方法) → InterceptorB.intercept → 在B的拦截逻辑中调用 invocation.proceed() → 这又会调用 ProxyA.invoke → (匹配A的拦截方法) → InterceptorA.intercept → 在A的拦截逻辑中调用 invocation.proceed() → 最终调用原始 Executorquery() 方法。

插件可拦截的四大对象和目标方法如下。插件开发实战将在《插件开发与拦截链》专篇展开,但这里的原理是基础。

  • Executor(query, update, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • StatementHandler(prepare, parameterize, batch, update, query)
  • ParameterHandler(getParameterObject, setParameters)
  • ResultSetHandler(handleResultSets, handleOutputParameters)

下面的序列图清晰地展示了这个层层代理的拦截过程。

sequenceDiagram participant Caller as 调用者 participant ProxyB as InterceptorB代理(最外层) participant ProxyA as InterceptorA代理 participant RealSubject as 真实目标(Executor) Caller->>ProxyB: query() activate ProxyB ProxyB->>ProxyB: InvocationHandler.invoke() note right of ProxyB: 匹配B的@Signature ProxyB->>ProxyB: interceptorB.intercept(invocation) note right of ProxyB: B的前置增强... ProxyB->>ProxyA: invocation.proceed() -> query() activate ProxyA ProxyA->>ProxyA: InvocationHandler.invoke() note right of ProxyA: 匹配A的@Signature ProxyA->>ProxyA: interceptorA.intercept(invocation) note right of ProxyA: A的前置增强... ProxyA->>RealSubject: invocation.proceed() -> query() activate RealSubject RealSubject->>RealSubject: 执行真正的业务逻辑 RealSubject-->>ProxyA: 返回结果 deactivate RealSubject note right of ProxyA: A的后置增强... ProxyA-->>ProxyB: 返回结果 deactivate ProxyA note right of ProxyB: B的后置增强... ProxyB-->>Caller: 返回最终结果 deactivate ProxyB
  • 图表主旨概括:本序列图描绘了 MyBatis 插件拦截链的责任链模式实现。它展示了当有多个拦截器时,方法调用是如何从最外层的代理对象层层传递,最终到达真实目标对象的。
  • 逐层/逐元素分解
    • 链的构建 :由 InterceptorChain.pluginAll() 静态构建,形成 new ProxyB(new ProxyA(realObj)) 的结构。
    • 链的调用 :通过 Invocation.proceed() 方法,每个拦截器可以决定是否将调用传递给链中的下一个处理器,这提供了增强或终止链的能力。
  • 设计原理映射责任链模式JDK动态代理 。目标对象在链尾,多个 Interceptor 以代理的形式串联在一起。
  • 工程联系与关键结论插件的执行顺序与 plugins 配置文件中的声明顺序一致。理解 pluginAll 的层层包装机制,是避免插件间相互影响和正确开发自定义插件的前提。

9. 一个 SQL 请求的完整执行时序

作为对前几个模块的总结,让我们用一个精练但完整的序列图,串联起一个 selectList 请求穿越 MyBatis 四层架构的全过程。

sequenceDiagram participant Client as 应用代码 participant SqlSess as DefaultSqlSession participant CachingEx as CachingExecutor participant BaseEx as BaseExecutor participant SimpleEx as SimpleExecutor participant RoutingH as RoutingStatementHandler participant PrepH as PreparedStatementHandler participant ParamH as DefaultParameterHandler participant JDBC as PreparedStatement participant ResultH as DefaultResultSetHandler Client->>SqlSess: 1. selectList(statement, param) SqlSess->>CachingEx: 2. query(ms, param, ...) activate CachingEx CachingEx->>CachingEx: 3. 检查二级缓存 (TransactionalCache) note right of CachingEx: 二级缓存未命中 CachingEx->>BaseEx: 4. delegate.query(...) deactivate CachingEx activate BaseEx BaseEx->>BaseEx: 5. 模板方法骨架开始
创建CacheKey BaseEx->>BaseEx: 6. 检查一级缓存 (localCache) note right of BaseEx: 一级缓存未命中 BaseEx->>BaseEx: 7. queryFromDatabase() BaseEx->>SimpleEx: 8. 钩子方法:doQuery() deactivate BaseEx activate SimpleEx SimpleEx->>RoutingH: 9. new RoutingStatementHandler(ms, ...) note right of RoutingH: 策略路由:创建PreparedStatementHandler SimpleEx->>RoutingH: 10. prepare(connection, ...) RoutingH->>PrepH: 11. delegate.prepare() PrepH->>JDBC: 12. connection.prepareStatement(sql) JDBC-->>PrepH: PreparedStatement实例 SimpleEx->>RoutingH: 13. parameterize(statement) RoutingH->>PrepH: 14. delegate.parameterize() PrepH->>ParamH: 15. setParameters(ps) ParamH->>JDBC: 16. ps.setXxx(参数)
[通过TypeHandler] SimpleEx->>PrepH: 17. query(statement, ...) PrepH->>JDBC: 18. ps.execute() JDBC-->>PrepH: ResultSet结果集 PrepH->>ResultH: 19. handleResultSets(statement) activate ResultH ResultH->>ResultH: 20. 遍历ResultSet
通过ResultMap和TypeHandler
将行数据映射为Java对象 ResultH-->>PrepH: 映射后的Java对象列表 deactivate ResultH PrepH-->>SimpleEx: 结果列表 SimpleEx->>SimpleEx: 21. closeStatement(stmt) SimpleEx-->>BaseEx: 结果列表 BaseEx->>BaseEx: 22. localCache.putObject(key, list) BaseEx-->>CachingEx: 结果列表 CachingEx->>CachingEx: 23. tcm.putObject(cache, key, list) CachingEx-->>SqlSess: 结果列表 SqlSess-->>Client: 结果列表
  • 图表主旨概括 :本序列图完整地追踪了一个 SQL 查询从应用层 selectList 调用开始,经 SqlSession 门面、CachingExecutor 装饰器、BaseExecutor 模板方法、SimpleExecutor 钩子、RoutingStatementHandler 策略路由、PreparedStatementHandler 执行、ParameterHandler 参数设置,到 ResultSetHandler 结果映射,并缓存在一二级缓存的完整生命周期。
  • 逐层/逐元素分解 :图中使用彩色文字标注了模板方法骨架钩子方法策略路由等关键设计模式的应用点,清晰地展示了它们在调用链中的具体位置。
  • 设计原理映射
    • 18-25步是 BaseExecutor 模板方法模式的体现。
    • 12-13步是 RoutingStatementHandler 策略模式的体现。
    • 4-7步和31-32步是 CachingExecutor 装饰器模式的体现。
    • 整个执行链路中,InterceptorChain 可以在多个节点(ExecutorStatementHandler 等)进行拦截,体现了责任链模式。
  • 工程联系与关键结论理解了这张图,你就理解了 MyBatis 的所有核心流程。它揭示了 MyBatis 如何在极致的解耦和清晰的职责划分下,完成一项看似简单的数据库查询任务。任何性能分析和故障排查都可以在这张图的不同节点上找到突破口。

10. 设计模式总结与 Spring 核心容器对比

MyBatis 的源代码是设计模式的教科书级范例,下面总结其在本文中体现的关键模式。

设计模式 MyBatis 具体应用
门面模式 SqlSession / DefaultSqlSession 封装了底层 ExecutorStatementHandler 等复杂性,提供统一 API。
模板方法模式 BaseExecutor.query/update 是骨架,doQuery/doUpdate 等是留给子类的钩子。BaseStatementHandler 也有类似体现。
策略模式 BaseExecutor 的子类(Simple/Reuse/Batch)是不同执行策略。RoutingStatementHandler 根据类型路由到不同的 StatementHandler
装饰器模式 CachingExecutorExecutor 外层装饰了二级缓存功能。
责任链模式 InterceptorChain.pluginAll 通过 JDK 动态代理,将所有 Interceptor 串联成一个拦截链。
工厂模式 SqlSessionFactory 负责创建 SqlSessionTransactionFactory 负责创建 Transaction
建造者模式 SqlSessionFactoryBuilder 用来解析配置文件,一步一步构建出复杂的 SqlSessionFactory 对象。

10.1 与 Spring 核心容器的设计对照

将 MyBatis 的核心设计与 Spring 核心容器进行对比,可以发现大师们在处理相似问题时的异曲同工之妙。

模板方法模式:BaseExecutor vs AbstractPlatformTransactionManager

两者的骨架方法都定义了严格的操作流程,但侧重点不同。

  • BaseExecutor.query() 骨架 :其模板逻辑是业务流程 ,围绕缓存管理(清、查、存)展开,而将真正的数据库操作交给子类。钩子方法(doQuery)粒度较大,直接完成整个数据库交互。
  • AbstractPlatformTransactionManager.getTransaction() 骨架 :其模板逻辑是事务传播行为的处理 ,这是一个复杂的策略决策过程。它根据当前存在的事务状态,决定是创建、挂起还是抛出异常。它的钩子方法(doBegin, doSuspend, doResume, doCommit, doRollback)粒度更细,分别对应事务生命周期的不同阶段。

结论 :MyBatis 的模板方法关注单次操作生命周期的增强 ,而 Spring 的模板方法关注复杂行为的策略性协调

责任链模式:InterceptorChain.pluginAll vs ReflectiveMethodInvocation.proceed

两者都实现了责任链模式,但实现机制截然不同。

  • MyBatis 的实现(静态/包装式)

    • 机制 :通过 InterceptorChain.pluginAll 在对象创建时,静态地用 JDK 动态代理层层包装。
    • 结构new ProxyA(new ProxyB(realObj))
    • 调用 :通过 Invocation.proceed() 手动传递调用。
    • 特点:链的构建是一次性的,之后不可变。目标对象被彻底封装在代理链内部。直观,但链条增长会增加方法调用栈深度。
  • Spring AOP 的实现(动态/递归式)

    • 机制 :在方法调用时,动态地 根据 Advisor 匹配结果,生成一个 ReflectiveMethodInvocation 对象。
    • 结构 :一个List<Interceptor>和一个索引指针currentInterceptorIndex
    • 调用 :核心在于一个递归的 proceed() 方法。每次调用 proceed(),索引+1,取出链中的下一个拦截器,并调用其 invoke(this)this 就是 MethodInvocation 本身。拦截器必须在内部手动调用 mi.proceed() 来驱动链条。
    • 特点:链的构建和执行都更灵活,可以根据运行时状态动态调整。

结论 :MyBatis 的代理链更偏向于静态增强 ,像一个洋葱,一层层静态包裹。而 Spring AOP 的链是动态执行,像一个单向链表,通过指针递增和递归调用动态推进。MyBatis 的方式对目标对象的侵入性更小(无需实现任何接口),而 Spring AOP 的方式则提供了更大的运行时灵活性。

11. 生产事故排查专题

事故一:一级缓存在事务未提交时导致"脏读"

  • 现象:在同一个 Service 方法中,事务尚未提交。先通过方法 A 更新了某个字段,然后调用查询方法 B 查询同一条数据,结果 B 返回的是更新前的旧数据。
  • 排查 :检查日志发现,更新操作和查询操作都在同一个 SqlSession 内。分析 MyBatis 源码,BaseExecutor.query 会先检查一级缓存。由于更新和查询发生在同一个 SqlSessionquery 方法直接从 localCache 中取到了更新前缓存的旧数据,而未去数据库进行二次查询。
  • 根因 :对 MyBatis 一级缓存机制理解不足。BaseExecutor 中的 localCacheSqlSession 级别的。虽然 MyBatis 在执行 update 操作时通常会清空 localCache,但在某些复杂场景下(如通过 update(sql) 不更新特定对象的场景,或使用 @Options(flushCache = FlushCachePolicy.FALSE)),缓存可能未被正确清理。
  • 解决
    1. 显式调用 sqlSession.clearCache()
    2. 在那个查询方法上使用 @Options(flushCache = Options.FlushCachePolicy.TRUE),强制在查询前清空一级缓存。
    3. 将可能引起数据不一致的操作放在不同的 SqlSession 中。
  • 最佳实践在涉及混合读写且对数据一致性要求极高的业务场景中,需要警惕一级缓存的存在。避免在同一个 SqlSession 中执行完更新再执行期望获取最新数据的查询,或者主动管理一级缓存的生命周期。

事故二:BatchExecutor 未及时刷新导致 SQL 丢失

  • 现象:一个批量插入大量数据的离线任务,运行结束后,发现大量数据并未成功写入数据库,且日志没有报错。
  • 排查 :检查代码发现,为提升性能,使用了 BatchExecutor 执行批量插入。代码在循环中调用了 xxxMapper.insert(entity),但在循环结束后没有调用 sqlSession.flushStatements()。随后直接提交了事务。
  • 根因 :对 BatchExecutor 工作机制的误解。BatchExecutordoUpdate 方法只是将参数缓存到 BatchResult 中,并通过 statement.addBatch() 添加到批处理,并不会立即执行。真正的执行发生在 flushStatements() 中。而 DefaultSqlSession.commit() 内部虽然会调用 flushStatements(true),但这个行为取决于具体的 MyBatis 版本和配置。在发生未捕获异常导致的回滚时,这些未刷新的批处理语句就会丢失。
  • 解决 :在所有批处理操作结束后,显式调用一次 sqlSession.flushStatements()
  • 最佳实践使用 BatchExecutor 时,务必在事务提交临界点前手动调用 flushStatements(),确保所有批量操作都被发送到数据库执行。习惯性地在 finally 块中进行 flushStatementscommit/rollback 操作。

事故三:Executor 类型选择不当导致 OOM

  • 现象 :一个数据查询服务,需要从单表中读取数百万条数据进行处理。系统以 OUT_OF_MEMORY 错误崩溃,堆内存被耗尽。
  • 排查 :分析堆 Dump 文件,发现大量 HashMap 和数据库结果集相关的对象占满了内存。经查,系统使用了 MyBatis 默认的 SimpleExecutor,并且没有启用流式查询。
  • 根因
    1. 使用默认的 SimpleExecutor 执行全表查询,MyBatis 会将所有结果一次性映射为一个 List 并全部加载到内存中,导致内存溢出。
    2. ReuseExecutor 在此场景下会有改善,但所有 Statement 对象会被缓存,依然会占用可观的资源。
    3. 未使用流式查询或分页。
  • 解决
    1. 核心方案 :改用流式查询(ResultHandler)。ResultHandler 是 MyBatis 提供的回调接口,可以逐条处理查询结果,而不用将整个结果集加载到内存。这需要在创建 SqlSession 时或查询方法中指定。
    2. 替代方案:使用物理分页,将海量数据分批次查询处理。
  • 最佳实践绝不要在大数据量查询场景下使用 selectList 等一次性返回全部结果的方法。必须使用 ResultHandler 或分页插件来避免 OOM。Executor 类型的选择对大数据量操作的性能和稳定性至关重要。

12. 面试高频专题

  1. MyBatis 的四大对象是什么?各自的职责?

    • 标准回答:Executor、StatementHandler、ParameterHandler、ResultSetHandler。Executor 负责调度和执行,管理缓存和事务。StatementHandler 负责创建并配置 JDBC Statement。ParameterHandler 负责将 Java 参数映射到 Statement。ResultSetHandler 负责将 JDBC ResultSet 映射为 Java 对象。
    • 多角度追问
      • 追问1 :它们之间是如何协作的?请描述一个 selectList 的完整调用过程。
      • 追问2:StatementHandler 内部又分了哪几种子类型?它们是如何被选择的?(考察策略模式)
      • 追问3 :如果我要实现一个慢 SQL 告警功能,我应该拦截哪个对象的哪个方法?(StatementHandlerquery/update 或在 Executorquery/update 中)
    • 加分回答:能够画出四大对象的类关系图,并点出各个对象运用了哪些设计模式。能区分 MyBatis 核心处理层与基础支持层。
  2. MyBatis 的 Executor 有哪几种类型?各自的区别和适用场景?在实际项目中如何选择?

    • 标准回答 :三种:SimpleExecutor(默认,每次新建 Statement)、ReuseExecutor(缓存 Statement,复用)、BatchExecutor(批量处理)。默认 SimpleExecutor 适合绝大多数简单操作。ReuseExecutor 适合管理资源紧张且重复语句多的事务。BatchExecutor 专门用于大规模批量写操作。
    • 多角度追问
      • 追问1 :在 MyBatis 全局配置和一次 openSession 调用中,如何指定使用哪种 Executor?
      • 追问2 :在 Spring 整合环境下,SqlSessionTemplateexecutorType 默认是什么?如果是 BATCH,需要注意什么?(SqlSessionTemplate 是线程安全的,但 BatchExecutor 有状态)
      • 追问3 :使用 ReuseExecutor 时,如果 SQL 语句用到的参数变了,Statement 中的参数会如何更新?(handler.parameterize(stmt) 重新设置参数)
    • 加分回答 :能够结合 BaseExecutor 的模板方法模式和 CachingExecutor 的装饰者模式,说明他们与 Executor 的关系。
  3. BaseExecutor 是如何使用模板方法模式的?

    • 标准回答BaseExecutorquery() 方法定义了查询的主流程(缓存检查、查询、缓存写入),而将 doQuerydoUpdate 等具体执行方法延迟到子类实现。
    • 多角度追问
      • 追问1 :为什么 queryFromDatabasedoQuery 要拆成两个方法?
      • 追问2queryStack 字段在模板方法中扮演什么角色?(防止递归查询时的错误缓存清理)
      • 追问3 :这个模板方法和 Spring AbstractPlatformTransactionManager.getTransaction 的模板方法在设计意图上有何不同?
    • 加分回答:能对比 Spring 的模板方法,指出 MyBatis 的模板更偏向单次操作生命周期的管理,而 Spring 的更偏向于复杂策略的协调。
  4. CachingExecutor 的装饰器模式有什么优势?

    • 标准回答 :在不改变 SimpleExecutor 等原始执行器代码的前提下,动态地为其增加二级缓存功能。这符合开闭原则,对扩展开放,对修改关闭。
    • 多角度追问
      • 追问1CachingExecutor 中的 TransactionalCacheManager 是用来做什么的?
      • 追问2:如果我想实现一个日志记录,在每次 SQL 执行前后打印日志,用装饰器模式好还是用插件好?为什么?
      • 追问3:装饰器模式和代理模式的区别是什么?
    • 加分回答 :能清晰解释 CachingExecutor 不仅实现了接口,还持有另一个 Executor 实例的组合关系,这是装饰器模式区别于普通代理的关键。
  5. RoutingStatementHandler 是如何选择具体的 StatementHandler 的?

    • 标准回答 :在 RoutingStatementHandler 的构造函数中,根据 MappedStatement 对象里配置的 statementType 属性,通过一个 switch 语句来创建 SimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler
    • 多角度追问
      • 追问1statementType 在哪里配置?
      • 追问2:这也是一种策略模式,它和 Executor 体系中用子类实现的策略模式有何区别?
      • 追问3:如果 MyBatis 未来要支持一种新的 Statement 类型,应该如何扩展?
    • 加分回答 :能指出 RoutingStatementHandler 是策略上下文,其构建具体策略的时机是在构造时,是一种比较简单的策略模式应用。
  6. 一级缓存和二级缓存在 Executor 层面是如何实现的?

    • 标准回答
      • 一级缓存 :在 BaseExecutor 中通过 localCache(一个 PerpetualCache,本质是 HashMap)实现,SqlSession 级别,默认开启。
      • 二级缓存 :通过 CachingExecutor 装饰器实现,MappedStatement 级别(命名空间级别),可在多个 SqlSession 间共享,需要显式配置。
    • 多角度追问
      • 追问1:一级缓存可能导致什么问题?如何解决?
      • 追问2 :二级缓存的脏数据风险是如何通过 TransactionalCacheManager 来控制的?如果事务回滚,暂存的数据会怎样?
      • 追问3:一个查询是先查一级缓存还是二级缓存?
    • 加分回答 :能够结合源码说明一级缓存的 queryStack 和二级缓存的 TransactionalCacheManager 这两个精巧设计。 好的,这是您要求的后续面试题目(7-15)的完整内容,严格按照"标准回答 + 多角度追问 + 加分回答"的结构撰写,无任何省略。

  1. MyBatis 的插件机制是如何设计的?用到了什么设计模式?

    • 标准回答 :MyBatis 插件机制基于 JDK 动态代理责任链模式 实现。开发者实现 Interceptor 接口并添加 @Intercepts@Signature 注解指定拦截目标。InterceptorChain 在创建四大对象(ExecutorStatementHandlerParameterHandlerResultSetHandler)时,调用 pluginAll(target),逐个遍历已注册的 Interceptor,每个拦截器通过 Plugin.wrap(target, interceptor) 判断是否需要拦截,若需要则生成一个动态代理对象。这样多个拦截器就会形成一个层层包裹的代理链,调用时从最外层开始,依次通过每个拦截器的 intercept 方法,最终到达真实对象。这体现了责任链模式:请求在一条由拦截器组成的链上传递,每个拦截器都可以处理请求或将其传递给链中的下一个。
    • 多角度追问
      • 追问1:插件可以对哪四个对象进行拦截?分别能拦截哪些方法?这些方法的签名是从哪里定义的?
      • 追问2:如果同一个方法被多个插件拦截,它们的执行顺序是怎样的?如何控制这种顺序?
      • 追问3 :在插件中调用 invocation.proceed() 会触发什么?如果忘记调用 proceed() 会有什么后果?这背后的原理是什么?
      • 追问4:MyBatis 的插件拦截和 Spring AOP 的拦截有哪些本质区别?(结合第 13 题思考)
    • 加分回答 :能结合 Plugin 类的源码说明 InvocationHandler 的实现,指出 signatureMap 的作用,并解释为何代理的对象必须是接口(JDK 动态代理的限制)。还能画出插件代理链的嵌套结构图,说明 pluginAll 的包装顺序是"先注册(配置)的先包裹,后注册的后包裹,形成'洋葱'结构"。
  2. SqlSessionFactory 和 SqlSessionFactoryBuilder 的区别?

    • 标准回答SqlSessionFactoryBuilder 是一个建造者 ,负责解析 MyBatis 配置文件(XML 或 Configuration 对象),并构建出 SqlSessionFactory 实例。它的生命周期短暂,一旦工厂构建完成即可销毁。SqlSessionFactory 是一个工厂 ,负责创建 SqlSession 实例。它一旦创建就应在应用生命周期内持续存在,通常采用单例模式。
    • 多角度追问
      • 追问1SqlSessionFactoryBuilder 有哪几个重载的 build 方法?它们分别接受哪些参数?
      • 追问2 :为什么 SqlSessionFactoryBuilder 被设计为即时销毁,而 SqlSessionFactory 要全局唯一?
      • 追问3SqlSessionFactory 创建的 SqlSession 默认 ExecutorType 是什么?如何在 openSession 时指定不同的 ExecutorType
      • 追问4 :在 Spring 整合环境中,我们是直接操作 SqlSessionFactory 吗?SqlSessionTemplate 是如何封装 SqlSessionFactory 的?
    • 加分回答 :能点出 SqlSessionFactoryBuilder 内部是通过 XMLConfigBuilder 解析配置,构建出 Configuration 对象,然后调用 new DefaultSqlSessionFactory(config)。这体现了建造者模式工厂模式的完美配合:建造者组装复杂部件,工厂则批量生产产品。
  3. 为什么说 SqlSession 是一个门面模式?

    • 标准回答SqlSession 接口对外提供了 selectOneselectListinsertupdatedeletecommitrollback 等众多操作数据库的 API,隐藏了 MyBatis 内部复杂的核心处理层(ExecutorStatementHandlerResultSetHandler 等)的交互细节。客户端只需调用 SqlSession 这个门面,无需了解内部的执行器路由、缓存管理、语句处理、参数映射等复杂逻辑,这正是门面模式的典型应用:为子系统中的一组接口提供一个统一的高层接口。
    • 多角度追问
      • 追问1DefaultSqlSession 中的 selectOne 方法,其内部实现逻辑是怎样的?(点出委托给 executor.query
      • 追问2 :除了 DefaultSqlSession,MyBatis 还提供了哪些 SqlSession 的增强实现?它们在门面之上又增加了什么功能?(例如 SqlSessionManager、Spring 中的 SqlSessionTemplate
      • 追问3 :门面模式和代理模式的区别是什么?SqlSession 是门面,那 MapperProxy 是代理还是门面?
    • 加分回答 :能够通过时序图展示一个 selectList 调用如何从 SqlSession 一路穿透到 JDBC Statement,并说明这种设计将客户端与核心组件解耦,使得核心处理层的任何变化(如无缝替换 Executor 实现)都不会影响客户端调用。
  4. 一个 Mapper 方法调用最终是如何触发 JDBC Statement 执行的?

    • 标准回答 :当调用 userMapper.getUserById(1L) 时,流程如下:
      1. 该调用会被 MapperProxy(JDK 动态代理)拦截。
      2. MapperProxy 根据方法签名查找对应的 MappedStatement
      3. 决定执行类型(SqlCommandType),调用 sqlSession.selectOne
      4. DefaultSqlSession 将请求委托给 CachingExecutor.query
      5. CachingExecutor 检查二级缓存,未命中则交给 BaseExecutor.query
      6. BaseExecutor 检查一级缓存,未命中则调用子类 SimpleExecutor.doQuery
      7. SimpleExecutor 创建 RoutingStatementHandler,其内部创建 PreparedStatementHandler
      8. 通过 StatementHandler.prepare 创建 JDBC PreparedStatement,再通过 ParameterHandler 设置参数。
      9. 调用 PreparedStatement.execute() 执行 SQL。
      10. ResultSetHandler 处理结果集并返回。
    • 多角度追问
      • 追问1MapperProxy 是如何将 Mapper 接口方法与 MappedStatement 关联起来的?MapperMethod 起了什么作用?
      • 追问2 :如果方法参数中有 @Param("id") 注解,参数是如何在 ParameterHandler 中被正确读取到的?
      • 追问3 :当返回结果是 List 或单个对象时,执行分支有何不同?selectOneselectListDefaultSqlSession 层是怎么实现的?
    • 加分回答 :能完整画出从代理方法调用到 ResultSetHandler 返回结果的序列图,并标出每一层所用的设计模式。同时可提及 SqlCommandMethodSignature 等辅助类的职责。
  5. MyBatis 和 Hibernate/JPA 在架构设计上的本质区别?

    • 标准回答 :本质上是SQL 中心 vs 对象中心 两种 ORM 哲学的体现。
      • MyBatis:面向 SQL 的半自动化框架。架构设计的核心是 SQL 执行链路的优化,它不做自动映射,而是将 SQL 的控制权完全交给开发者。其核心组件围绕 SQL 的解析、参数设置、执行和结果集映射展开。
      • Hibernate/JPA:面向对象的全自动化框架 。架构设计的核心是对象关系映射(ORM),通过 HQL/JPQL 或 Criteria API 自动生成 SQL,屏蔽了底层数据库差异。其核心组件围绕 SessionEntityManager、状态管理(持久化上下文)、一级/二级缓存、脏检查、级联操作等对象生命周期管理展开。
    • 多角度追问
      • 追问1 :在架构层面,MyBatis 的 SqlSession 和 Hibernate 的 Session 有何异同?
      • 追问2 :MyBatis 的 Executor 体系与 Hibernate 的 LoadEventListenerPersistEventListener 等事件监听器体系相比,在扩展性设计上谁更灵活?为什么?
      • 追问3:为什么说 MyBatis 更容易进行 SQL 优化,而 Hibernate 在这方面会遇到"抽象泄露"的问题?
    • 加分回答:能从设计模式角度总结:MyBatis 大量使用门面、策略、模板方法、装饰器来构建一条从接口到底层 JDBC 的通路,而 Hibernate 则更多使用状态模式(对象生命周期)、拦截器、监听器模式来管理对象图谱的变更。
  6. MyBatis 的 BaseExecutor.query 与 Spring 的 AbstractPlatformTransactionManager.getTransaction 都用了模板方法模式,它们的钩子方法设计有何异同?

    • 标准回答
      • 相同点:两者都在骨架方法中定义了一个固定的操作流程,并将流程中可变的部分抽象为钩子方法留给子类实现。
      • 不同点
        • 模板粒度BaseExecutor.query 的骨架是单次操作生命周期的管理 (清缓存、查缓存、查库、存缓存),钩子方法 doQuery 粒度很粗,直接完成了整个数据库查询。而 Spring 的 getTransaction 模板是事务传播行为的策略性协调 ,钩子方法 doBegindoSuspenddoResume 等粒度更细,分别对应事务生命周期的不同阶段。
        • 扩展目的:MyBatis 的钩子是为了实现不同的 Statement 管理策略(简单、复用、批处理)。Spring 的钩子则为了实现不同事务资源(JDBC、JTA、Hibernate)的适配。
        • 异常处理BaseExecutor.query 的骨架方法内部捕获了部分异常并封装为 PersistenceException,而 Spring 的模板则定义了完整的声明式回滚规则,异常处理更复杂。
    • 多角度追问
      • 追问1 :MyBatis 的 BaseExecutor.update 方法也是模板方法,它的骨架与 query 有何不同?
      • 追问2 :Spring 的 AbstractPlatformTransactionManager 中,getTransaction 骨架是如何处理"当前已存在事务"这一情况的?这跟策略模式有何关联?
      • 追问3 :如果要在 BaseExecutor.query 的骨架中增加一个"执行后发送事件"的功能,在不修改基类的情况下,利用 MyBatis 现有机制可以怎么做?(提示:插件)
    • 加分回答 :能对比出 MyBatis 的模板方法更侧重流程不变性 ,而 Spring 的模板方法则融入了大量的事务传播策略逻辑,是一个更复杂的"模板+策略"混合体。
  7. MyBatis 的 InterceptorChain.pluginAll 与 Spring AOP 的 ReflectiveMethodInvocation.proceed 都用了责任链模式,它们的实现方式有何区别?

    • 标准回答
      • MyBatis(静态代理包装式) :在对象创建时,通过 pluginAll 一次性将所有 Interceptor 以 JDK 动态代理的方式层层嵌套包裹在目标对象外部,形成一个静态的代理链。调用时,请求在 Plugin.invokeinterceptor.interceptinvocation.proceed() 之间传递,是一个代理嵌套的调用过程。链的构建是静态的一次性操作
      • Spring AOP(动态递归调用式) :在方法调用时,动态地创建一个 ReflectiveMethodInvocation 对象,其内部包含一个拦截器列表和一个索引。通过一个递归的 proceed() 方法驱动链条:每次调用 proceed() 都会将索引加一,并调用下一个拦截器的 invoke(this)。拦截器必须在其 invoke 方法中手动调用 mi.proceed() 来继续驱动链条。链的构建是运行时动态的,更灵活。
    • 多角度追问
      • 追问1:MyBatis 的方式有什么局限性?Spring AOP 的方式又有什么性能或调试上的挑战?
      • 追问2 :在 MyBatis 中,如果我想根据运行时的 SQL 动态决定是否执行某个插件逻辑,应该怎么做?(可以在 intercept 方法内部判断,但无法动态增删链节点,这体现了静态链的限制)
      • 追问3:从代码阅读的角度,哪种实现更容易理解调用顺序?为什么?
    • 加分回答 :能画出两种模式的调用栈示意图:MyBatis 是一个"洋葱",调用栈逐层递进;Spring AOP 是一个"单链表遍历",随着递归调用"指针"后移。同时能指出 MyBatis 的插件链实现是 "代理包装"模式 ,而 Spring 是 "拦截器迭代器"模式,虽然都叫责任链,但结构不同。
  8. SpringManagedTransaction 是如何与 Spring 事务管理器协作的?

    • 标准回答SpringManagedTransaction 是 MyBatis 与 Spring 集成时的事务实现。它通过以下方式融入 Spring 事务生态:
      1. 获取连接 :不直接从 DataSource 获取连接,而是调用 DataSourceUtils.getConnection(dataSource),该方法会从 TransactionSynchronizationManager 中获取当前线程绑定的、由 Spring 事务管理器管理的数据库连接。
      2. 提交/回滚commit()rollback() 方法内部会先检查连接是否是事务性的(通过 DataSourceUtils.isConnectionTransactional 判断)。如果是,则什么都不做 ,将事务提交/回滚的控制权完全交给 Spring 的 AbstractPlatformTransactionManager
      3. 关闭连接close() 方法调用 DataSourceUtils.releaseConnection(connection, dataSource),由 Spring 决定是释放回连接池还是解除线程绑定,而不是物理关闭连接。
    • 多角度追问
      • 追问1 :如果在一个没有 Spring 事务的方法中调用 MyBatis,SpringManagedTransaction 的行为是什么?(连接非事务性,autoCommittrue,每次操作自动提交)
      • 追问2TransactionSynchronizationManager 是如何将连接与线程绑定的?这保证了什么?(线程安全,同一事务下多个 DAO 操作共享同一连接)
      • 追问3 :如果 MyBatis 和 Spring JDBC 模板混合使用,SpringManagedTransaction 的设计如何保证它们在同一事务中?
    • 加分回答 :能深入源码,指出 SpringManagedTransaction 持有 isConnectionTransactionalautoCommit 两个标志位,并解释它们在不同事务传播行为下的状态。并说明这与 JdbcTransaction 直接管理提交/回滚形成鲜明对比,体现了"控制反转"的思想。
  9. (系统设计题)设计一个轻量级的 ORM 框架,要求支持 SQL 映射、结果集自动映射、插件拦截和缓存。请参照 MyBatis 的架构,给出核心接口和组件的设计伪代码。

    • 标准回答 和加分回答(含伪代码): 好的,我将模拟设计一个名为"TinyORM"的轻量级框架。

      核心接口与组件设计:

      java 复制代码
      // 1. 门面接口:TinySession
      public interface TinySession {
          <T> T selectOne(String sqlId, Object param);
          <T> List<T> selectList(String sqlId, Object param);
          int insert(String sqlId, Object param);
          int update(String sqlId, Object param);
          int delete(String sqlId, Object param);
          void commit();
          void rollback();
          void close();
      }
      
      // 2. 执行器接口:Executor
      public interface Executor {
          <E> List<E> query(MappedStatement ms, Object parameter);
          int update(MappedStatement ms, Object parameter);
          void commit(boolean required);
          void rollback(boolean required);
          // ... 缓存相关方法
      }
      
      // 3. 模板方法基类:BaseExecutor
      public abstract class BaseExecutor implements Executor {
          protected Cache localCache = new PerpetualCache("local"); // 一级缓存
      
          @Override
          public <E> List<E> query(MappedStatement ms, Object parameter) {
              CacheKey key = createCacheKey(ms, parameter);
              List<E> list = localCache.get(key);
              if (list != null) return list;
              list = doQuery(ms, parameter); // 钩子方法
              localCache.put(key, list);
              return list;
          }
      
          protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter);
          protected abstract int doUpdate(MappedStatement ms, Object parameter);
      }
      
      // 4. 语句处理器接口及路由:StatementHandler
      public interface StatementHandler {
          void prepare(Connection conn);
          void parameterize(Object param);
          <E> List<E> query();
          int update();
      }
      
      public class RoutingStatementHandler implements StatementHandler {
          private StatementHandler delegate;
      
          public RoutingStatementHandler(MappedStatement ms) {
              switch (ms.getStatementType()) {
                  case PREPARED: delegate = new PreparedStatementHandler(ms); break;
                  // ... 其他类型
              }
          }
          // 所有方法委托给 delegate
      }
      
      // 5. 参数处理器与结果集处理器
      public interface ParameterHandler {
          void setParameters(PreparedStatement ps, Object param);
      }
      
      public interface ResultSetHandler {
          <E> List<E> handleResultSets(Statement stmt);
      }
      
      // 6. 插件机制:Interceptor 和责任链
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      public @interface Intercepts {
          Signature[] value();
      }
      
      @Retention(RetentionPolicy.RUNTIME)
      @Target({})
      public @interface Signature {
          Class<?> type();
          String method();
          Class<?>[] args();
      }
      
      public interface Interceptor {
          Object intercept(Invocation invocation) throws Throwable;
          default Object plugin(Object target) {
              return Plugin.wrap(target, this);
          }
      }
      
      public class InterceptorChain {
          private List<Interceptor> interceptors = new ArrayList<>();
          public Object pluginAll(Object target) {
              for (Interceptor interceptor : interceptors) {
                  target = interceptor.plugin(target);
              }
              return target;
          }
          public void addInterceptor(Interceptor interceptor) {
              interceptors.add(interceptor);
          }
      }
      
      // 7. 门面实现:DefaultTinySession
      public class DefaultTinySession implements TinySession {
          private Executor executor;
          private Configuration config;
      
          @Override
          public <E> List<E> selectList(String sqlId, Object param) {
              MappedStatement ms = config.getMappedStatement(sqlId);
              return executor.query(ms, param);
          }
          // ... 其他方法类似
      }
      
      // 8. 工厂和建造者
      public class TinySessionFactoryBuilder {
          public TinySessionFactory build(InputStream configStream) {
              Configuration config = new XMLConfigParser(configStream).parse();
              return new DefaultTinySessionFactory(config);
          }
      }
      public interface TinySessionFactory {
          TinySession openSession();
      }

      设计说明

      • 采用 门面模式TinySession)封装内部复杂性。
      • 模板方法模式BaseExecutor.query())固化缓存流程,doQuery 留给子类实现策略。
      • 策略模式RoutingStatementHandler)适配不同 Statement 类型。
      • 责任链模式JDK 动态代理InterceptorChain.pluginAllPlugin)支持插件拦截。
      • 二级缓存可以通过装饰器模式实现:CachingExecutor implements Executor,内部持有一个 delegate
    • 多角度追问

      • 追问1 :如果要求支持注解 SQL(如 @Select("SELECT ...")),你的框架应该如何扩展?
      • 追问2 :在你的设计中,一级缓存如何做到线程安全?如果需要跨会话的二级缓存,应该如何设计 Cache 接口和缓存 Key 的计算逻辑?
      • 追问3 :如何让这个框架与 Spring 无缝集成?需要考虑哪些切入点?(例如提供 TinySessionFactoryBean,实现 TransactionSynchronization 等)
    • 加分回答 :能在伪代码中体现 MappedStatement 的结构设计(包含 SqlSourceResultMap 列表、StatementType),并说明 BoundSqlParameterMapping 的作用。同时考虑到集成 Spring 时,Transaction 接口的适配逻辑。


文末速查表:MyBatis 核心组件一览

组件 职责 关键方法 相关设计模式
门面 DefaultSqlSession 为数据操作提供统一API,委托给 Executor selectOne, selectList, update, insert, delete 门面模式,委托模式
构建器 SqlSessionFactoryBuilder 解析配置,构建 SqlSessionFactory build(InputStream) , build(Reader) 建造者模式
工厂 SqlSessionFactory 创建 SqlSession 实例 openSession() 工厂方法模式
核心执行器 BaseExecutor SQL 执行、一级缓存、事务管理的骨架实现 query(), update(), createCacheKey() 模板方法模式
策略执行器 SimpleExecutor, ReuseExecutor, BatchExecutor 实现具体的 JDBC Statement 管理策略 doQuery(), doUpdate() 策略模式
装饰执行器 CachingExecutor 为执行器增加二级缓存能力 query(), commit(), delegate.query() 装饰器模式
事务管理器 SpringManagedTransaction 与Spring事务集成,获取/释放事务绑定连接 getConnection(), commit(), rollback(), close() 适配器模式(将Spring事务适配为MyBatis事务)
语句路由 RoutingStatementHandler 根据 StatementType 选择具体的 Statement 处理器 构造函数,prepare(), parameterize() 策略模式
语句处理器 PreparedStatementHandler 封装 PreparedStatement 操作 parameterize(), query(), update() 适配器模式(封装JDBC差异)
参数处理器 DefaultParameterHandler 将Java参数设置到 PreparedStatement setParameters() -
结果集处理器 DefaultResultSetHandler ResultSet 映射为Java对象集合 handleResultSets() -
插件链 InterceptorChain 管理所有插件,为四大对象生成代理链 pluginAll() 责任链模式动态代理
插件 Plugin InvocationHandler 实现,封装单个插件逻辑 wrap(), invoke() JDK 动态代理

延伸阅读

  1. MyBatis 官方文档:mybatis.org/mybatis-3/
  2. 《MyBatis 3 源码深度解析》,易百教程等。
  3. Spring 事务管理核心源码:AbstractPlatformTransactionManagerTransactionSynchronizationManager
相关推荐
敖正炀2 小时前
一级/二级缓存深度:生命周期、脏读与生产最佳实践
mybatis
空中海5 小时前
MyBatis 基础认知、配置体系与核心映射
mybatis
空中海5 小时前
05 MyBatis 架构设计、渐进式综合项目与专家题库
mybatis
空中海7 小时前
03 MyBatis Spring Boot 集成、事务、测试与工程化体系
spring boot·后端·mybatis
Nicander2 天前
理解 mybatis 源码:vibe-coding一个mini-mybatis
后端·mybatis
庞轩px2 天前
致远互联实习复盘:一条SQL替代300次循环查询,组织架构选择器从5秒降到300毫秒
java·sql·mysql·mybatis·实习经历·n+1问题·join联表查询
952363 天前
MyBatis
后端·spring·mybatis
misL NITL4 天前
idea、mybatis报错Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required
tomcat·intellij-idea·mybatis
是宇写的啊4 天前
MyBatis-Plus
java·开发语言·mybatis