MyBatis 的内部结构并不复杂,但很容易被看散:SqlSessionMapperProxyMappedStatementExecutorStatementHandlerTypeHandler、一级缓存、二级缓存、插件链分别出现在不同包里,单看任何一个类都像是局部技巧。把它们放回完整调用链之后,MyBatis 的设计会变得很清楚:

MyBatis 的核心不是“把对象自动映射成表”,而是“把 Java 方法稳定地映射到一条可执行的 SQL 描述,再把执行过程拆成可替换的流水线组件”。

本文以 MyBatis 3.5.19 的官方文档与源码 XRef 为基准,重点讨论 MyBatis 运行时内部架构,不展开 MyBatis-Spring 的事务同步、Spring Boot 自动装配,也不讨论 MyBatis-Plus 等增强框架。

总体架构

MyBatis 可以分成两条主线:构建期主线和执行期主线。

构建期负责把 XML、注解、类型别名、类型处理器、插件、缓存配置等材料编译成一个 Configuration 对象。执行期从 SqlSessionFactory 打开 SqlSession,再通过 MapperProxySqlSession API 找到 MappedStatement,一路委托到 ExecutorStatementHandlerParameterHandlerResultSetHandler 和 JDBC。

MyBatis 构建期与执行期总览

这个图里最重要的是 Configuration。它不是普通配置类,而是 MyBatis 的运行时元数据仓库。MappedStatementResultMapCacheInterceptorTypeHandlerObjectFactoryLanguageDriverMapperRegistry 都挂在这里。一次查询真正运行时,MyBatis 不是重新解析 XML,而是围绕 Configuration 里已经建好的对象图做派发。

[PATTERN] 元数据对象图模式:框架启动时把外部描述语言编译成内存对象图,运行时只做查表、组装和委托。MyBatis 的 XML 不是运行时脚本,而是构建 MappedStatementSqlSourceResultMap 等对象的输入材料。

三个入口对象

官方文档把 MyBatis 的核心入口压缩成三个对象:SqlSessionFactoryBuilderSqlSessionFactorySqlSession。这三个对象刚好对应三个生命周期。

对象 生命周期 线程安全 职责
SqlSessionFactoryBuilder 构建期临时对象 不应复用为服务对象 读取配置,构造 SqlSessionFactory
SqlSessionFactory 应用级单例 可长期复用 持有 Configuration,创建 SqlSession
SqlSession 请求/事务级短生命周期对象 不可跨线程共享 持有 Executor,执行 CRUD 与提交回滚

SqlSessionFactoryBuilder 的存在感很弱,但它划出了一条边界:解析配置不是每次 SQL 执行都做的事。构建完成后,DefaultSqlSessionFactory 内部持有同一个 Configuration。每次 openSession(),工厂从 Environment 中取出 DataSourceTransactionFactory,创建 Transaction,再由 Configuration.newExecutor() 创建执行器,最后组装成 DefaultSqlSession

sequenceDiagram
    participant App as App
    participant SFB as SqlSessionFactoryBuilder
    participant XCB as XMLConfigBuilder
    participant CFG as Configuration
    participant SSF as DefaultSqlSessionFactory
    participant TXF as TransactionFactory
    participant EX as Executor
    participant SS as DefaultSqlSession

    App->>SFB: build(inputStream)
    SFB->>XCB: parse()
    XCB->>CFG: 填充环境、mapper、插件、类型处理器
    SFB-->>App: SqlSessionFactory
    App->>SSF: openSession()
    SSF->>TXF: newTransaction(dataSource, ...)
    SSF->>CFG: newExecutor(transaction, executorType)
    CFG-->>SSF: Executor
    SSF-->>App: DefaultSqlSession(Configuration, Executor)

这个创建过程解释了一个常见误解:SqlSession 不是轻量的“数据库连接包装器”。它确实通过 Transaction 间接拿连接,但它更重要的身份是一次 MyBatis 执行上下文,里面有本次会话的 Executor、一级缓存、脏数据标记和提交回滚边界。

DefaultSqlSessionFactory.openSessionFromDataSource(...) 的源码骨架可以压缩成四步:

1
2
3
4
5
Environment environment = configuration.getEnvironment();
TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);

