MapperMethod 是 MyBatis 中连接”Java 方法调用”和”SQL 执行”的桥梁——它把一次 Mapper 接口的方法调用,翻译成对 SqlSession 的具体 API 调用。

可以把它理解为:**Mapper 接口里每个方法在运行时的”执行说明书”**。

一、它在调用链中的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
业务代码: userMapper.selectById(1L)


MapperProxy(JDK 动态代理 InvocationHandler)
│ 根据 Method 找到对应的 MapperMethod(带缓存)

MapperMethod.execute(sqlSession, args) ← 关键角色


SqlSession.selectOne / insert / update / delete / selectList / ...


Executor → StatementHandler → JDBC

Mapper 接口本身没有实现类,是 MapperProxy 在拦截方法调用,最终把活儿交给 MapperMethod 来”分发”。

二、它内部的两大组成

MapperMethod 由两个内部类组成,把”该调哪个 SqlSession 方法”和”参数/返回值怎么处理”分开:

1)SqlCommand——决定”调什么”

1
2
3
4
public static class SqlCommand {
private final String name; // = MappedStatement 的 id
private final SqlCommandType type; // SELECT / INSERT / UPDATE / DELETE / FLUSH
}
  • 在构造时,根据 接口全名 + 方法名Configuration 里查 MappedStatement
  • 拿到 SQL 类型,决定后续要走 selectOne 还是 insert/update/delete

2)MethodSignature——决定”怎么调”

1
2
3
4
5
6
7
8
9
10
11
12
public static class MethodSignature {
private final boolean returnsMany; // 返回 List/数组/Collection?
private final boolean returnsMap; // 返回 Map?(@MapKey)
private final boolean returnsVoid; // 返回 void?
private final boolean returnsCursor; // 返回 Cursor?
private final boolean returnsOptional; // 返回 Optional?
private final Class<?> returnType;
private final String mapKey; // @MapKey 的值
private final Integer resultHandlerIndex; // ResultHandler 参数位置
private final Integer rowBoundsIndex; // RowBounds 参数位置
private final ParamNameResolver paramNameResolver; // 解析 @Param、参数名
}
  • 通过反射分析方法签名(参数列表、返回值类型、注解)
  • ParamNameResolverargs[] 数组转换成 MyBatis 需要的 ParamMap(处理 @Param、多参数、RowBoundsResultHandler 等)

三、execute() 方法——核心分发逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT:
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
case UPDATE:
param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
case DELETE:
param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args); // selectList
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args); // selectMap
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args); // selectCursor
} else {
param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}

它做了三件关键的事:

  1. 根据 SqlCommandType 路由到对应的 SqlSession API(selectOne / selectList / selectMap / selectCursor / insert / update / delete)
  2. 参数转换:把 Java 方法的 args[] 通过 ParamNameResolver 转成 MapperMethod.ParamMap(MyBatis 内部统一的参数容器)
  3. 返回值适配:自动包装成 List / Map / Optional / Cursor / 数组等;INSERT/UPDATE/DELETE 还会处理 int / long / boolean / void 的行数返回

四、它什么时候被创建

MapperProxy 触发,懒加载 + 缓存

1
2
3
4
5
6
7
8
9
// MapperProxy.invoke()
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);

// MapperProxy.cachedMapperMethod()
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method,
k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
  • 每个 Method 第一次被调用时构建一次
  • 缓存在 MapperProxyFactory.methodCacheConcurrentHashMap<Method, MapperMethod>)中
  • 同一个 Mapper 接口的所有代理共享这份缓存,构建只发生一次

新版 MyBatis 中 MapperMethod 之上还包了一层 PlainMethodInvoker,但思路一致:反射方法 → 元数据对象 → 复用执行

五、和 MappedStatement 的关系

对象 视角 内容
MappedStatement SQL 视角 一条 SQL 的所有配置(id、SqlSource、resultMap、缓存…)
MapperMethod Java 方法视角 一个接口方法的调用元数据(命令类型、参数解析器、返回值适配规则)

二者通过 id(接口全名 + 方法名)一一对应:

1
2
3
4
5
6
7
8
UserMapper.selectById(Long)

├── MapperMethod ← 描述"这个 Java 方法怎么调"
│ └─ SqlCommand.name = "com.kewen.UserMapper.selectById"

└── 通过 name 关联到 ↓

└── MappedStatement ← 描述"这条 SQL 怎么执行"

可以理解为:MapperMethod 把”调用方”翻译标准化,MappedStatement 把”执行方”配置标准化,SqlSession 是它们之间的胶水层。

六、一个完整的对照例子

接口:

1
2
3
4
public interface UserMapper {
@Select("select id, name from user where id = #{id}")
Optional<User> selectById(@Param("id") Long id);
}

调用 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
2
3
4
5
6
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession; // ← 只有它
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache;
...
}

MapperProxy 只持有三样东西:SqlSession、接口类型、方法缓存。它故意不接触 MappedStatement

二、MappedStatement 藏在哪?——藏在 Configuration 里

整条引用链是这样的:

1
2
3
4
5
6
7
8
MapperProxy
│ 持有 SqlSession

SqlSession (DefaultSqlSession)
│ 持有 Configuration

Configuration
│ 持有 Map<String, MappedStatement> mappedStatements ← 在这里!

也就是说:MappedStatement 不是某个对象的字段,而是统一注册在 Configuration 这个全局容器里,所有对象都通过 id(字符串 key)按需去查。

三、那 MapperProxy 是怎么”用”到 MappedStatement 的?

不直接用,而是把活儿一路传下去,最终由 Executor 在执行时去 Configuration 里查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MapperProxy.invoke(method, args)


MapperMethod.execute(sqlSession, args)
│ 这里只持有一个字符串 id:command.getName() = "com.kewen.UserMapper.selectById"

sqlSession.selectOne(id, param)


DefaultSqlSession.selectOne()
│ configuration.getMappedStatement(id) ← 这一步才把 id 兑换成 MappedStatement

Executor.query(MappedStatement, param, ...)


StatementHandler / ParameterHandler / ResultSetHandler

关键的一句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
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────┐
│ Configuration │
│ Map<String, MappedStatement> mappedStatements │
│ ───────────────────────────────────────────── │
│ "com.kewen.UserMapper.selectById" → MappedStatement1 │
│ "com.kewen.UserMapper.insert" → MappedStatement2 │
│ ... │
└────────────────▲────────────────────────────────────────┘
│ getMappedStatement(id)

│(只在执行那一刻去查)

MapperProxy ──► MapperMethod ──► SqlSession ──► Executor
(无 MS 引用) (只存 id 字符串) (有 Configuration) ← 在这里把 id 换成 MS

六、把前面的概念彻底串起来

对象 持有什么 不持有什么
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 整个执行框架能保持简洁、可扩展的核心设计之一。