MapperMethod
MapperMethod 是 MyBatis 中连接”Java 方法调用”和”SQL 执行”的桥梁——它把一次 Mapper 接口的方法调用,翻译成对 SqlSession 的具体 API 调用。
可以把它理解为:**Mapper 接口里每个方法在运行时的”执行说明书”**。
一、它在调用链中的位置
1 | 业务代码: userMapper.selectById(1L) |
Mapper 接口本身没有实现类,是
MapperProxy在拦截方法调用,最终把活儿交给MapperMethod来”分发”。
二、它内部的两大组成
MapperMethod 由两个内部类组成,把”该调哪个 SqlSession 方法”和”参数/返回值怎么处理”分开:
1)SqlCommand——决定”调什么”
1 | public static class SqlCommand { |
- 在构造时,根据
接口全名 + 方法名去Configuration里查MappedStatement - 拿到 SQL 类型,决定后续要走
selectOne还是insert/update/delete
2)MethodSignature——决定”怎么调”
1 | public static class MethodSignature { |
- 通过反射分析方法签名(参数列表、返回值类型、注解)
ParamNameResolver把args[]数组转换成 MyBatis 需要的ParamMap(处理@Param、多参数、RowBounds、ResultHandler等)
三、execute() 方法——核心分发逻辑
1 | public Object execute(SqlSession sqlSession, Object[] args) { |
它做了三件关键的事:
- 根据 SqlCommandType 路由到对应的 SqlSession API(selectOne / selectList / selectMap / selectCursor / insert / update / delete)
- 参数转换:把 Java 方法的
args[]通过ParamNameResolver转成MapperMethod.ParamMap(MyBatis 内部统一的参数容器) - 返回值适配:自动包装成
List/Map/Optional/Cursor/ 数组等;INSERT/UPDATE/DELETE 还会处理int / long / boolean / void的行数返回
四、它什么时候被创建
由 MapperProxy 触发,懒加载 + 缓存:
1 | // MapperProxy.invoke() |
- 每个
Method第一次被调用时构建一次 - 缓存在
MapperProxyFactory.methodCache(ConcurrentHashMap<Method, MapperMethod>)中 - 同一个 Mapper 接口的所有代理共享这份缓存,构建只发生一次
新版 MyBatis 中
MapperMethod之上还包了一层PlainMethodInvoker,但思路一致:反射方法 → 元数据对象 → 复用执行。
五、和 MappedStatement 的关系
| 对象 | 视角 | 内容 |
|---|---|---|
| MappedStatement | SQL 视角 | 一条 SQL 的所有配置(id、SqlSource、resultMap、缓存…) |
| MapperMethod | Java 方法视角 | 一个接口方法的调用元数据(命令类型、参数解析器、返回值适配规则) |
二者通过 id(接口全名 + 方法名)一一对应:
1 | UserMapper.selectById(Long) |
可以理解为:MapperMethod 把”调用方”翻译标准化,MappedStatement 把”执行方”配置标准化,SqlSession 是它们之间的胶水层。
六、一个完整的对照例子
接口:
1 | public interface UserMapper { |
调用 userMapper.selectById(1L) 时:
| 阶段 | 对象/动作 |
|---|---|
| MapperProxy.invoke | 拦截 selectById 方法 |
| 取 MapperMethod | 缓存命中 → MapperMethod 实例 |
| SqlCommand | name=com.xxx.UserMapper.selectById, type=SELECT |
| MethodSignature | returnsOptional=true, paramNames=[“id”] |
| convertArgsToSqlCommandParam | [1L] → {id=1, param1=1} |
| 路由 | 走 sqlSession.selectOne(name, param) |
| 返回值适配 | 包成 Optional.ofNullable(user) |
总结
**MapperMethod = Mapper 接口方法的”运行时元数据 + 调用分发器”**:
- 静态部分:
SqlCommand(要调哪条 SQL)+MethodSignature(参数和返回值怎么处理) - 动态部分:
execute()把 Java 方法调用翻译成对 SqlSession 的标准 API 调用,并完成参数封装与返回值适配
它和 MappedStatement 是 MyBatis 运行时的两大基石:
MappedStatement 描述”SQL 长什么样”,MapperMethod 描述”Java 方法怎么调用这条 SQL”,MapperProxy 把两者串起来,让你用接口方法的方式无感知地执行 SQL。
MapperMehtod 与 MappedStatement 关系
MapperProxy里**确实看不到MappedStatement` 的直接引用**,这是 MyBatis 刻意设计的”分层解耦”。下面把这个迷雾解开。
一、MapperProxy 的字段,确实没有 MappedStatement
1 | public class MapperProxy<T> implements InvocationHandler, Serializable { |
MapperProxy 只持有三样东西:SqlSession、接口类型、方法缓存。它故意不接触 MappedStatement。
二、MappedStatement 藏在哪?——藏在 Configuration 里
整条引用链是这样的:
1 | MapperProxy |
也就是说:MappedStatement 不是某个对象的字段,而是统一注册在 Configuration 这个全局容器里,所有对象都通过 id(字符串 key)按需去查。
三、那 MapperProxy 是怎么”用”到 MappedStatement 的?
它不直接用,而是把活儿一路传下去,最终由 Executor 在执行时去 Configuration 里查:
1 | MapperProxy.invoke(method, args) |
关键的一句:Configuration.getMappedStatement(id) —— 这才是 MappedStatement 第一次”现身”的地方。在此之前,整条链路传递的都只是它的 id 字符串。
四、为什么这样设计?——三个好处
1)MapperProxy 极度轻量
它要为每个 Mapper 接口、每个调用方生成代理实例(JDK 动态代理对象)。如果让它持有 MappedStatement 引用,就要:
- 知道接口下所有方法 → 启动时全量装配
- 内存重复持有 → 浪费
而现在它只持有 SqlSession 一个引用,干净得不能再干净。
2)MappedStatement 全局唯一、按需取用
所有 MappedStatement 集中放在 Configuration.mappedStatements(一个 StrictMap<String, MappedStatement>)里:
- 唯一来源:避免多份拷贝
- 延迟绑定:方法被调用时才去查,没被调用过的 SQL 一次都不会被触碰
- 支持热更新/扩展:动态注册、覆盖检查、跨 namespace 引用都集中管理
3)解耦”调用入口”和”SQL 元数据”
MapperProxy/MapperMethod关心的是 “接口方法 → SQL 的 id”Executor及其下游关心的是 “id → 真正的 SQL 怎么执行”
中间用一根字符串 id 作为契约,两边互不依赖具体实现,这就是典型的”以 ID 解耦”设计。
五、用一张图总结
1 | ┌─────────────────────────────────────────────────────────┐ |
六、把前面的概念彻底串起来
| 对象 | 持有什么 | 不持有什么 |
|---|---|---|
| MapperProxy | SqlSession、Method 缓存 | ❌ 不持有 MappedStatement |
| MapperMethod | SqlCommand(id 字符串)、MethodSignature | ❌ 不持有 MappedStatement |
| SqlSession | Configuration、Executor | ✅ 间接通过 Configuration 拿到 |
| Configuration | Map<String, MappedStatement> | —— |
| Executor | 收到的是 MappedStatement 对象本身 | —— |
结论:你在 MapperProxy 里看不到 MappedStatement 是正常且正确的。MyBatis 在调用入口侧(Proxy / MapperMethod)只用 SQL 的 id 字符串 作为符号,到了 SqlSession 内部才通过
Configuration.getMappedStatement(id)把 id 兑换成真正的MappedStatement对象交给 Executor 执行。这种”ID 在前,元数据在后“的解耦方式,是 MyBatis 整个执行框架能保持简洁、可扩展的核心设计之一。