这几行保留了旧文里最关键的观察:MyBatis 创建 SqlSession 时不是直接 new 一个 JDBC 连接,而是沿着 Environment -> TransactionFactory -> Transaction -> Executor -> DefaultSqlSession 逐层抽象。Environment 解决数据源和事务工厂从哪里来,TransactionFactory 解决连接事务由谁管理,Executor 解决 SQL 用什么策略执行,DefaultSqlSession 才是暴露给应用的会话门面。

Configuration 是总装配台

Configuration 的职责密度很高,几乎所有核心组件都能从这里追到。它至少承担四类职责。

第一类是注册表。typeAliasRegistry 处理别名,typeHandlerRegistry 处理 Java 类型与 JDBC 类型转换,mapperRegistry 处理 Mapper 接口与代理工厂,languageRegistry 处理脚本语言驱动。

第二类是语句仓库。mappedStatements 保存每个 SQL 语句的最终描述,key 通常是 namespace.id。Mapper 接口方法能够定位 SQL,本质上靠的就是这个 id 约定:接口全限定名作为 namespace,方法名作为 statement id。

第三类是对象工厂。Configuration 负责创建 ExecutorStatementHandlerParameterHandlerResultSetHandler,并在这些对象创建之后调用 interceptorChain.pluginAll()。插件之所以能拦截执行器、语句处理器、参数处理器和结果集处理器,是因为这些对象都经过 Configuration 的统一创建点。

第四类是全局行为开关。比如 cacheEnabledlazyLoadingEnabledmapUnderscoreToCamelCasedefaultExecutorTypelocalCacheScope 等配置并不散落在各处,而是在运行时被组件按需读取。

flowchart LR
    CFG["Configuration"]
    CFG --> Registry["注册表:TypeAlias / TypeHandler / Mapper / LanguageDriver"]
    CFG --> Statements["语句仓库:MappedStatement / ResultMap / Cache"]
    CFG --> Factories["组件工厂:Executor / StatementHandler / ParameterHandler / ResultSetHandler"]
    CFG --> Settings["行为开关:cache / lazyLoading / executorType / localCacheScope"]
    Factories --> Plugins["InterceptorChain.pluginAll(...)"]

[PATTERN] 总装配台模式:一个框架级配置对象既保存元数据,也提供核心组件的统一创建点。统一创建点让插件、默认策略、全局开关有了稳定注入位置。

Mapper XML 如何变成 MappedStatement

Mapper XML 的解析不是简单地把 SQL 字符串放进 Map。MyBatis 会把一条 <select><insert><update><delete> 编译成 MappedStatement,它是执行期最核心的语句描述对象。

一个 MappedStatement 通常包含这些信息:

字段/关联对象 作用
id 语句唯一标识,通常是 namespace.methodName
sqlSource SQL 来源,负责根据参数生成 BoundSql
statementType STATEMENTPREPAREDCALLABLE
sqlCommandType SELECTINSERTUPDATEDELETE
parameterMap 参数映射描述,现代用法里更多由 ParameterMapping 承担
resultMaps 结果集到对象的映射规则
cache namespace 级二级缓存引用
flushCacheRequired / useCache 缓存刷新和读取策略
keyGenerator 主键回填策略

Mapper XML 解析大致经过几层:

flowchart TD
    MapperXML["Mapper XML"] --> XMLMapperBuilder["XMLMapperBuilder"]
    XMLMapperBuilder --> Namespace["namespace 绑定"]
    XMLMapperBuilder --> ResultMap["解析 resultMap"]
    XMLMapperBuilder --> Cache["解析 cache / cache-ref"]
    XMLMapperBuilder --> Statements["解析 statement 节点"]
    Statements --> XMLStatementBuilder["XMLStatementBuilder"]
    XMLStatementBuilder --> LanguageDriver["LanguageDriver"]
    LanguageDriver --> SqlSource["SqlSource"]
    XMLStatementBuilder --> MappedStatement["MappedStatement"]
    MappedStatement --> Configuration["Configuration.mappedStatements"]

SqlSource 是这里的关键。MyBatis 并不把 XML 中的 SQL 直接当字符串执行,而是把它包装成能在运行时产出 BoundSql 的对象。

