为什么 Mapper 接口不需要写实现类?
结论先行:Mapper 接口的「实现类」是运行期由 JDK 动态代理生成的 MapperProxy。SqlSession.getMapper() 委托 MapperRegistry 拿到 MapperProxyFactory,用接口的 ClassLoader 创建代理;方法调用被拦截后,按「接口全限定名 + 方法名」拼出 statementId,再走 SqlSession 的正常执行链。
1. 代理是怎么产生的
sqlSession.getMapper(UserMapper.class)
│
▼ Configuration.getMapper()
MapperRegistry.getMapper()
│ knownMappers: Map<Class<?>, MapperProxyFactory<?>>
▼
MapperProxyFactory.newInstance(sqlSession)
│ Proxy.newProxyInstance(interface.getClassLoader(),
│ new Class[]{UserMapper.class},
│ new MapperProxy<>(sqlSession, mapperInterface, methodCache))
▼
返回接口代理对象
关键类只有三个:
| 类 | 职责 |
|---|---|
MapperRegistry | 注册表,保存「接口 → 代理工厂」的映射 |
MapperProxyFactory | 用反射创建代理实例,缓存 Method → MapperMethod |
MapperProxy | InvocationHandler 实现,负责把方法调用翻译成 SQL 执行 |
2. 代理的 invoke 做了什么
// MapperProxy.invoke 的简化逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args); // toString/equals/hashCode 走原逻辑
}
// ① 缓存 MapperMethod,避免每次反射解析
MapperMethod mapperMethod = cachedMapperMethod(method);
// ② 执行:内部按 SQL 命令类型分派
return mapperMethod.execute(sqlSession, args);
}
MapperMethod.execute 按 SqlCommandType 分派:
| 命令类型 | 调用 | 返回值处理 |
|---|---|---|
SELECT | sqlSession.selectList/selectOne | 单值、Optional、Map、多行 |
INSERT | sqlSession.insert | 影响行数,useGeneratedKeys 时回填主键 |
UPDATE | sqlSession.update | 影响行数 |
DELETE | sqlSession.delete | 影响行数 |
FLUSH | sqlSession.flushStatements | — |
3. statementId 怎么对上 XML
MapperMethod 里维护 SqlCommand:
// 拼接规则:接口全限定名 + "." + 方法名
this.name = mapperInterface.getName() + "." + method.getName();
所以下面两者必须严格一致:
package com.example.mapper;
public interface UserMapper {
User selectById(@Param("id") Long id);
}
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectById" resultType="com.example.User">
SELECT id, name FROM t_user WHERE id = #{id}
</select>
</mapper>
用 @Select 注解时不需要 XML,MapperAnnotationBuilder 在启动时把注解解析成 MappedStatement,namespace 与 id 同样按上述规则生成。
4. 一个手写的等价实现
理解代理最直接的方式是自己用 JDK 动态代理写一遍:
public class SimpleMapperProxy implements InvocationHandler {
private final SqlSession sqlSession;
public SimpleMapperProxy(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String statementId = method.getDeclaringClass().getName() + "." + method.getName();
Object param = args == null ? null : (args.length == 1 ? args[0] : toParamMap(method, args));
if (method.getReturnType().isAssignableFrom(List.class)) {
return sqlSession.selectList(statementId, param);
}
return sqlSession.selectOne(statementId, param);
}
}
// 使用
UserMapper mapper = (UserMapper) Proxy.newProxyInstance(
UserMapper.class.getClassLoader(),
new Class<?>[]{UserMapper.class},
new SimpleMapperProxy(sqlSession));
这几十行就是 MyBatis Mapper 机制的核心,剩下的是参数解析、注解、缓存等工程细节。
5. 为什么不支持方法重载
statementId 只用「接口名 + 方法名」,不带参数签名。两个重载方法会映射到同一个 MappedStatement,参数语义无法区分,所以 MyBatis 明确不支持 Mapper 方法重载(启动时不会报错,运行时行为不可预期)。
6. 常见追问
追问一:Mapper 接口能写 default 方法吗?
能。MyBatis 3.4.2 起 MapperProxy 会识别 method.isDefault(),直接反射调用接口的默认实现,不进入 SQL 执行链;更早的版本会抛 BindingException。所以 default 方法里可以组合多个 Mapper 方法。
追问二:代理对象能直接 toString() 吗?
能。MapperProxy 对 Object 声明的方法直接放行,返回代理描述,不会去执行 SQL。
追问三:getMapper 每次返回新对象吗?
是。MapperProxyFactory 内部会缓存 MapperMethod,但代理实例每次新建。它不持有状态(只持有 SqlSession),所以新建开销很小。
追问四:Spring 集成后是谁调用的 getMapper?
MapperFactoryBean 在启动时调用 sqlSessionTemplate.getMapper() 生成代理,注册进 IoC 容器。@Autowired 注入的就是这个代理对象。
追问五:为什么注解和 XML 可以混用?
两者最终都变成 Configuration 里的 MappedStatement,只是来源不同。MapperAnnotationBuilder 先加载注解,XMLMapperBuilder 再加载 XML;同名 id 会冲突并抛异常。
小结
Mapper 接口不需要实现类,是因为 MyBatis 用接口本身作为「SQL 的坐标」:接口名是 namespace,方法名是 statement id,参数是 SQL 参数,返回值是映射结果。MapperProxy 只做了一件事——把这个坐标翻译成 SqlSession 调用。