MyBatis 核心原理详解

MyBatis 核心原理详解

定位:MyBatis 系列第 3 篇------三层架构、配置解析、Mapper 动态代理、执行流程与核心配置对象的源码级解析

适用版本:MyBatis 3.5.x(JDK 8+)


目录

  1. 三层架构
  2. 启动期:配置解析
  3. [Mapper 动态代理](#Mapper 动态代理)
  4. [执行流程:Executor 与四大 Handler](#执行流程:Executor 与四大 Handler)
  5. 核心配置对象
  6. 事务与连接管理
  7. 总结
  8. 常见高频面试题

一、三层架构

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 设计价值

  1. 接口即契约:方法签名与 SQL 定义一一对应,编译期即可约束调用方。
  2. 零模板代码:DAO 层不写实现类,新增方法只需接口声明 + SQL 定义。
  3. 可扩展骨架:代理机制之上可以再叠加插件(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 的职责:

  1. 遍历 ResultSet(多结果集场景循环处理);
  2. 按 ResultMap 逐行映射:自动映射(列名/驼峰)+ 显式映射(resultMap 声明);
  3. 嵌套 resultMap:依据 <id> 列组合值判重合并(02 篇 8.4 节);
  4. 嵌套查询属性:包装为延迟加载代理(02 篇 9.2 节);
  5. 空结果返回空 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 连接的懒加载

JdbcTransactionopenSession并不 立即获取连接,而是在第一条 SQL 真正执行、StatementHandler 调用 transaction.getConnection() 时才从 DataSource 取连接。这意味着:只读逻辑分支、条件短路时不会占用连接。

6.3 Spring 整合模式

Spring 下使用 SpringManagedTransaction:连接与会话生命周期由 Spring 事务同步机制(TransactionSynchronizationManager)托管,同一事务内复用同一连接与 SqlSession。详见 06 篇。


七、总结

  1. 三层架构:接口层(SqlSession/Mapper)面向开发者;核心层(配置解析、SQL 解析执行、缓存、插件)是框架主体;基础层提供数据源、事务、日志。
  2. 启动期:SqlSessionFactoryBuilder 一次性解析配置产出全局唯一 Configuration(持有 MapperRegistry、mappedStatements、TypeHandlerRegistry 等),DefaultSqlSessionFactory 只是 Configuration 的薄包装。
  3. Mapper 代理链:getMapper → MapperProxyFactory → JDK Proxy + MapperProxy → MapperMethod(SqlCommand 定位 statementId,MethodSignature 处理返回类型与参数)→ SqlSession 对应方法。接口无实现类,代理是唯一的「实现」。
  4. 执行链:Executor(缓存调度)→ StatementHandler(创建/参数化 Statement)→ ParameterHandler(TypeHandler 绑定参数)→ ResultSetHandler(ResultMap 组装对象)。四大 Handler 均可被插件拦截。
  5. Executor 四兄弟:Simple(默认)、Reuse(复用 Statement)、Batch(批量)、Caching(二级缓存装饰器)。
  6. 核心对象分工:MappedStatement 是语句的静态定义,SqlSource 决定 SQL 如何产生(Raw/Dynamic/Provider),BoundSql 是每次执行的最终产物,插件改 SQL 即改写 BoundSql。
  7. 连接懒加载: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、一级缓存、事务与连接等会话级可变状态,多线程共享会导致缓存串读与连接竞争,因此只能作为方法局部变量使用。

相关推荐
MZA6661 小时前
实战:基于 XXL-JOB + 策略模式 + Nacos 配置化的 CSV 导出任务系统设计
java·spring boot·mybatis
Shota Kishi9 小时前
Solana Shreds 的 UDP 直投架构:Shredstream UDP Forwarding 的设计与实现
网络协议·架构·udp·区块链·solana
mldong12 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
这个DBA有点耶15 小时前
异构数据集成怎么做?5 种同步方案对比 + 金融级 CDC 实战解析
数据库·oracle·架构
这个DBA有点耶16 小时前
MySQL 8.0.20移除了Block Nested Loop,之前学的JOIN优化知识还适用吗?
数据库·mysql·架构
陆柒14516 小时前
从洋葱模型到 Elpis Core:我对 Node.js 服务端开发的理解
架构
懂软件的胡子个哥16 小时前
微信 API 消息回调怎么设计,才能避免丢消息和重复处理
运维·微信·架构·wechatapi·个人微信号二次开发
co松柏19 小时前
一文吃透 Pi:10w stars 的极简 Agent harness
后端·架构
小聪70819 小时前
elpis-core 抽离 npm 包过程的难点和卡点
前端·架构