静态 SQL 会尽量提前解析。动态 SQL 则会被解析成 SqlNode 树,比如 IfSqlNodeForEachSqlNodeChooseSqlNodeTrimSqlNodeMixedSqlNode。运行时传入参数后,DynamicSqlSource 通过这棵树生成最终 SQL,再产生 BoundSql

flowchart LR
    XMLScript["XML SQL 节点"] --> XMLScriptBuilder["XMLScriptBuilder"]
    XMLScriptBuilder --> StaticText["StaticTextSqlNode"]
    XMLScriptBuilder --> If["IfSqlNode"]
    XMLScriptBuilder --> ForEach["ForEachSqlNode"]
    XMLScriptBuilder --> Choose["ChooseSqlNode"]
    XMLScriptBuilder --> Trim["TrimSqlNode"]
    StaticText --> Mixed["MixedSqlNode"]
    If --> Mixed
    ForEach --> Mixed
    Choose --> Mixed
    Trim --> Mixed
    Mixed --> DynamicSqlSource["DynamicSqlSource"]
    DynamicSqlSource --> BoundSql["BoundSql: SQL + ParameterMapping + additionalParameters"]

BoundSql 是 SQL 执行前的最后形态:它包含最终 SQL 字符串、参数映射列表、原始参数对象,以及动态 SQL 产生的额外参数。#{} 会变成 ?ParameterMapping${} 会直接拼进 SQL 字符串。前者走 JDBC 参数绑定,后者是字符串替换,必须只用于白名单字段名、排序方向等无法参数化的位置。

Mapper 接口为什么能执行 SQL

Mapper 接口本身没有实现类。sqlSession.getMapper(UserMapper.class) 返回的是 JDK 动态代理,代理背后是 MapperProxy

调用链可以写成这样:

1
2
3
4
5
UserMapper.selectById(1)
-> MapperProxy.invoke(...)
-> MapperMethod.execute(sqlSession, args)
-> SqlSession.selectOne / selectList / insert / update / delete
-> Executor.query / update

MapperProxy 只负责代理分发,不真正理解 SQL。真正把“Java 方法”翻译成“MyBatis 命令”的是 MapperMethod。它内部有两个重要对象:

内部对象 职责
SqlCommand 根据 Mapper 接口和方法名解析 MappedStatement id,并识别 SQL 命令类型
MethodSignature 解析方法返回值、参数名、RowBoundsResultHandler、是否返回集合/Map/Optional 等

这就是为什么 Mapper 方法签名可以看起来很自然:返回 List<User> 时调用 selectList,返回单个对象时调用 selectOne,返回 Map 时走 selectMap,增删改则返回影响行数或布尔值。方法签名层面的差异,被 MapperMethod 翻译成了对 SqlSession API 的不同调用。

sequenceDiagram
    participant App as 业务代码
    participant Proxy as MapperProxy
    participant Method as MapperMethod
    participant Session as DefaultSqlSession
    participant Config as Configuration
    participant Executor as Executor

    App->>Proxy: mapper.selectById(id)
    Proxy->>Method: execute(sqlSession, args)
    Method->>Config: 查找 namespace.method 的 MappedStatement
    Method->>Session: selectOne(statementId, param)
    Session->>Executor: query(mappedStatement, param, ...)

[PATTERN] 接口代理到命令对象模式:Mapper 接口负责提供类型安全的 Java 入口,MapperProxy 负责拦截,MapperMethod 负责把方法语义翻译成框架内部命令。接口没有实现类,但方法调用并不是“魔法”,只是被稳定地映射到 MappedStatement

Executor 是执行主干

Executor 是 SQL 执行链的主干。SqlSession 面向用户,StatementHandler 面向 JDBC,中间调度、缓存、事务、延迟加载、批处理都集中在 Executor

MyBatis 内置三种基础执行器:

执行器 适用场景 核心行为
SimpleExecutor 默认场景 每次执行创建新的 Statement
ReuseExecutor 同一 SQL 重复执行 复用相同 SQL 对应的 Statement
BatchExecutor 批量写入 暂存多个更新,等待 flushStatements()

Configuration.newExecutor() 会根据 ExecutorType 选择基础执行器。如果全局 cacheEnabled=true,还会用 CachingExecutor 包一层。最后再套插件代理。

