为什么 MyBatis 的 Mapper 接口不需要实现类?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
动态代理理解:面试官不仅仅是想知道 "不需要实现类" 这个结论,更是想考察你是否理解 JDK 动态代理的核心机制——
Proxy+InvocationHandler。 -
MyBatis 映射原理:看你能否说清楚 Mapper 接口方法是如何与 XML 中的 SQL 关联起来的,这个 "桥梁" 是怎么架起来的。
-
源码级深度:高级面试中,面试官会期望你说出
MapperProxy、MapperMethod、MappedStatement这些关键类的作用。
核心答案
因为 MyBatis 使用 JDK 动态代理,在运行时自动为 Mapper 接口生成代理对象,开发者的方法调用会被 MapperProxy 拦截,根据接口全限定名 + 方法名找到对应的 MappedStatement(SQL 元数据),然后委托给 SqlSession 执行。
Mapper 接口与 SQL 的关联依赖于一个核心约定:接口的全限定名 + 方法名 = Mapper XML 中的 namespace + SQL 标签的 id。MyBatis 启动时解析 XML 中每个 SQL 标签,生成 MappedStatement 对象,以 namespace.id 为 key 存入 Configuration 的 mappedStatements 集合中。代理对象拦截到方法调用时,就能通过这个 key 找到对应的 SQL。
深度解析
一、完整调用链路
整个调用链路可以分为两个阶段:
-
代理创建阶段(步骤 ①~④):调用
sqlSession.getMapper(UserMapper.class)时,MyBatis 从Configuration中的MapperRegistry找到对应的MapperProxyFactory,通过 JDK 动态代理创建一个实现了UserMapper接口的代理对象。这个代理对象持有一个MapperProxy(实现了InvocationHandler)。 -
方法调用阶段(步骤 ⑤~⑨):当开发者调用
mapper.selectById(1L)时,代理对象将调用转发给MapperProxy.invoke()。invoke()方法将 Java 方法封装为MapperMethod对象(会缓存),MapperMethod根据方法签名判断 SQL 类型,构造出statementId(全限定名 + 方法名),然后委托给SqlSession的对应方法执行。
二、源码关键类
| 类名 | 职责 |
|---|---|
MapperProxy | 实现 InvocationHandler,拦截 Mapper 接口的方法调用 |
MapperProxyFactory | 工厂类,通过 JDK 动态代理创建 Mapper 代理对象 |
MapperMethod | 封装Mapper 接口方法的信息(SQL 类型、参数、返回值),负责路由到 SqlSession |
MapperRegistry | 注册中心,维护 Class<T> → MapperProxyFactory<T> 的映射 |
MappedStatement | 一条 SQL 的完整元数据(SQL 文本、参数映射、结果映射等) |
三、核心源码解析
MapperProxy——代理对象的核心
// MapperProxy 实现了 InvocationHandler
public class MapperProxy<T> implements InvocationHandler {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
// 方法缓存:避免每次调用都创建 MapperMethod
private final Map<Method, MapperMethod> methodCache;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果调用的是 Object 的方法(如 toString、hashCode),直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 将 Method 封装为 MapperMethod(带缓存)
MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行:路由到 SqlSession 的对应方法
return mapperMethod.execute(sqlSession, args);
}
}
MapperProxy 的 invoke() 方法是整个代理机制的核心入口:
- 首先判断方法是否来自
Object类(如toString()、hashCode()),如果是就直接执行,不走代理逻辑。 - 然后将
java.lang.reflect.Method封装为MapperMethod对象,并缓存到methodCache中。MapperMethod解析了方法的返回类型、参数注解(如@Param)等信息。 - 最后调用
mapperMethod.execute(sqlSession, args),根据 SQL 类型路由到SqlSession的对应方法。
MapperMethod——SQL 类型的路由器
public class MapperMethod {
private final SqlCommand command; // SQL 类型 + statementId
private final MethodSignature method; // 方法签名信息
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 构造 statementId = "接口全限定名.方法名"
// 比如 "com.example.mapper.UserMapper.selectById"
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case SELECT:
if (method.returnsMany()) {
// 返回集合 → selectList
result = sqlSession.selectList(command.getName(), args);
} else {
// 返回单个对象 → selectOne
result = sqlSession.selectOne(command.getName(), args);
}
break;
case INSERT:
result = sqlSession.insert(command.getName(), args);
break;
case UPDATE:
result = sqlSession.update(command.getName(), args);
break;
case DELETE:
result = sqlSession.delete(command.getName(), args);
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
}
MapperMethod 的核心逻辑是根据 command.getType()(SQL 类型)路由到 SqlSession 的对应方法。command.getName() 就是 statementId(全限定名 + 方法名),SqlSession 用这个 key 从 Configuration 中找到对应的 MappedStatement,然后执行 SQL。
四、接口与 XML 的关联约定
Mapper 接口能 "自动" 找到 XML 中的 SQL,靠的是一个命名约定:
<!-- Mapper XML -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- id 必须和接口方法名一致 -->
<select id="selectById" resultType="User">
SELECT * FROM t_user WHERE id = #{id}
</select>
</mapper>
// Mapper 接口
package com.example.mapper;
public interface UserMapper {
// 方法名必须和 XML 中 <select> 的 id 一致
User selectById(Long id);
}
关联规则:
- XML 的
namespace= 接口的全限定名(com.example.mapper.UserMapper) - SQL 标签的
id= 接口的方法名(selectById) - 两者拼接后的
statementId=Configuration中MappedStatement的 key
如果名字对不上会怎样? 启动时不会报错(因为接口和 XML 是独立解析的),但调用方法时会抛出 BindingException:Invalid bound statement (not found): com.example.mapper.UserMapper.selectById。
五、为什么用 JDK 动态代理而不是 CGLIB?
| 对比维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 要求 | 必须有接口 | 不需要接口,生成子类 |
| Mapper 接口 | ✅ 天然是接口,完美适配 | ❌ 接口无法被继承生成子类 |
| 性能 | 略低(JDK 8 之后差距很小) | 略高 |
| 依赖 | JDK 内置,无额外依赖 | 需要引入 CGLIB 库 |
Mapper 接口本身就是接口,JDK 动态代理是唯一选择(CGLIB 通过生成子类实现代理,接口没有实现类,无法生成子类)。这是最自然、最合适的方案。
六、常见误区
- 误区一:"MyBatis 帮我生成了 Mapper 的实现类(.class 文件)"
- 不是。JDK 动态代理是在运行时内存中生成代理类(
$Proxy0),不会生成.class文件(除非开启-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true用于调试)。代理对象是动态创建的,没有静态的实现类。
- 不是。JDK 动态代理是在运行时内存中生成代理类(
- 误区二:"XML 中的 SQL 必须和接口方法一一对应"
- 是的。XML 中的每个
<select>/<insert>/<update>/<delete>标签都必须有对应的接口方法,反之亦然。如果 XML 中有 SQL 但接口没方法,启动不报错但调用报错;如果接口有方法但 XML 没有 SQL,调用时抛BindingException。
- 是的。XML 中的每个
- 误区三:"注解方式的 Mapper 也有 XML"
- 不一定。使用
@Select、@Insert等注解后,SQL 直接写在接口方法上,不需要 XML。MyBatis 会解析注解生成MappedStatement,代理机制完全相同。
- 不一定。使用
面试高频追问
- 追问一:如果 Mapper 接口方法名和 XML 的
id不一致会怎样?- 启动时不会报错(接口和 XML 独立解析)。运行时调用该方法会抛出
BindingException:Invalid bound statement (not found): com.example.mapper.UserMapper.xxx。排查时先检查namespace是否和全限定名一致,再检查方法名和id是否一致。
- 启动时不会报错(接口和 XML 独立解析)。运行时调用该方法会抛出
- 追问二:MyBatis 是在什么时候把 Mapper 接口注册到
MapperRegistry的?- 在 MyBatis 初始化阶段,
XMLMapperBuilder解析 Mapper XML 时,会根据namespace将对应的接口注册到MapperRegistry中。如果使用mybatis-config.xml中的<mapper class="..."/>或@MapperScan(Spring),也会触发注册。
- 在 MyBatis 初始化阶段,
- 追问三:同一个 Mapper 接口能不能同时用注解和 XML?
- 可以,但不能冲突。同一个方法不能既用
@Select注解又在 XML 中定义同名 SQL,否则启动报错。可以一部分方法用注解,另一部分用 XML。
- 可以,但不能冲突。同一个方法不能既用
常见面试变体
- 变体一:"MyBatis 的 Mapper 接口是怎么和 XML 关联的?"
- 变体二:"说说 MyBatis 的动态代理机制?"
- 变体三:"MyBatis 调用 Mapper 方法的完整流程是什么?"
记忆口诀
接口无实现靠代理,JDK 动态 Proxy + MapperProxy;全限定名加方法名,等于 namespace 加 SQL id;MapperMethod 路由到 SqlSession,代理拦截一切自动搞定。
总结
Mapper 接口不需要实现类,是因为 MyBatis 使用 JDK 动态代理在运行时为接口生成代理对象。MapperProxy 拦截方法调用,根据接口全限定名 + 方法名找到对应的 MappedStatement,通过 MapperMethod 路由到 SqlSession 的对应方法执行 SQL。整个关联依赖于 namespace + id 的命名约定。