概述
前文《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 动态代理和责任链模式,对四大对象进行拦截增强。 - 设计模式:门面、模板方法、策略、装饰器、责任链、工厂、建造者模式的集中体现。
文章组织架构图
- 总览说明:全文 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 处理结果集 → 返回最终结果。
下面这张类关系图,清晰地展示了各核心接口与类之间的结构关系。
- 图表主旨概括 :本类图展示了 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。
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中的selectList、selectOne、update、insert、delete等方法,其内部逻辑惊人地一致:获取MappedStatement,然后调用executor的对应方法。- 它本身不包含任何数据库访问逻辑,纯粹是一个门面 和委托器 。它将 MyBatis 核心处理层(
Executor、StatementHandler等)这个复杂的子系统,简化为一个易于使用的接口。 - 这种设计使得核心处理层的任何内部变化(如更换
Executor实现),都不会影响到调用方。
以下序列图展示了 DefaultSqlSession 作为门面的委托过程。
- 图表主旨概括 :本序列图旨在展现
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。
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对象管理事务。commit和rollback方法直接调用java.sql.Connection的对应方法。它适用于独立运行、不依赖外部事务管理器的 MyBatis 应用程序。ManagedTransaction:它的commit和rollback方法是空的!它不主动管理事务,而是将事务管理权交给外部容器(如 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负责实际的数据库查询。doQuery、doUpdate等抽象方法就是钩子方法 ,将具体执行逻辑的实现延迟到SimpleExecutor、ReuseExecutor、BatchExecutor等子类中。queryStack是一个防止递归查询清缓存的精巧设计。当 MyBatis 进行关联查询(如嵌套结果映射)时,可能会递归调用query方法。queryStack > 0时不会清空缓存,保证了缓存行为在递归场景下的正确性。
下面这张序列图生动展示了 BaseExecutor 的模板方法骨架和一级缓存的工作流程。
- 图表主旨概括 :本序列图展示了
BaseExecutor.query()方法的完整执行流程,清晰地描绘了模板方法模式和一级缓存的交互逻辑。 - 逐层/逐元素分解 :
BaseExecutor(模板类):负责流程控制,定义了查询的主流程:缓存检查、缓存写和清空逻辑。SimpleExecutor等子类(具体类) :负责实现doQuery钩子方法,封装了与 JDBC 交互的细节。
- 设计原理映射 :模板方法模式 。
BaseExecutor.query()是模板,doQuery()/doUpdate()是钩子。 - 工程联系与关键结论 :一级缓存是
BaseExecutor级别的,意味着同一个SqlSession内共享。这是 MyBatis 隐形数据行为最常见的来源,理解其工作机制对于避免"脏读"至关重要。
4.2 Executor 的三兄弟:策略模式的体现
BaseExecutor 的三个子类代表了三种不同的 Statement 管理策略,这是策略模式的典型应用。
-
SimpleExecutor(简单执行器) 这是 MyBatis 的默认执行器。它的策略是:每次执行update或query,都会创建一个新的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,对数据库造成压力,性能较低。 -
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。 -
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; // 返回一个不可知的值 } // ... }关键点 :调用
BatchExecutor的doUpdate后,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 的装饰过程。
- 图表主旨概括 :本序列图揭示了
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提供了统一的接口。
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 类型是 Statement、PreparedStatement 还是 CallableStatement。
以下序列图演示了这个路由过程。
- 图表主旨概括 :本序列图演示了
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的指定位置(例如作为VARCHAR或TIMESTAMP)。TypeHandler体系是 MyBatis 类型转换的核心,其扩展机制将在后续篇章详述。
7. ResultSetHandler 与结果集映射
JDBC 执行完成后,返回一个 ResultSet。ResultSetHandler 的职责就是将这个 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列表,调用applyPropertyMappings从ResultSet中获取列值并设置到对象属性中。每一步都离不开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)。这个过程形成了一个嵌套的代理链,这正是责任链模式的体现:
- 原始对象
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)
下面的序列图清晰地展示了这个层层代理的拦截过程。
- 图表主旨概括:本序列图描绘了 MyBatis 插件拦截链的责任链模式实现。它展示了当有多个拦截器时,方法调用是如何从最外层的代理对象层层传递,最终到达真实目标对象的。
- 逐层/逐元素分解 :
- 链的构建 :由
InterceptorChain.pluginAll()静态构建,形成new ProxyB(new ProxyA(realObj))的结构。 - 链的调用 :通过
Invocation.proceed()方法,每个拦截器可以决定是否将调用传递给链中的下一个处理器,这提供了增强或终止链的能力。
- 链的构建 :由
- 设计原理映射 :责任链模式 和JDK动态代理 。目标对象在链尾,多个
Interceptor以代理的形式串联在一起。 - 工程联系与关键结论 :插件的执行顺序与
plugins配置文件中的声明顺序一致。理解pluginAll的层层包装机制,是避免插件间相互影响和正确开发自定义插件的前提。
9. 一个 SQL 请求的完整执行时序
作为对前几个模块的总结,让我们用一个精练但完整的序列图,串联起一个 selectList 请求穿越 MyBatis 四层架构的全过程。
创建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可以在多个节点(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"的轻量级框架。
核心接口与组件设计:
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.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。