对应的源码骨架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;

if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}

if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);

这里有两个容易漏掉的细节。第一,ExecutorType 有两次兜底:先取全局 defaultExecutorType,仍为空再退回 SIMPLE。第二,CachingExecutor 是装饰器,不是第四种基础执行器;插件代理又包在缓存装饰器外层。因此一次执行可能同时经过“基础执行器策略”“二级缓存装饰器”“插件代理”三层结构。

flowchart TD
    TX["Transaction"] --> NewExecutor["Configuration.newExecutor(tx, executorType)"]
    NewExecutor --> Choose{"ExecutorType"}
    Choose -->|SIMPLE| Simple["SimpleExecutor"]
    Choose -->|REUSE| Reuse["ReuseExecutor"]
    Choose -->|BATCH| Batch["BatchExecutor"]
    Simple --> CacheCheck{"cacheEnabled?"}
    Reuse --> CacheCheck
    Batch --> CacheCheck
    CacheCheck -->|true| Caching["CachingExecutor(delegate)"]
    CacheCheck -->|false| Plugin
    Caching --> Plugin["InterceptorChain.pluginAll(executor)"]
    Plugin --> Executor["最终 Executor"]

BaseExecutor 提供了模板方法:先检查关闭状态和一级缓存,再决定是否调用子类的 doQuery()。真正和 JDBC Statement 打交道的是子类的 doQuery() / doUpdate(),但一级缓存、延迟加载、事务提交回滚这类横切逻辑放在基类里。

1
2
3
4
5
6
7
8
9
Executor.query(...)
-> MappedStatement.getBoundSql(parameter)
-> createCacheKey(ms, parameter, rowBounds, boundSql)
-> localCache.getObject(cacheKey)
-> doQuery(...) // 缓存未命中
-> configuration.newStatementHandler(...)
-> statementHandler.prepare(...)
-> statementHandler.parameterize(...)
-> statementHandler.query(...)

旧文里的 BaseExecutor.query(...) 片段还保留了几个判断点:

  • 查询前如果执行器已关闭,直接抛出 ExecutorException
  • queryStack == 0 && ms.isFlushCacheRequired() 时会清空本地缓存。
  • 只有 resultHandler == null 时才尝试从 localCache 读取结果;自定义 ResultHandler 会绕开这条缓存读取路径。
  • 数据库查询前会把 EXECUTION_PLACEHOLDER 放进本地缓存,查询完成后移除占位并写入真实结果;这用于处理嵌套查询中的循环引用和延迟加载场景。
  • StatementType.CALLABLE 的输出参数会额外进入 localOutputParameterCache
  • 最外层查询结束后会触发 deferredLoads,并在 localCacheScope=STATEMENT 时清空一级缓存。

这个结构很典型:BaseExecutor 不知道具体如何创建和复用 Statement,但它知道一次查询必须遵守哪些框架级规则。SimpleExecutorReuseExecutorBatchExecutor 只替换真正执行 JDBC 的那部分策略。

[PATTERN] 模板方法 + 策略执行器模式BaseExecutor 固化查询、缓存、提交、回滚的骨架,具体执行器替换 Statement 创建、复用、批处理策略。

StatementHandler 是 JDBC 边界

如果说 Executor 是执行主干,StatementHandler 就是 MyBatis 和 JDBC 的接缝。

RoutingStatementHandler 根据 MappedStatement.statementType 选择具体实现:

statementType StatementHandler JDBC 对象
STATEMENT SimpleStatementHandler Statement
PREPARED PreparedStatementHandler PreparedStatement
CALLABLE CallableStatementHandler CallableStatement

BaseStatementHandler 在构造时会创建两个关键协作者:ParameterHandlerResultSetHandler。前者负责把 Java 参数绑定到 JDBC PreparedStatement,后者负责把 ResultSet 映射成 Java 对象。

