MyBatis 核心原理详解
定位:MyBatis 系列第 3 篇------三层架构、配置解析、Mapper 动态代理、执行流程与核心配置对象的源码级解析
适用版本:MyBatis 3.5.x(JDK 8+)
目录
- 三层架构
- 启动期:配置解析
- [Mapper 动态代理](#Mapper 动态代理)
- [执行流程:Executor 与四大 Handler](#执行流程:Executor 与四大 Handler)
- 核心配置对象
- 事务与连接管理
- 总结
- 常见高频面试题
一、三层架构
1.1 架构全景
┌─────────────────────────────────────────────────────────────┐
│ 接口层(API):SqlSession / SqlSessionFactory / Mapper接口 │
├─────────────────────────────────────────────────────────────┤
│ 核心层: │
│ 配置解析 XMLConfigBuilder / XMLMapperBuilder │
│ SQL解析 SqlSource → BoundSql │
│ SQL执行 Executor → StatementHandler │
│ → ParameterHandler → ResultSetHandler │
│ 缓存机制 一级缓存 / 二级缓存(04篇) │
│ 插件机制 Interceptor(05篇) │
├─────────────────────────────────────────────────────────────┤
│ 基础层:数据源(UNPOOLED/POOLED/JNDI) / 事务(JDBC/MANAGED) │
│ 日志(SLF4J/Log4j2/JDK Logging) / 反射工具 │
└─────────────────────────────────────────────────────────────┘
各层职责边界:
| 层 | 职责 | 关键类 |
|---|---|---|
| 接口层 | 面向开发者的 API,屏蔽内部复杂性 | SqlSession、SqlSessionFactory、Mapper 接口 |
| 核心层 | 配置解析、SQL 解析与执行、映射、缓存、插件 | Configuration、Executor、四大 Handler |
| 基础层 | 数据源、事务、日志等基础能力 | DataSource、Transaction、Log |
1.2 核心类协作关系
SqlSessionFactoryBuilder ──build──→ DefaultSqlSessionFactory
│ 持有
▼
Configuration(全局唯一)
├── MapperRegistry ── MapperProxyFactory
├── mappedStatements ── MappedStatement ── SqlSource
├── TypeHandlerRegistry
└── caches / keyGenerators / resultMaps
DefaultSqlSessionFactory ──openSession──→ DefaultSqlSession
│ 持有 Executor
▼
Simple/Reuse/Batch Executor(或被 CachingExecutor 装饰)
│ 委托
▼
RoutingStatementHandler
→ PreparedStatementHandler
├── ParameterHandler(DefaultParameterHandler)
└── ResultSetHandler(DefaultResultSetHandler)
二、启动期:配置解析
2.1 解析入口
java
// SqlSessionFactoryBuilder.build 的核心逻辑(简化)
public SqlSessionFactory build(InputStream inputStream) {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream);
return build(parser.parse()); // parse() 返回 Configuration
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
SqlSessionFactoryBuilder 是一次性对象:构建完 Factory 后即可丢弃,因为它不持有任何运行期状态。
2.2 Configuration:全局配置中枢
Configuration 是 MyBatis 的「大脑」,启动期解析的所有产物都注册在其中:
| 成员 | 类型 | 职责 |
|---|---|---|
mapperRegistry |
MapperRegistry | Mapper 接口注册表:knownMappers(Map<Class, MapperProxyFactory>) |
typeHandlerRegistry |
TypeHandlerRegistry | Java 类型 + JDBC 类型 → TypeHandler 映射 |
typeAliasRegistry |
TypeAliasRegistry | 别名 → Class |
mappedStatements |
Map<String, MappedStatement> | statementId → 语句定义(StrictMap,重复定义即报错) |
caches |
Map<String, Cache> | namespace → 二级缓存实例 |
keyGenerators |
Map<String, KeyGenerator> | 主键生成器注册表 |
resultMaps |
Map<String, ResultMap> | resultMap 注册表 |
| settings | 各字段 | cacheEnabled、lazyLoadingEnabled 等全局开关 |
2.3 Mapper 注册的两条路径
XML 路径 :<mappers> 声明 → XMLMapperBuilder 解析 → 每个 <select>/<insert> 构建 MappedStatement 放入 mappedStatements → 若存在同名接口则同时注册到 MapperRegistry。
注解路径 :MapperRegistry.addMapper(Class) → MapperAnnotationBuilder.parse() 解析方法上的 @Select 等注解生成 MappedStatement。
冲突检测:同一 statementId(namespace.id)重复定义时,StrictMap 直接抛出异常------这就是「XML 与注解不能同时定义同一方法」报错的源头。
三、Mapper 动态代理
3.1 代理的创建
getMapper 不产生任何实现类字节码之外的东西------它只是向 JDK Proxy 要一个代理:
java
// DefaultSqlSession
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
// MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> factory = knownMappers.get(type);
return factory.newInstance(sqlSession);
}
// MapperProxyFactory(简化)
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy);
}
注册前提:接口必须在启动期进入 knownMappers(XML <mapper class>/<package>、注解注册或 Spring 的 @MapperScan),否则 getMapper 抛 BindingException。
3.2 MapperProxy:调用分发
java
// MapperProxy.invoke(简化)
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. Object 原生方法直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. JDK 8+ default 方法
if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
// 3. 普通方法:经 MapperMethod 执行(带缓存)
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
methodCache(ConcurrentHashMap)缓存 Method → MapperMethod 的映射,反射解析只做一次。
3.3 MapperMethod:方法签名到 SQL 调用的翻译器
MapperMethod 由两部分组成:
| 组成 | 解析内容 |
|---|---|
SqlCommand |
statementId(接口全限定名.方法名)、SQLCommandType(SELECT/INSERT/UPDATE/DELETE) |
MethodSignature |
返回类型(是否集合/Map/Cursor/Optional)、RowBounds 参数位置、ResultHandler 参数位置、参数名解析器 |
执行逻辑(简化):
java
public Object execute(SqlSession sqlSession, Object[] args) {
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
return rowCountResult(sqlSession.insert(command.getName(), param));
}
case UPDATE: { /* 同理 sqlSession.update */ }
case DELETE: { /* 同理 sqlSession.delete */ }
case SELECT:
if (method.returnsVoid()) { /* 配合 ResultHandler 流式处理 */ }
if (method.returnsMany()) return executeForMany(sqlSession, args);
if (method.returnsMap()) return executeForMap(sqlSession, args);
if (method.returnsCursor()) return executeForCursor(sqlSession, args);
Object param = method.convertArgsToSqlCommandParam(args);
return sqlSession.selectOne(command.getName(), param);
default: throw new BindingException("Unknown execution method");
}
}
参数封装 :ParamNameResolver.getNamedParams 处理多参数------无 @Param 时生成 arg0/arg1 + param1/param2 双键 Map;有 @Param 时追加命名键。这就是 01 篇「多参数默认命名」规则的来源。
3.4 设计价值
- 接口即契约:方法签名与 SQL 定义一一对应,编译期即可约束调用方。
- 零模板代码:DAO 层不写实现类,新增方法只需接口声明 + SQL 定义。
- 可扩展骨架:代理机制之上可以再叠加插件(05 篇拦截 Executor 层)与 Spring 托管(06 篇 MapperFactoryBean)。
四、执行流程:Executor 与四大 Handler
4.1 Executor 四种实现
| 实现 | 行为 | 实现要点 |
|---|---|---|
SimpleExecutor |
每次执行新建 Statement,执行完即关闭 | 默认行为,无状态 |
ReuseExecutor |
会话内复用相同 SQL 文本的 Statement | statementMap(SQL→Statement)缓存,hasStatementFor 判断 |
BatchExecutor |
语句 addBatch 累积,flushStatements 统一执行 | 缓存上一条 Statement,SQL 变化则先 flush 再换 |
CachingExecutor |
装饰器,查询先过二级缓存 | cacheEnabled=true 时包裹上述三者 |
创建时机:openSession 时按 defaultExecutorType(或方法参数)创建基础 Executor,若 cacheEnabled=true 则再包一层 CachingExecutor。
4.2 一次 SELECT 的完整调用链
java
// 1. 入口:SqlSession.selectList
List<E> result = sqlSession.selectList(statementId, parameter);
// 2. CachingExecutor.query:二级缓存检查(04篇展开)
// 未命中 → 委托被装饰的 Executor(如 SimpleExecutor)
// 3. BaseExecutor.query:一级缓存检查(04篇展开)
// 未命中 → queryFromDatabase → doQuery
// 4. SimpleExecutor.doQuery
Statement stmt = null;
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 路由 StatementHandler:Simple/Reuse/Batch 对应不同实现
StatementHandler handler = configuration.newStatementHandler(this, ms, ...);
stmt = prepareStatement(handler, ms.getStatementLog());
// └── handler.prepare(connection):创建 PreparedStatement
// └── handler.parameterize(stmt):参数绑定
return handler.query(stmt, resultHandler);
// └── stmt.execute()
// └── ResultSetHandler.handleResultSets(stmt)
} finally {
closeStatement(stmt); // SimpleExecutor 用完即关
}
4.3 ParameterHandler:参数绑定
DefaultParameterHandler.setParameters 的核心循环:
java
// 简化自 DefaultParameterHandler.setParameters
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (ParameterMapping pm : parameterMappings) {
String propertyName = pm.getProperty();
Object value = resolveValue(parameterObject, propertyName); // MetaObject 反射取值
TypeHandler th = pm.getTypeHandler();
th.setParameter(ps, i, value, pm.getJdbcType()); // 最终 setXXX
i++;
}
每个 ? 对应一条 ParameterMapping(属性名 + jdbcType + typeHandler),值经 TypeHandler 完成 Java → JDBC 转换------与 02 篇 TypeHandler 章节形成闭环。
4.4 ResultSetHandler:结果组装
DefaultResultSetHandler.handleResultSets 的职责:
- 遍历 ResultSet(多结果集场景循环处理);
- 按 ResultMap 逐行映射:自动映射(列名/驼峰)+ 显式映射(resultMap 声明);
- 嵌套 resultMap:依据
<id>列组合值判重合并(02 篇 8.4 节); - 嵌套查询属性:包装为延迟加载代理(02 篇 9.2 节);
- 空结果返回空 List 而非 null。
4.5 UPDATE 流程的差异
- 无 ResultSetHandler 参与,返回
statement.getUpdateCount(); - KeyGenerator 介入 :
Jdbc3KeyGenerator(useGeneratedKeys,执行后取生成的键回填)、SelectKeyGenerator(BEFORE 先执行取键、AFTER 后执行回填); - 执行后清空一级缓存(04 篇)。
4.6 StatementHandler 的路由
RoutingStatementHandler 按 Executor 类型路由:
| Executor | StatementHandler |
|---|---|
| SimpleExecutor | SimpleStatementHandler / PreparedStatementHandler |
| ReuseExecutor | ReuseStatementHandler |
| BatchExecutor | BatchStatementHandler |
绝大多数场景是 PreparedStatementHandler(预编译语句);SimpleStatementHandler 用于无参 Statement;存储过程走 CallableStatementHandler。
五、核心配置对象
5.1 MappedStatement:一条 SQL 的完整定义
| 字段 | 说明 |
|---|---|
id |
namespace.方法名,全局唯一定位键 |
sqlCommandType |
SELECT / INSERT / UPDATE / DELETE / FLUSH |
sqlSource |
SQL 来源(见 5.2) |
resultMaps / resultType |
结果映射声明 |
cache |
绑定的二级缓存(namespace 级,04 篇) |
keyGenerator |
NoKeyGenerator / Jdbc3KeyGenerator / SelectKeyGenerator |
timeout / fetchSize |
语句级超时与抓取大小 |
flushCache / useCache |
执行前是否清缓存 / 是否使用二级缓存 |
5.2 SqlSource 体系
| 实现 | 触发条件 | 运行期行为 |
|---|---|---|
RawSqlSource |
无动态标签 | 启动期即完成 #{} → ? 解析,BoundSql 可复用,性能最优 |
DynamicSqlSource |
含 <if> 等动态标签 |
每次执行遍历 SqlNode 树重新拼接(02 篇原理) |
ProviderSqlSource |
注解 Provider 方式 | 每次调用 Provider 方法构建 SQL |
优化启示:能用静态 SQL 就不用动态标签------动态标签的代价是每次执行都要 OGNL 求值与字符串拼接(虽然通常不是瓶颈,但超高频语句值得注意)。
5.3 BoundSql:运行期产物
java
public class BoundSql {
private final String sql; // 最终 SQL(#{} 已替换为 ?)
private final List<ParameterMapping> parameterMappings; // 参数位置→属性映射
private final Object parameterObject; // 本次调用入参
private final Map<String, Object> additionalParameters; // bind/foreach 产生的附加参数
}
插件改写 SQL 的本质就是拿到 BoundSql 后反射替换其 sql 字段(分页插件、多租户插件的工作方式,见 05 篇)。
5.4 对象生命周期总结
| 对象 | 生命周期 |
|---|---|
| Configuration / MappedStatement / SqlSource | 启动期创建,全程复用,不可变 |
| SqlSessionFactory | 应用级单例 |
| SqlSession / Executor | 会话级,用完即关 |
| BoundSql | 每次执行生成(RawSqlSource 内部可缓存) |
| Statement / PreparedStatement | 语句级(Batch/Reuse 例外) |
六、事务与连接管理
6.1 原生模式
java
// 手动事务(默认)
try (SqlSession session = factory.openSession()) {
// ... 写操作
session.commit(); // 或 rollback
}
// 自动提交
try (SqlSession session = factory.openSession(true)) { ... }
6.2 连接的懒加载
JdbcTransaction 在 openSession 时并不 立即获取连接,而是在第一条 SQL 真正执行、StatementHandler 调用 transaction.getConnection() 时才从 DataSource 取连接。这意味着:只读逻辑分支、条件短路时不会占用连接。
6.3 Spring 整合模式
Spring 下使用 SpringManagedTransaction:连接与会话生命周期由 Spring 事务同步机制(TransactionSynchronizationManager)托管,同一事务内复用同一连接与 SqlSession。详见 06 篇。
七、总结
- 三层架构:接口层(SqlSession/Mapper)面向开发者;核心层(配置解析、SQL 解析执行、缓存、插件)是框架主体;基础层提供数据源、事务、日志。
- 启动期:SqlSessionFactoryBuilder 一次性解析配置产出全局唯一 Configuration(持有 MapperRegistry、mappedStatements、TypeHandlerRegistry 等),DefaultSqlSessionFactory 只是 Configuration 的薄包装。
- Mapper 代理链:getMapper → MapperProxyFactory → JDK Proxy + MapperProxy → MapperMethod(SqlCommand 定位 statementId,MethodSignature 处理返回类型与参数)→ SqlSession 对应方法。接口无实现类,代理是唯一的「实现」。
- 执行链:Executor(缓存调度)→ StatementHandler(创建/参数化 Statement)→ ParameterHandler(TypeHandler 绑定参数)→ ResultSetHandler(ResultMap 组装对象)。四大 Handler 均可被插件拦截。
- Executor 四兄弟:Simple(默认)、Reuse(复用 Statement)、Batch(批量)、Caching(二级缓存装饰器)。
- 核心对象分工:MappedStatement 是语句的静态定义,SqlSource 决定 SQL 如何产生(Raw/Dynamic/Provider),BoundSql 是每次执行的最终产物,插件改 SQL 即改写 BoundSql。
- 连接懒加载:openSession 不占连接,首条 SQL 执行时才获取;Spring 下由事务同步托管。
八、常见高频面试题
1. MyBatis 的 Mapper 接口没有实现类,为什么能直接注入使用?
要点:启动期接口注册进 MapperRegistry(对应 MapperProxyFactory);getMapper 时用 JDK 动态代理生成实例,InvocationHandler 是 MapperProxy。代理按「接口全限定名.方法名」定位 MappedStatement,经 MapperMethod 处理参数(ParamNameResolver 封装 ParamMap)与返回类型后转调 SqlSession 执行。
2. 描述一条 SELECT 在 MyBatis 中的完整执行流程?
要点:SqlSession → MapperProxy → MappedStatement → Executor(CachingExecutor 查二级缓存 → BaseExecutor 查一级缓存)→ StatementHandler.prepare 创建 PreparedStatement → ParameterHandler 用 TypeHandler 绑定参数 → execute → ResultSetHandler 按 ResultMap 组装对象 → 写入缓存返回。
3. MyBatis 的 Executor 有哪些?各自的区别?
要点:SimpleExecutor 每次新建 Statement(默认);ReuseExecutor 会话内按 SQL 文本复用 Statement;BatchExecutor 批量累积、flushStatements 统一执行(配合 ExecutorType.BATCH);CachingExecutor 是装饰器,承载二级缓存,cacheEnabled 时包裹前三者。
4. BoundSql 是什么?SqlSource 有哪些实现?
要点:BoundSql 是运行期产物,含最终 SQL 文本(#{} 已替换为 ?)、parameterMappings、parameterObject 与附加参数。SqlSource 三实现:RawSqlSource(无动态标签,启动期解析完,可复用)、DynamicSqlSource(含动态标签,每次执行 OGNL 求值拼接)、ProviderSqlSource(注解 Provider)。
5. MyBatis 的一级缓存、二级缓存分别在哪个组件实现?
要点:一级缓存在 BaseExecutor(PerpetualCache,SqlSession 级);二级缓存在 CachingExecutor + MappedStatement 绑定的 Cache(namespace 级,TransactionalCache 装饰)。详见 04 篇。
6. MyBatis 分页 RowBounds 是物理分页吗?
要点:不是,是内存逻辑分页------先取全部结果再截取,大结果集有 OOM 风险。物理分页应使用分页插件改写 SQL(05 篇)或手写 LIMIT。
7. MyBatis 是什么时候获取数据库连接的?
要点:懒加载。openSession 时不获取连接,第一条 SQL 执行、StatementHandler 需要 Connection 时才通过 Transaction 从 DataSource 获取;这使短路分支不占用连接。
8. 为什么说 SqlSessionFactory 是线程安全的而 SqlSession 不是?
要点:SqlSessionFactory 本质是持有不可变 Configuration 的薄包装,无会话状态;SqlSession 持有 Executor、一级缓存、事务与连接等会话级可变状态,多线程共享会导致缓存串读与连接竞争,因此只能作为方法局部变量使用。
