概述
前文《MyBatis 与 Spring 整合原理》详细剖析了 MapperScannerRegistrar、MapperFactoryBean 和 SqlSessionTemplate 如何利用 Spring 扩展点将 MyBatis 无缝接入 Spring 容器。然而,当 Mapper 代理最终调用 SqlSession.selectOne() 时,MyBatis 内部到底发生了什么?SqlSession 如何将请求委托给 Executor?StatementHandler 如何封装 JDBC 的 PreparedStatement?InterceptorChain 如何在执行链路中织入插件逻辑?本文将正面拆解 MyBatis 的内部执行链路,揭示其架构设计的精妙之处。
MyBatis 作为 Java 生态中最受欢迎的持久化框架之一,其成功的核心在于精巧的架构设计。它不像 Hibernate 那样试图隐藏 SQL,而是将 SQL 的控制权交给开发者,同时通过 SqlSession、Executor、StatementHandler、ResultSetHandler 等核心组件的精密协作,将 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 的大脑和心脏,负责完成所有核心的数据库操作流程。它包含四大核心对象:
- Executor(执行器):负责调度和执行 SQL。它管理一级缓存和事务,是整个执行流程的总指挥。
- StatementHandler(语句处理器):负责封装 JDBC Statement 操作,如创建
Statement或PreparedStatement,并调用ParameterHandler设置参数。 - ParameterHandler(参数处理器):负责将用户传入的 Java 参数映射到
PreparedStatement的占位符上。 - ResultSetHandler(结果集处理器):负责将 JDBC 返回的
ResultSet映射成 Java 对象集合。
- 基础支持层:提供最底层的通用能力,包括配置解析(
XNode、XMLConfigBuilder)、类型转换(TypeHandler体系)、缓存(Cache接口及其实现)、日志(适配多种日志框架)、反射(Reflector、MetaObject)和资源加载等。这一层为上层提供坚实的基础支撑。
一条简单的 SQL 请求在 MyBatis 内部会经历如下流转路径:
SqlSession 接收请求 → 委托给 Executor → Executor 检查缓存、获取连接 → 创建 StatementHandler → StatementHandler 创建 JDBC Statement → 委托 ParameterHandler 设置参数 → 执行 SQL → 委托 ResultSetHandler 处理结果集 → 返回最终结果。
下面这张类关系图,清晰地展示了各核心接口与类之间的结构关系。
classDiagram
class SqlSessionFactory {
<<interface>>
+openSession() SqlSession
}
class SqlSession {
<<interface>>
+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 {
<<interface>>
+query(ms, parameter, rowBounds, resultHandler) List
+update(ms, parameter) int
+commit(required) void
+rollback(required) void
+flushStatements() List~BatchResult~
}
class BaseExecutor {
<<abstract>>
#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 {
<<interface>>
+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 {
<<interface>>
+setParameters(ps) void
}
class DefaultParameterHandler
class ResultSetHandler {
<<interface>>
+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 核心接口与类之间的静态结构关系。从
SqlSession到Executor再到StatementHandler、ParameterHandler和ResultSetHandler,清晰地揭示了“四大对象”的依赖层次和具体实现。 - 逐层/逐元素分解:
- 接口层:以
SqlSession和DefaultSqlSession为核心,它持有Executor引用,作为客户端调用的入口。 - 核心处理层:
Executor体系最为复杂,BaseExecutor通过模板方法模式定义了执行骨架,SimpleExecutor、ReuseExecutor、BatchExecutor是具体策略实现,而CachingExecutor则作为装饰器,为所有执行器增加了二级缓存能力。RoutingStatementHandler充当策略路由器,根据配置创建不同的StatementHandler实现。 - 基础支持层(隐式):
Configuration对象贯穿始终,为所有组件提供运行时配置。
- 接口层:以
- 设计原理映射:
- 门面模式:
SqlSession接口和DefaultSqlSession实现。 - 模板方法模式:
BaseExecutor的query()和update()方法。 - 策略模式:
BaseExecutor的不同子类和RoutingStatementHandler。 - 装饰器模式:
CachingExecutor对Executor接口的实现。
- 门面模式:
- 工程联系与关键结论:理解
DefaultSqlSession只是一个“门面”,它将所有工作委托给Executor,而Executor又依赖于StatementHandler等组件,是深入 MyBatis 源码的第一步。这种分层解耦的设计,使得每一层都可以独立演进和测试。
2. SqlSession:门面模式下的统一入口
SqlSession 是 MyBatis 为开发者提供的顶层接口。无论是执行 SQL、获取 Mapper 代理还是管理事务,所有操作都必须通过它来完成。这完美体现了门面模式的思想:为一个子系统中的一组接口提供一个统一的高层接口,从而简化子系统的使用。
2.1 门面之下的复杂性
开发者只需调用 sqlSession.selectOne("..."),但这一行代码背后,隐藏着复杂的操作序列:
- 从
Configuration中查找MappedStatement。 - 决定使用哪种
Executor。 - 检查一级/二级缓存。
- 从连接池获取数据库连接。
- 创建
StatementHandler并创建 JDBCStatement。 - 应用插件拦截链对
Executor、StatementHandler等方法的拦截。 - 处理参数映射。
- 执行 JDBC 操作。
- 处理结果集映射。
- 关闭资源和清理缓存。
DefaultSqlSession 作为默认实现,巧妙地将这些复杂性委托给了 Executor 和 Configuration。
// 代码位置: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中的selectList、selectOne、update、insert、delete等方法,其内部逻辑惊人地一致:获取MappedStatement,然后调用executor的对应方法。- 它本身不包含任何数据库访问逻辑,纯粹是一个门面和委托器。它将 MyBatis 核心处理层(
Executor、StatementHandler等)这个复杂的子系统,简化为一个易于使用的接口。 - 这种设计使得核心处理层的任何内部变化(如更换
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封装了内部的Executor、Configuration等组件的交互细节,为外部提供了一个简单、统一的接口。 - 工程联系与关键结论:
SqlSession是每个数据库会话的边界,而Executor是每个操作的生命周期控制器。理解它们的委托关系,是定位性能瓶颈和异常根源的第一步。
2.2 工厂与建造者的协作
要获取一个 SqlSession 实例,必须先创建一个 SqlSessionFactory。而 SqlSessionFactory 的创建又依赖于 SqlSessionFactoryBuilder。这里体现了工厂模式和建造者模式的协作。
- 建造者模式(
SqlSessionFactoryBuilder):用于构建复杂的SqlSessionFactory对象。它提供了多个重载的build方法,可以接收Reader、InputStream等不同来源的 MyBatis 配置文件,内部会调用XMLConfigBuilder解析 XML,最终生成一个Configuration对象,并用它来创建SqlSessionFactory。SqlSessionFactoryBuilder的生命周期很短暂,一旦工厂构建完成,它就应该被销毁。 - 工厂模式(
SqlSessionFactory):一旦SqlSessionFactory被创建,它就应该在整个应用生命周期中存在。它提供了openSession方法,用于创建新的SqlSession实例。这个工厂方法封装了创建SqlSession的细节,包括从Configuration中获取Environment(数据源和事务工厂配置),并通过TransactionFactory创建Transaction,最终组装出一个可用的DefaultSqlSession。
// 使用建造者模式创建工厂
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 设计
// 代码位置: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对象管理事务。commit和rollback方法直接调用java.sql.Connection的对应方法。它适用于独立运行、不依赖外部事务管理器的 MyBatis 应用程序。ManagedTransaction:它的commit和rollback方法是空的!它不主动管理事务,而是将事务管理权交给外部容器(如 Web 容器或 Spring 容器)。getConnection方法每次都会从数据源获取一个新的连接,这在非 Web 环境下需要谨慎处理连接关闭。
3.2 SpringManagedTransaction:与 Spring 事务的完美协作
在与 Spring 整合的环境下,MyBatis 提供了一个关键的实现:SpringManagedTransaction。这个类是 MyBatis 事务管理从“自力更生”到“融入生态”的桥梁。相关原理在《MyBatis 与 Spring 整合原理》和《Spring 事务管理抽象》中已有深入阐述,这里我们聚焦其核心机制。
// 代码位置: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 接口与模板方法骨架
// 代码位置: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 的抽象实现,定义了执行流程的骨架。这便是模板方法模式的体现。
// 代码位置: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负责实际的数据库查询。doQuery、doUpdate等抽象方法就是钩子方法,将具体执行逻辑的实现延迟到SimpleExecutor、ReuseExecutor、BatchExecutor等子类中。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 管理策略,这是策略模式的典型应用。
-
SimpleExecutor(简单执行器) 这是 MyBatis 的默认执行器。它的策略是:每次执行update或query,都会创建一个新的Statement对象,用完立即关闭。// 代码位置: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,对数据库造成压力,性能较低。 -
ReuseExecutor(可重用执行器) 它的策略是:缓存已经创建好的Statement,以 SQL 字符串为 Key。执行时,如果缓存中存在,就复用。// 代码位置: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。 -
BatchExecutor(批处理执行器) 它的策略是:只处理update操作。它将多个Statement添加到批处理队列中,通过 JDBC 的addBatch()和executeBatch()来批量提交,从而大幅提升批量插入或更新的性能。// 代码位置: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; // 返回一个不可知的值 } // ... }关键点:调用
BatchExecutor的doUpdate后,SQL 并未立即执行,而是加入了队列。只有当调用flushStatements()方法或事务提交时,才会真正执行stmt.executeBatch()。忘记调用flushStatements是导致数据未持久化的常见原因。
4.3 CachingExecutor:为执行器穿上二级缓存的“马甲”
CachingExecutor 实现了 Executor 接口,但它不亲自执行 SQL,而是将请求委托给内部的 delegate 执行器。这是标准的装饰器模式应用,它在不改变原有执行器功能的前提下,动态地为执行器增加了二级缓存的能力。
// 代码位置: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 交互的细节委托给 StatementHandler。StatementHandler 屏蔽了 Statement、PreparedStatement、CallableStatement 三种 JDBC 接口的差异。
5.1 设计:封装、路由与模板
StatementHandler 接口定义了 prepare、parameterize、query、update 等方法。其实现体系同样充满了设计智慧:
- 策略模式(路由):
RoutingStatementHandler扮演路由角色。它根据MappedStatement中配置的statementType,创建并委托给具体的StatementHandler实现。 - 模板方法模式:
BaseStatementHandler是抽象基类,它提取了所有StatementHandler的共性,如获取Configuration、BoundSql等,并定义了后续操作的骨架。 - 适配器模式(隐式):三个具体实现类——
SimpleStatementHandler(处理普通 SQL)、PreparedStatementHandler(处理预编译 SQL)、CallableStatementHandler(处理存储过程)——分别封装了不同类型 JDBC Statement 的差异,为上层Executor提供了统一的接口。
// 代码位置: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 类型是 Statement、PreparedStatement 还是 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
// 代码位置: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的指定位置(例如作为VARCHAR或TIMESTAMP)。TypeHandler体系是 MyBatis 类型转换的核心,其扩展机制将在后续篇章详述。
7. ResultSetHandler 与结果集映射
JDBC 执行完成后,返回一个 ResultSet。ResultSetHandler 的职责就是将这个 ResultSet 转换为应用程序需要的 Java 对象列表。
7.1 DefaultResultSetHandler 的处理流程
// 代码位置: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列表,调用applyPropertyMappings从ResultSet中获取列值并设置到对象属性中。每一步都离不开TypeHandler。 - 嵌套映射与延迟加载:当遇到
<association>或<collection>标签定义的嵌套映射时,MyBatis 采用截然不同的处理逻辑(handleRowValuesForNestedResultMap)。对于延迟加载,此时并不会立即执行关联查询,而是会创建一个目标对象的代理对象(通过 CGLIB 或 Javassist),并在代理对象中记录下关联查询所需的所有信息。只有当真正访问该属性时,才会触发代理对象去执行额外的 SQL 查询。延迟加载的详细机制将在《映射器原理》篇中深入。
8. 插件拦截链:责任链模式在 MyBatis 中的应用
MyBatis 的插件机制是其扩展性的核心。它允许开发者在 SQL 执行过程中的特定点进行拦截和增强,例如分页、加密、审计等功能。其底层实现巧妙地运用了 JDK 动态代理和责任链模式。
8.1 InterceptorChain 的实现原理
插件拦截的核心类是 InterceptorChain。
// 代码位置: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 静态方法。
// 代码位置: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)。这个过程形成了一个嵌套的代理链,这正是责任链模式的体现:
- 原始对象
Executor是链尾。 - 拦截器A的
plugin方法会创建一个代理ProxyA,它持有Executor的引用。 - 拦截器B的
plugin方法会再创建一个代理ProxyB,它持有ProxyA的引用。 - ..。
- 最终返回的是最外层的代理对象。
当外部调用 executor.query() 方法时,调用流程如下:
ProxyB.invoke → (匹配B的拦截方法) → InterceptorB.intercept → 在B的拦截逻辑中调用 invocation.proceed() → 这又会调用 ProxyA.invoke → (匹配A的拦截方法) → InterceptorA.intercept → 在A的拦截逻辑中调用 invocation.proceed() → 最终调用原始 Executor 的 query() 方法。
插件可拦截的四大对象和目标方法如下。插件开发实战将在《插件开发与拦截链》专篇展开,但这里的原理是基础。
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. <font color="red">模板方法骨架开始</font><br>创建CacheKey
BaseEx->>BaseEx: 6. 检查一级缓存 (localCache)
note right of BaseEx: 一级缓存未命中
BaseEx->>BaseEx: 7. queryFromDatabase()
BaseEx->>SimpleEx: 8. <font color="red">钩子方法:doQuery()</font>
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(参数) <br> [通过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<br>通过ResultMap和TypeHandler<br>将行数据映射为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可以在多个节点(Executor、StatementHandler等)进行拦截,体现了责任链模式。
- 18-25步是
- 工程联系与关键结论:理解了这张图,你就理解了 MyBatis 的所有核心流程。它揭示了 MyBatis 如何在极致的解耦和清晰的职责划分下,完成一项看似简单的数据库查询任务。任何性能分析和故障排查都可以在这张图的不同节点上找到突破口。
10. 设计模式总结与 Spring 核心容器对比
MyBatis 的源代码是设计模式的教科书级范例,下面总结其在本文中体现的关键模式。
| 设计模式 | MyBatis 具体应用 |
|---|---|
| 门面模式 | SqlSession / DefaultSqlSession 封装了底层 Executor、StatementHandler 等复杂性,提供统一 API。 |
| 模板方法模式 | BaseExecutor.query/update 是骨架,doQuery/doUpdate 等是留给子类的钩子。BaseStatementHandler 也有类似体现。 |
| 策略模式 | BaseExecutor 的子类(Simple/Reuse/Batch)是不同执行策略。RoutingStatementHandler 根据类型路由到不同的 StatementHandler。 |
| 装饰器模式 | CachingExecutor 在 Executor 外层装饰了二级缓存功能。 |
| 责任链模式 | InterceptorChain.pluginAll 通过 JDK 动态代理,将所有 Interceptor 串联成一个拦截链。 |
| 工厂模式 | SqlSessionFactory 负责创建 SqlSession。TransactionFactory 负责创建 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()来驱动链条。 - 特点:链的构建和执行都更灵活,可以根据运行时状态动态调整。
- 机制:在方法调用时,动态地根据 Advisor 匹配结果,生成一个
结论:MyBatis 的代理链更偏向于静态增强,像一个洋葱,一层层静态包裹。而 Spring AOP 的链是动态执行,像一个单向链表,通过指针递增和递归调用动态推进。MyBatis 的方式对目标对象的侵入性更小(无需实现任何接口),而 Spring AOP 的方式则提供了更大的运行时灵活性。
11. 生产事故排查专题
事故一:一级缓存在事务未提交时导致“脏读”
- 现象:在同一个 Service 方法中,事务尚未提交。先通过方法 A 更新了某个字段,然后调用查询方法 B 查询同一条数据,结果 B 返回的是更新前的旧数据。
- 排查:检查日志发现,更新操作和查询操作都在同一个
SqlSession内。分析 MyBatis 源码,BaseExecutor.query会先检查一级缓存。由于更新和查询发生在同一个SqlSession,query方法直接从localCache中取到了更新前缓存的旧数据,而未去数据库进行二次查询。 - 根因:对 MyBatis 一级缓存机制理解不足。
BaseExecutor中的localCache是SqlSession级别的。虽然 MyBatis 在执行update操作时通常会清空localCache,但在某些复杂场景下(如通过update(sql)不更新特定对象的场景,或使用@Options(flushCache = FlushCachePolicy.FALSE)),缓存可能未被正确清理。 - 解决:
- 显式调用
sqlSession.clearCache()。 - 在那个查询方法上使用
@Options(flushCache = Options.FlushCachePolicy.TRUE),强制在查询前清空一级缓存。 - 将可能引起数据不一致的操作放在不同的
SqlSession中。
- 显式调用
- 最佳实践:在涉及混合读写且对数据一致性要求极高的业务场景中,需要警惕一级缓存的存在。避免在同一个
SqlSession中执行完更新再执行期望获取最新数据的查询,或者主动管理一级缓存的生命周期。
事故二:BatchExecutor 未及时刷新导致 SQL 丢失
- 现象:一个批量插入大量数据的离线任务,运行结束后,发现大量数据并未成功写入数据库,且日志没有报错。
- 排查:检查代码发现,为提升性能,使用了
BatchExecutor执行批量插入。代码在循环中调用了xxxMapper.insert(entity),但在循环结束后没有调用sqlSession.flushStatements()。随后直接提交了事务。 - 根因:对
BatchExecutor工作机制的误解。BatchExecutor的doUpdate方法只是将参数缓存到BatchResult中,并通过statement.addBatch()添加到批处理,并不会立即执行。真正的执行发生在flushStatements()中。而DefaultSqlSession.commit()内部虽然会调用flushStatements(true),但这个行为取决于具体的 MyBatis 版本和配置。在发生未捕获异常导致的回滚时,这些未刷新的批处理语句就会丢失。 - 解决:在所有批处理操作结束后,显式调用一次
sqlSession.flushStatements()。 - 最佳实践:使用
BatchExecutor时,务必在事务提交临界点前手动调用flushStatements(),确保所有批量操作都被发送到数据库执行。习惯性地在finally块中进行flushStatements和commit/rollback操作。
事故三:Executor 类型选择不当导致 OOM
- 现象:一个数据查询服务,需要从单表中读取数百万条数据进行处理。系统以
OUT_OF_MEMORY错误崩溃,堆内存被耗尽。 - 排查:分析堆 Dump 文件,发现大量
HashMap和数据库结果集相关的对象占满了内存。经查,系统使用了 MyBatis 默认的SimpleExecutor,并且没有启用流式查询。 - 根因:
- 使用默认的
SimpleExecutor执行全表查询,MyBatis 会将所有结果一次性映射为一个List并全部加载到内存中,导致内存溢出。 ReuseExecutor在此场景下会有改善,但所有Statement对象会被缓存,依然会占用可观的资源。- 未使用流式查询或分页。
- 使用默认的
- 解决:
- 核心方案:改用流式查询(ResultHandler)。
ResultHandler是 MyBatis 提供的回调接口,可以逐条处理查询结果,而不用将整个结果集加载到内存。这需要在创建SqlSession时或查询方法中指定。 - 替代方案:使用物理分页,将海量数据分批次查询处理。
- 核心方案:改用流式查询(ResultHandler)。
- 最佳实践:绝不要在大数据量查询场景下使用
selectList等一次性返回全部结果的方法。必须使用ResultHandler或分页插件来避免 OOM。Executor类型的选择对大数据量操作的性能和稳定性至关重要。
12. 面试高频专题
-
MyBatis 的四大对象是什么?各自的职责?
- 标准回答:Executor、StatementHandler、ParameterHandler、ResultSetHandler。Executor 负责调度和执行,管理缓存和事务。StatementHandler 负责创建并配置 JDBC Statement。ParameterHandler 负责将 Java 参数映射到 Statement。ResultSetHandler 负责将 JDBC ResultSet 映射为 Java 对象。
- 多角度追问:
- 追问1:它们之间是如何协作的?请描述一个
selectList的完整调用过程。 - 追问2:StatementHandler 内部又分了哪几种子类型?它们是如何被选择的?(考察策略模式)
- 追问3:如果我要实现一个慢 SQL 告警功能,我应该拦截哪个对象的哪个方法?(
StatementHandler的query/update或在Executor的query/update中)
- 追问1:它们之间是如何协作的?请描述一个
- 加分回答:能够画出四大对象的类关系图,并点出各个对象运用了哪些设计模式。能区分 MyBatis 核心处理层与基础支持层。
-
MyBatis 的 Executor 有哪几种类型?各自的区别和适用场景?在实际项目中如何选择?
- 标准回答:三种:SimpleExecutor(默认,每次新建 Statement)、ReuseExecutor(缓存 Statement,复用)、BatchExecutor(批量处理)。默认
SimpleExecutor适合绝大多数简单操作。ReuseExecutor适合管理资源紧张且重复语句多的事务。BatchExecutor专门用于大规模批量写操作。 - 多角度追问:
- 追问1:在 MyBatis 全局配置和一次
openSession调用中,如何指定使用哪种 Executor? - 追问2:在 Spring 整合环境下,
SqlSessionTemplate的executorType默认是什么?如果是BATCH,需要注意什么?(SqlSessionTemplate是线程安全的,但BatchExecutor有状态) - 追问3:使用
ReuseExecutor时,如果 SQL 语句用到的参数变了,Statement 中的参数会如何更新?(handler.parameterize(stmt)重新设置参数)
- 追问1:在 MyBatis 全局配置和一次
- 加分回答:能够结合
BaseExecutor的模板方法模式和CachingExecutor的装饰者模式,说明他们与Executor的关系。
- 标准回答:三种:SimpleExecutor(默认,每次新建 Statement)、ReuseExecutor(缓存 Statement,复用)、BatchExecutor(批量处理)。默认
-
BaseExecutor 是如何使用模板方法模式的?
- 标准回答:
BaseExecutor的query()方法定义了查询的主流程(缓存检查、查询、缓存写入),而将doQuery、doUpdate等具体执行方法延迟到子类实现。 - 多角度追问:
- 追问1:为什么
queryFromDatabase和doQuery要拆成两个方法? - 追问2:
queryStack字段在模板方法中扮演什么角色?(防止递归查询时的错误缓存清理) - 追问3:这个模板方法和 Spring
AbstractPlatformTransactionManager.getTransaction的模板方法在设计意图上有何不同?
- 追问1:为什么
- 加分回答:能对比 Spring 的模板方法,指出 MyBatis 的模板更偏向单次操作生命周期的管理,而 Spring 的更偏向于复杂策略的协调。
- 标准回答:
-
CachingExecutor 的装饰器模式有什么优势?
- 标准回答:在不改变
SimpleExecutor等原始执行器代码的前提下,动态地为其增加二级缓存功能。这符合开闭原则,对扩展开放,对修改关闭。 - 多角度追问:
- 追问1:
CachingExecutor中的TransactionalCacheManager是用来做什么的? - 追问2:如果我想实现一个日志记录,在每次 SQL 执行前后打印日志,用装饰器模式好还是用插件好?为什么?
- 追问3:装饰器模式和代理模式的区别是什么?
- 追问1:
- 加分回答:能清晰解释
CachingExecutor不仅实现了接口,还持有另一个Executor实例的组合关系,这是装饰器模式区别于普通代理的关键。
- 标准回答:在不改变
-
RoutingStatementHandler 是如何选择具体的 StatementHandler 的?
- 标准回答:在
RoutingStatementHandler的构造函数中,根据MappedStatement对象里配置的statementType属性,通过一个switch语句来创建SimpleStatementHandler、PreparedStatementHandler或CallableStatementHandler。 - 多角度追问:
- 追问1:
statementType在哪里配置? - 追问2:这也是一种策略模式,它和 Executor 体系中用子类实现的策略模式有何区别?
- 追问3:如果 MyBatis 未来要支持一种新的 Statement 类型,应该如何扩展?
- 追问1:
- 加分回答:能指出
RoutingStatementHandler是策略上下文,其构建具体策略的时机是在构造时,是一种比较简单的策略模式应用。
- 标准回答:在
-
一级缓存和二级缓存在 Executor 层面是如何实现的?
- 标准回答:
- 一级缓存:在
BaseExecutor中通过localCache(一个PerpetualCache,本质是 HashMap)实现,SqlSession级别,默认开启。 - 二级缓存:通过
CachingExecutor装饰器实现,MappedStatement级别(命名空间级别),可在多个SqlSession间共享,需要显式配置。
- 一级缓存:在
- 多角度追问:
- 追问1:一级缓存可能导致什么问题?如何解决?
- 追问2:二级缓存的脏数据风险是如何通过
TransactionalCacheManager来控制的?如果事务回滚,暂存的数据会怎样? - 追问3:一个查询是先查一级缓存还是二级缓存?
- 加分回答:能够结合源码说明一级缓存的
queryStack和二级缓存的TransactionalCacheManager这两个精巧设计。 好的,这是您要求的后续面试题目(7-15)的完整内容,严格按照“标准回答 + 多角度追问 + 加分回答”的结构撰写,无任何省略。
- 标准回答:
-
MyBatis 的插件机制是如何设计的?用到了什么设计模式?
- 标准回答:MyBatis 插件机制基于 JDK 动态代理 和 责任链模式 实现。开发者实现
Interceptor接口并添加@Intercepts和@Signature注解指定拦截目标。InterceptorChain在创建四大对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)时,调用pluginAll(target),逐个遍历已注册的Interceptor,每个拦截器通过Plugin.wrap(target, interceptor)判断是否需要拦截,若需要则生成一个动态代理对象。这样多个拦截器就会形成一个层层包裹的代理链,调用时从最外层开始,依次通过每个拦截器的intercept方法,最终到达真实对象。这体现了责任链模式:请求在一条由拦截器组成的链上传递,每个拦截器都可以处理请求或将其传递给链中的下一个。 - 多角度追问:
- 追问1:插件可以对哪四个对象进行拦截?分别能拦截哪些方法?这些方法的签名是从哪里定义的?
- 追问2:如果同一个方法被多个插件拦截,它们的执行顺序是怎样的?如何控制这种顺序?
- 追问3:在插件中调用
invocation.proceed()会触发什么?如果忘记调用proceed()会有什么后果?这背后的原理是什么? - 追问4:MyBatis 的插件拦截和 Spring AOP 的拦截有哪些本质区别?(结合第 13 题思考)
- 加分回答:能结合
Plugin类的源码说明InvocationHandler的实现,指出signatureMap的作用,并解释为何代理的对象必须是接口(JDK 动态代理的限制)。还能画出插件代理链的嵌套结构图,说明pluginAll的包装顺序是“先注册(配置)的先包裹,后注册的后包裹,形成‘洋葱’结构”。
- 标准回答:MyBatis 插件机制基于 JDK 动态代理 和 责任链模式 实现。开发者实现
-
SqlSessionFactory 和 SqlSessionFactoryBuilder 的区别?
- 标准回答:
SqlSessionFactoryBuilder是一个建造者,负责解析 MyBatis 配置文件(XML 或Configuration对象),并构建出SqlSessionFactory实例。它的生命周期短暂,一旦工厂构建完成即可销毁。SqlSessionFactory是一个工厂,负责创建SqlSession实例。它一旦创建就应在应用生命周期内持续存在,通常采用单例模式。 - 多角度追问:
- 追问1:
SqlSessionFactoryBuilder有哪几个重载的build方法?它们分别接受哪些参数? - 追问2:为什么
SqlSessionFactoryBuilder被设计为即时销毁,而SqlSessionFactory要全局唯一? - 追问3:
SqlSessionFactory创建的SqlSession默认ExecutorType是什么?如何在openSession时指定不同的ExecutorType? - 追问4:在 Spring 整合环境中,我们是直接操作
SqlSessionFactory吗?SqlSessionTemplate是如何封装SqlSessionFactory的?
- 追问1:
- 加分回答:能点出
SqlSessionFactoryBuilder内部是通过XMLConfigBuilder解析配置,构建出Configuration对象,然后调用new DefaultSqlSessionFactory(config)。这体现了建造者模式与工厂模式的完美配合:建造者组装复杂部件,工厂则批量生产产品。
- 标准回答:
-
为什么说 SqlSession 是一个门面模式?
- 标准回答:
SqlSession接口对外提供了selectOne、selectList、insert、update、delete、commit、rollback等众多操作数据库的 API,隐藏了 MyBatis 内部复杂的核心处理层(Executor、StatementHandler、ResultSetHandler等)的交互细节。客户端只需调用SqlSession这个门面,无需了解内部的执行器路由、缓存管理、语句处理、参数映射等复杂逻辑,这正是门面模式的典型应用:为子系统中的一组接口提供一个统一的高层接口。 - 多角度追问:
- 追问1:
DefaultSqlSession中的selectOne方法,其内部实现逻辑是怎样的?(点出委托给executor.query) - 追问2:除了
DefaultSqlSession,MyBatis 还提供了哪些SqlSession的增强实现?它们在门面之上又增加了什么功能?(例如SqlSessionManager、Spring 中的SqlSessionTemplate) - 追问3:门面模式和代理模式的区别是什么?
SqlSession是门面,那MapperProxy是代理还是门面?
- 追问1:
- 加分回答:能够通过时序图展示一个
selectList调用如何从SqlSession一路穿透到 JDBC Statement,并说明这种设计将客户端与核心组件解耦,使得核心处理层的任何变化(如无缝替换Executor实现)都不会影响客户端调用。
- 标准回答:
-
一个 Mapper 方法调用最终是如何触发 JDBC Statement 执行的?
- 标准回答:当调用
userMapper.getUserById(1L)时,流程如下:- 该调用会被
MapperProxy(JDK 动态代理)拦截。 MapperProxy根据方法签名查找对应的MappedStatement。- 决定执行类型(
SqlCommandType),调用sqlSession.selectOne。 DefaultSqlSession将请求委托给CachingExecutor.query。CachingExecutor检查二级缓存,未命中则交给BaseExecutor.query。BaseExecutor检查一级缓存,未命中则调用子类SimpleExecutor.doQuery。SimpleExecutor创建RoutingStatementHandler,其内部创建PreparedStatementHandler。- 通过
StatementHandler.prepare创建 JDBCPreparedStatement,再通过ParameterHandler设置参数。 - 调用
PreparedStatement.execute()执行 SQL。 ResultSetHandler处理结果集并返回。
- 该调用会被
- 多角度追问:
- 追问1:
MapperProxy是如何将 Mapper 接口方法与MappedStatement关联起来的?MapperMethod起了什么作用? - 追问2:如果方法参数中有
@Param("id")注解,参数是如何在ParameterHandler中被正确读取到的? - 追问3:当返回结果是
List或单个对象时,执行分支有何不同?selectOne和selectList在DefaultSqlSession层是怎么实现的?
- 追问1:
- 加分回答:能完整画出从代理方法调用到
ResultSetHandler返回结果的序列图,并标出每一层所用的设计模式。同时可提及SqlCommand、MethodSignature等辅助类的职责。
- 标准回答:当调用
-
MyBatis 和 Hibernate/JPA 在架构设计上的本质区别?
- 标准回答:本质上是SQL 中心 vs 对象中心两种 ORM 哲学的体现。
- MyBatis:面向 SQL 的半自动化框架。架构设计的核心是 SQL 执行链路的优化,它不做自动映射,而是将 SQL 的控制权完全交给开发者。其核心组件围绕 SQL 的解析、参数设置、执行和结果集映射展开。
- Hibernate/JPA:面向对象的全自动化框架。架构设计的核心是对象关系映射(ORM),通过 HQL/JPQL 或 Criteria API 自动生成 SQL,屏蔽了底层数据库差异。其核心组件围绕
Session、EntityManager、状态管理(持久化上下文)、一级/二级缓存、脏检查、级联操作等对象生命周期管理展开。
- 多角度追问:
- 追问1:在架构层面,MyBatis 的
SqlSession和 Hibernate 的Session有何异同? - 追问2:MyBatis 的
Executor体系与 Hibernate 的LoadEventListener、PersistEventListener等事件监听器体系相比,在扩展性设计上谁更灵活?为什么? - 追问3:为什么说 MyBatis 更容易进行 SQL 优化,而 Hibernate 在这方面会遇到“抽象泄露”的问题?
- 追问1:在架构层面,MyBatis 的
- 加分回答:能从设计模式角度总结:MyBatis 大量使用门面、策略、模板方法、装饰器来构建一条从接口到底层 JDBC 的通路,而 Hibernate 则更多使用状态模式(对象生命周期)、拦截器、监听器模式来管理对象图谱的变更。
- 标准回答:本质上是SQL 中心 vs 对象中心两种 ORM 哲学的体现。
-
MyBatis 的
BaseExecutor.query与 Spring 的AbstractPlatformTransactionManager.getTransaction都用了模板方法模式,它们的钩子方法设计有何异同?- 标准回答:
- 相同点:两者都在骨架方法中定义了一个固定的操作流程,并将流程中可变的部分抽象为钩子方法留给子类实现。
- 不同点:
- 模板粒度:
BaseExecutor.query的骨架是单次操作生命周期的管理(清缓存、查缓存、查库、存缓存),钩子方法doQuery粒度很粗,直接完成了整个数据库查询。而 Spring 的getTransaction模板是事务传播行为的策略性协调,钩子方法doBegin、doSuspend、doResume等粒度更细,分别对应事务生命周期的不同阶段。 - 扩展目的:MyBatis 的钩子是为了实现不同的 Statement 管理策略(简单、复用、批处理)。Spring 的钩子则为了实现不同事务资源(JDBC、JTA、Hibernate)的适配。
- 异常处理:
BaseExecutor.query的骨架方法内部捕获了部分异常并封装为PersistenceException,而 Spring 的模板则定义了完整的声明式回滚规则,异常处理更复杂。
- 模板粒度:
- 多角度追问:
- 追问1:MyBatis 的
BaseExecutor.update方法也是模板方法,它的骨架与query有何不同? - 追问2:Spring 的
AbstractPlatformTransactionManager中,getTransaction骨架是如何处理“当前已存在事务”这一情况的?这跟策略模式有何关联? - 追问3:如果要在
BaseExecutor.query的骨架中增加一个“执行后发送事件”的功能,在不修改基类的情况下,利用 MyBatis 现有机制可以怎么做?(提示:插件)
- 追问1:MyBatis 的
- 加分回答:能对比出 MyBatis 的模板方法更侧重流程不变性,而 Spring 的模板方法则融入了大量的事务传播策略逻辑,是一个更复杂的“模板+策略”混合体。
- 标准回答:
-
MyBatis 的
InterceptorChain.pluginAll与 Spring AOP 的ReflectiveMethodInvocation.proceed都用了责任链模式,它们的实现方式有何区别?- 标准回答:
- MyBatis(静态代理包装式):在对象创建时,通过
pluginAll一次性将所有Interceptor以 JDK 动态代理的方式层层嵌套包裹在目标对象外部,形成一个静态的代理链。调用时,请求在Plugin.invoke→interceptor.intercept→invocation.proceed()之间传递,是一个代理嵌套的调用过程。链的构建是静态的一次性操作。 - Spring AOP(动态递归调用式):在方法调用时,动态地创建一个
ReflectiveMethodInvocation对象,其内部包含一个拦截器列表和一个索引。通过一个递归的proceed()方法驱动链条:每次调用proceed()都会将索引加一,并调用下一个拦截器的invoke(this)。拦截器必须在其invoke方法中手动调用mi.proceed()来继续驱动链条。链的构建是运行时动态的,更灵活。
- MyBatis(静态代理包装式):在对象创建时,通过
- 多角度追问:
- 追问1:MyBatis 的方式有什么局限性?Spring AOP 的方式又有什么性能或调试上的挑战?
- 追问2:在 MyBatis 中,如果我想根据运行时的 SQL 动态决定是否执行某个插件逻辑,应该怎么做?(可以在
intercept方法内部判断,但无法动态增删链节点,这体现了静态链的限制) - 追问3:从代码阅读的角度,哪种实现更容易理解调用顺序?为什么?
- 加分回答:能画出两种模式的调用栈示意图:MyBatis 是一个“洋葱”,调用栈逐层递进;Spring AOP 是一个“单链表遍历”,随着递归调用“指针”后移。同时能指出 MyBatis 的插件链实现是 “代理包装”模式,而 Spring 是 “拦截器迭代器”模式,虽然都叫责任链,但结构不同。
- 标准回答:
-
SpringManagedTransaction 是如何与 Spring 事务管理器协作的?
- 标准回答:
SpringManagedTransaction是 MyBatis 与 Spring 集成时的事务实现。它通过以下方式融入 Spring 事务生态:- 获取连接:不直接从
DataSource获取连接,而是调用DataSourceUtils.getConnection(dataSource),该方法会从TransactionSynchronizationManager中获取当前线程绑定的、由 Spring 事务管理器管理的数据库连接。 - 提交/回滚:
commit()和rollback()方法内部会先检查连接是否是事务性的(通过DataSourceUtils.isConnectionTransactional判断)。如果是,则什么都不做,将事务提交/回滚的控制权完全交给 Spring 的AbstractPlatformTransactionManager。 - 关闭连接:
close()方法调用DataSourceUtils.releaseConnection(connection, dataSource),由 Spring 决定是释放回连接池还是解除线程绑定,而不是物理关闭连接。
- 获取连接:不直接从
- 多角度追问:
- 追问1:如果在一个没有 Spring 事务的方法中调用 MyBatis,
SpringManagedTransaction的行为是什么?(连接非事务性,autoCommit为true,每次操作自动提交) - 追问2:
TransactionSynchronizationManager是如何将连接与线程绑定的?这保证了什么?(线程安全,同一事务下多个 DAO 操作共享同一连接) - 追问3:如果 MyBatis 和 Spring JDBC 模板混合使用,
SpringManagedTransaction的设计如何保证它们在同一事务中?
- 追问1:如果在一个没有 Spring 事务的方法中调用 MyBatis,
- 加分回答:能深入源码,指出
SpringManagedTransaction持有isConnectionTransactional和autoCommit两个标志位,并解释它们在不同事务传播行为下的状态。并说明这与JdbcTransaction直接管理提交/回滚形成鲜明对比,体现了“控制反转”的思想。
- 标准回答:
-
(系统设计题)设计一个轻量级的 ORM 框架,要求支持 SQL 映射、结果集自动映射、插件拦截和缓存。请参照 MyBatis 的架构,给出核心接口和组件的设计伪代码。
-
标准回答 和加分回答(含伪代码): 好的,我将模拟设计一个名为“TinyORM”的轻量级框架。
核心接口与组件设计:
// 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.pluginAll、Plugin)支持插件拦截。 - 二级缓存可以通过装饰器模式实现:
CachingExecutor implements Executor,内部持有一个delegate。
- 采用 门面模式(
-
多角度追问:
- 追问1:如果要求支持注解 SQL(如
@Select("SELECT ...")),你的框架应该如何扩展? - 追问2:在你的设计中,一级缓存如何做到线程安全?如果需要跨会话的二级缓存,应该如何设计
Cache接口和缓存 Key 的计算逻辑? - 追问3:如何让这个框架与 Spring 无缝集成?需要考虑哪些切入点?(例如提供
TinySessionFactoryBean,实现TransactionSynchronization等)
- 追问1:如果要求支持注解 SQL(如
-
加分回答:能在伪代码中体现
MappedStatement的结构设计(包含SqlSource、ResultMap列表、StatementType),并说明BoundSql和ParameterMapping的作用。同时考虑到集成 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 动态代理 |
延伸阅读
- MyBatis 官方文档:mybatis.org/mybatis-3/
- 《MyBatis 3 源码深度解析》,易百教程等。
- Spring 事务管理核心源码:
AbstractPlatformTransactionManager、TransactionSynchronizationManager。