flowchart TD
    Executor["Executor"] --> RSH["RoutingStatementHandler"]
    RSH --> SimpleSH["SimpleStatementHandler"]
    RSH --> PreparedSH["PreparedStatementHandler"]
    RSH --> CallableSH["CallableStatementHandler"]
    PreparedSH --> PH["ParameterHandler"]
    PreparedSH --> RSH2["ResultSetHandler"]
    PH --> TH["TypeHandler"]
    RSH2 --> RM["ResultMap"]
    RSH2 --> OF["ObjectFactory / MetaObject"]
    PreparedSH --> JDBC["PreparedStatement"]

一次 PreparedStatement 查询可以拆成三个动作:

  1. prepare(connection, timeout):创建 JDBC PreparedStatement,设置超时、fetchSize 等。
  2. parameterize(statement):通过 ParameterHandler 绑定参数。
  3. query(statement, resultHandler):执行 SQL,并交给 ResultSetHandler 处理结果。

这三个动作分开之后,插件也就有了清晰拦截点。分页插件通常拦截 ExecutorStatementHandler,审计插件常拦截 Executor,参数加密或类型转换更适合靠 TypeHandler,结果处理则靠 ResultSetHandler 或自定义映射规则。

TypeHandler 是类型系统边界

TypeHandler 是 MyBatis 处理 Java 类型和 JDBC 类型差异的核心扩展点。它的边界很明确:参数入库时调用 setParameter(),结果出库时调用 getResult()

1
2
3
4
5
6
7
8
9
10
Java 参数对象
-> MetaObject 读取属性
-> ParameterMapping 找到 TypeHandler
-> TypeHandler.setParameter(ps, index, value, jdbcType)
-> JDBC

JDBC ResultSet
-> ResultSetHandler 定位列
-> TypeHandler.getResult(rs, column)
-> MetaObject 写入 Java 对象属性

TypeHandlerRegistry 会根据 Java 类型和 JDBC 类型选择处理器。常见类型有内置处理器,枚举、JSON、加密字段、数据库专有类型则通常需要自定义处理器。

TypeHandler 与插件的差别在于粒度。插件拦的是执行过程,适合处理“这次 SQL 调用如何执行”的问题;TypeHandler 拦的是值转换,适合处理“这个字段如何进出数据库”的问题。把字段级转换写进插件,通常会让逻辑混在 SQL 执行链里,难以复用。

[PATTERN] 类型边界模式:框架不让业务对象直接碰 JDBC 细节,而是在 Java 类型与 JDBC 类型之间插入一个稳定转换层。字段加密、枚举编码、JSON 列、时间类型兼容都应优先落在这个层次。

ResultSetHandler 如何组装对象

结果映射是 MyBatis 内部最复杂的部分之一。简单场景下,列名和属性名对应,DefaultResultSetHandlerObjectFactory 创建对象,再用 MetaObject 写入属性。复杂场景则会牵涉 ResultMap、嵌套映射、延迟加载、自动映射、构造器注入和鉴别器。

flowchart TD
    RS["ResultSet"] --> Wrapper["ResultSetWrapper"]
    Wrapper --> RMap["ResultMap"]
    RMap --> Create["ObjectFactory 创建结果对象"]
    Create --> Meta["MetaObject 包装对象"]
    Meta --> Auto["自动映射列到属性"]
    Meta --> Explicit["按 ResultMapping 显式映射"]
    Explicit --> Nested{"嵌套 resultMap / select?"}
    Nested -->|resultMap| NestedResult["嵌套结果集组装"]
    Nested -->|select| Lazy["延迟加载代理"]
    Auto --> Result["返回对象 / 集合"]
    NestedResult --> Result
    Lazy --> Result

MetaObject 值得单独注意。它把对象属性读写、Map 访问、集合访问统一到同一套反射包装接口里。MyBatis 的参数读取和结果写入都大量依赖它,因此业务对象可以是普通 JavaBean,也可以是 Map 或更复杂的属性路径。

结果映射的核心难点不在“把一列赋给一个字段”,而在“如何保持对象身份”。一对多嵌套结果映射时,同一个父对象会在多行结果中重复出现。MyBatis 需要根据 id 映射和缓存 key 合并对象,否则 join 查询会产生重复父对象。这也是为什么复杂 resultMap 里应该认真声明 <id>:它不只是文档信息,也会影响嵌套结果组装。

缓存体系:一级缓存与二级缓存不是同一件事

MyBatis 有两层缓存,位置和生命周期完全不同。

最小配置只涉及两个开关:

1
2
<setting name="cacheEnabled" value="true" />
<setting name="localCacheScope" value="SESSION" />

cacheEnabled 的默认值是 true,但它只决定是否启用二级缓存装饰器;localCacheScope 的默认值是 SESSION,它决定一级缓存是保留到会话结束,还是在每条语句后按 STATEMENT 粒度清掉。

缓存 所在位置 生命周期 默认行为 主要用途
一级缓存 BaseExecutor.localCache 同一个 SqlSession 默认开启 避免同会话重复查询,支持嵌套查询循环引用处理
二级缓存 namespace 对应的 Cache,由 CachingExecutor 管理 SqlSession,按 namespace 需要 mapper 启用 <cache/> 才真正使用 缓存较稳定的查询结果

一级缓存不靠 CachingExecutor。只要走 BaseExecutor,本地缓存就存在。localCacheScope=SESSION 时,同一个 SqlSession 内相同 cache key 的查询会命中;localCacheScope=STATEMENT 时,每条语句执行后清空本地缓存,缓存主要只服务执行过程中的循环引用和嵌套查询。

二级缓存靠 CachingExecutor。当 cacheEnabled=trueMappedStatement 有 namespace cache,并且该语句 useCache=true、没有自定义 ResultHandler 时,CachingExecutor 会通过 TransactionalCacheManager 读写二级缓存。二级缓存写入不是简单地“查询完立刻全局可见”,而是与事务提交/回滚绑定:提交后进入缓存,回滚则丢弃本次暂存。

MyBatis 一级缓存与二级缓存边界

CacheKey 决定“同一条查询”如何判等。它不是只用 SQL 字符串做 key,而是把多个维度按顺序 update(...) 进去:

1
2
3
4
5
6
7
CacheKey =
ms.getId()
+ rowBounds.offset
+ rowBounds.limit
+ boundSql.sql
+ 每个非 OUT 参数的实际值
+ environment.id

判等时还会同时比较内部 hashcodechecksumcountupdateList 中每个对象。旧文里强调的点仍然成立:同一个 SqlSession 内,只有 statement id、分页边界、最终 SQL、参数值、环境 id 都一致时,一级缓存才会命中。动态 SQL 只要生成的最终 SQL 或参数序列不同,就会得到不同的缓存 key。

CachingExecutor.query(...) 的骨架则是另一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

这段代码说明二级缓存有三道门槛:MappedStatement 必须能拿到 namespace cache,语句自身必须允许 useCache,并且不能使用自定义 ResultHandler。未满足这些条件时,查询直接委托给基础执行器;二级缓存没有命中时,也只是先查库再把结果交给 TransactionalCacheManager 暂存。

这里有几个实践结论:

  • cacheEnabled=false 主要影响二级缓存包装,不等于关闭一级缓存。
  • localCacheScope 控制一级缓存策略;这是排查“同一个 SqlSession 内重复读旧数据”时更容易被忽略的配置。
  • 同一个 SqlSession 里查询后再更新,如果语句配置要求 flush cache,会清理相关缓存。
  • 二级缓存按 namespace 组织,跨 mapper 共享需要 cache-ref,否则不同 namespace 的缓存互不相干。
  • 返回可变对象时要谨慎使用二级缓存。缓存保存的是结果对象,业务代码修改对象状态可能造成难以排查的污染风险,除非通过序列化缓存、只读约束或业务规范隔离。

[PATTERN] 事务性缓存装饰器模式CachingExecutor 不替代基础执行器,而是装饰基础执行器;TransactionalCacheManager 把缓存写入延后到事务边界,让二级缓存遵守提交/回滚语义。

插件链的边界

MyBatis 插件并不是任意 AOP。官方插件只支持拦截四类对象的方法:

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

插件通过 InterceptorInvocationPlugin 和 JDK 动态代理实现。每个目标对象创建后,Configuration 会调用 interceptorChain.pluginAll(target),按注册顺序逐层包装。

flowchart LR
    Target["Executor / StatementHandler / ParameterHandler / ResultSetHandler"] --> I1["Interceptor A"]
    I1 --> I2["Interceptor B"]
    I2 --> I3["Interceptor C"]
    I3 --> Final["代理对象"]

插件能力强,但也有清晰代价:它拦的是框架内部协议。一个分页插件如果改写 BoundSql,就必须理解参数映射、cache key、count 查询、方言差异、RowBounds 行为;一个审计插件如果在 Executor.update 里读取参数对象,就必须处理 Map、集合、单参数、多参数、@Param 等不同形态。

插件适合做执行级横切逻辑,不适合承载领域逻辑。字段级转换用 TypeHandler,对象创建用 ObjectFactory,SQL 语言扩展用 LanguageDriver,结果结构变化优先用 resultMap。插件应该是最后的扩展手段,而不是第一个。

事务与连接在哪里

MyBatis 的事务抽象很薄,主要围绕 Transaction 接口展开。Environment 同时持有 DataSourceTransactionFactory。打开 SqlSession 时,DefaultSqlSessionFactory 用事务工厂创建 Transaction,执行器通过事务对象获取连接、提交、回滚和关闭。

flowchart TD
    Environment["Environment"] --> DS["DataSource"]
    Environment --> TXF["TransactionFactory"]
    TXF --> TX["Transaction"]
    TX --> Conn["Connection"]
    Executor --> TX
    StatementHandler --> Conn

MyBatis 自带 JdbcTransactionFactoryManagedTransactionFactory。前者直接管理 JDBC 连接的提交、回滚、关闭;后者把事务管理交给容器。在 Spring 集成场景中,真正常见的是 MyBatis-Spring 接管 SqlSession 生命周期,并把连接绑定到 Spring 事务同步机制。这个部分属于集成层,不改变 MyBatis 核心执行链的结构。

一次查询的完整路径

把上面的组件串起来,一次普通 Mapper 查询大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 业务代码调用 mapper.selectById(id)
2. MapperProxy 拦截接口方法
3. MapperMethod 根据接口名和方法名定位 MappedStatement
4. MapperMethod 根据返回值类型选择 SqlSession.selectOne / selectList
5. DefaultSqlSession 调用 Executor.query
6. Executor 从 MappedStatement 取 BoundSql,并创建 CacheKey
7. BaseExecutor 检查一级缓存;CachingExecutor 可能检查二级缓存
8. 缓存未命中时,Executor 创建 StatementHandler
9. StatementHandler 创建 PreparedStatement
10. ParameterHandler 使用 TypeHandler 绑定参数
11. PreparedStatement 执行 SQL
12. ResultSetHandler 使用 ResultMap、TypeHandler、ObjectFactory、MetaObject 组装结果
13. Executor 写入本地缓存,必要时暂存二级缓存
14. SqlSession 返回结果;事务提交后,二级缓存写入才正式可见

执行链可以压缩成下面这张图:

MyBatis 一次查询执行链

这条路径解释了 MyBatis 的可扩展性为什么集中在几个点上。Mapper 方法名定位语句,SqlSource 生成 SQL,Executor 管执行策略和缓存,StatementHandler 管 JDBC,TypeHandler 管值转换,ResultSetHandler 管对象组装。每一层只承担一个方向的变化。

组件关系速查

组件 上游 下游 关键问题
SqlSessionFactoryBuilder 配置输入流 SqlSessionFactory 如何把外部配置编译成 Configuration
Configuration XML、注解、全局设置 全部运行时组件 元数据放在哪里,组件从哪里创建
MappedStatement Mapper XML / 注解 Executor 一条 SQL 的完整执行描述是什么
SqlSource XML 脚本 / 注解 SQL BoundSql 参数进入后最终 SQL 长什么样
MapperProxy Mapper 接口调用 MapperMethod 接口方法如何变成命令
SqlSession MapperMethod / 用户 API Executor 一次会话如何管理执行器和事务边界
Executor SqlSession StatementHandler 执行策略、缓存、事务如何协调
StatementHandler Executor JDBC 如何创建 Statement 并执行
ParameterHandler StatementHandler TypeHandler / JDBC Java 参数如何绑定到 SQL
ResultSetHandler JDBC ResultSet Java 对象 结果集如何变成对象图
TypeHandler 参数/结果映射 JDBC 类型 Java 类型和数据库类型如何互转
Interceptor Configuration.pluginAll 四类可拦截对象 横切逻辑插在哪里

设计模式视角

MyBatis 的源码适合用设计模式阅读,但不要停在“它用了某某模式”的标签层面。更有价值的是看每个模式解决的变化点。

模式 代表位置 解决的变化
Builder XMLConfigBuilderXMLMapperBuilderMappedStatement.Builder XML/注解输入复杂,最终对象需要稳定且完整
Registry TypeHandlerRegistryMapperRegistryLanguageDriverRegistry 按类型、接口、语言查找扩展实现
Dynamic Proxy MapperProxy、插件 Plugin 接口无实现类,运行时拦截方法调用
Template Method BaseExecutorBaseStatementHandler 固定执行骨架,延迟具体执行策略
Decorator CachingExecutor 在不改基础执行器的情况下加入二级缓存
Chain of Responsibility InterceptorChain 多个插件按顺序包装同一目标对象
Strategy ExecutorTypeTransactionFactoryLanguageDriverTypeHandler 同一抽象下替换不同实现
Adapter / Facade MetaObjectObjectWrapper 统一 JavaBean、Map、Collection 的属性访问

[PATTERN] 窄接口多策略模式:MyBatis 的扩展点普遍很窄。TypeHandler 只管单值转换,LanguageDriver 只管 SQL 脚本,Executor 只管执行策略,TransactionFactory 只管事务对象创建。接口越窄,扩展越不容易污染主链路。

阅读源码的三条锚点

阅读 MyBatis 源码时,最容易陷入包和类名的细节。更有效的方式是抓三条锚点。

第一条锚点是 MappedStatement。任何 SQL 最后都要落到它。读懂 MappedStatement 的字段,就能理解 Mapper XML 里大部分配置为什么存在。

第二条锚点是 Executor.query()。它串起 BoundSqlCacheKey、一级缓存、二级缓存、StatementHandler 和事务边界。MyBatis 的运行时行为,大半都能从这里解释。

第三条锚点是 Configuration.newXxx()。凡是想找插件为什么能生效、某个处理器在哪里创建、默认策略在哪里替换,都应该回到 Configuration 的工厂方法。

1
2
3
MappedStatement 解释“这条 SQL 是什么”
Executor.query 解释“这条 SQL 怎么执行”
Configuration.newXxx 解释“执行链组件从哪里来”

常见误区

误区一:关闭 cacheEnabled 就关闭了所有缓存。
cacheEnabled 控制的是是否包装 CachingExecutor,主要影响二级缓存。一级缓存属于 BaseExecutor.localCache,仍然存在。要改变一级缓存行为,需要看 localCacheScope 和语句的 flush cache 设置。

误区二:Mapper 代理直接执行 SQL。
MapperProxy 只做方法拦截和缓存 MapperMethod。SQL 执行仍然经过 SqlSessionExecutor。Mapper 接口只是类型安全入口,不是执行引擎。

误区三:插件可以随意拦截 MyBatis 任意类。
插件只支持四类目标。拦截不到的类不是配置写法问题,而是 MyBatis 插件协议本身的边界。

误区四:动态 SQL 是运行时拼字符串。
动态 SQL 在构建期已经解析成 SqlNode 树,运行时是根据参数应用这棵树,生成 BoundSql。这比散乱字符串拼接有更清晰的参数映射和上下文。

误区五:resultMap 只是字段别名表。
resultMap 还承载对象身份、嵌套映射、延迟加载、构造器注入等规则。复杂 join 场景里,<id> 配置会影响父子对象合并。

小结

MyBatis 的架构可以用一句话收束:构建期把配置编译成 Configuration 里的元数据对象图,执行期把 Mapper 方法调用翻译成对 MappedStatement 的执行,再通过 Executor -> StatementHandler -> ParameterHandler / ResultSetHandler -> JDBC 这条流水线完成数据库访问。

它的克制也在这里。MyBatis 没有试图隐藏 SQL,而是把 SQL 的解析、参数绑定、结果映射、缓存和插件横切拆成了清晰组件。读源码时只要抓住 ConfigurationMappedStatementExecutor 三个中心点,剩下的类大多都能放回自己的位置。

参考资料