为什么 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/

面试考察点

当面试官提出这个问题时,不仅仅是想听到 “因为用了动态代理” 这个简单的结论,更希望通过你的回答考察以下几个层次:

  1. 对 JDK 动态代理机制的理解:你是否清楚 Java 中 “接口 + 动态代理” 是实现框架灵活性的核心手段之一。
  2. 对 MyBatis 框架核心运行原理的掌握:你是否了解 SqlSessionMapper 之间的纽带关系,以及框架是如何将接口方法调用“翻译”成数据库操作的。
  3. 对框架设计思想的理解:你是否能从 “解耦” 和 “约定优于配置” 的角度,理解这种设计带来的好处。
  4. 知识迁移与延伸能力:是否能联想到 Spring 中类似的设计(如 @Repository 接口),并能指出使用时需要注意的坑(如方法重载)。

核心答案

MyBatis 的 Mapper 接口不需要实现类,是因为 MyBatis 在运行时,会使用 JDK 动态代理技术,为这些接口生成一个代理对象。当我们调用 mapper.xxx() 方法时,实际上是在调用这个代理对象的方法。代理对象内部会拦截方法调用,并根据 接口的全限定名和方法名,找到对应的 MappedStatement(其中包含了要执行的 SQL、参数映射、结果映射等信息),然后通过 SqlSession 去执行 SQL,最终将结果返回。

简单来说:接口只是一个 “约定” 或 “描述”,真正的执行逻辑由 MyBatis 通过动态代理和配置文件/注解来动态实现。

深度解析

原理/机制

整个过程的核心是 JDK 动态代理方法签名与 SQL 语句的映射绑定

  1. 代理对象创建:当我们通过 SqlSession.getMapper(Class<T> type) 方法获取一个 Mapper 接口的实例时,MyBatis 会使用 MapperProxyFactory 创建一个实现了该接口的代理对象(MapperProxy)。
  2. 方法调用拦截:代理对象 MapperProxy 实现了 InvocationHandler 接口。任何对接口方法的调用,都会被它的 invoke 方法拦截。
  3. 查找与执行:在 invoke 方法中,MyBatis 会根据当前被调用的 方法对象(Method)接口的类名(Class),唯一确定一个 MappedStatement 的 ID(通常格式为:com.xxx.UserMapper.selectById)。然后,它会将方法参数转换为 SQL 参数,委托给 Executor 执行器去执行数据库操作,并将数据库返回的 ResultSet 按照方法返回类型(如 User, List<User>)进行映射转换。

代码示例

// 1. 定义 Mapper 接口(注意:没有任何实现类!)
public interface UserMapper {
    User selectUserById(Long id);
}

// 2. 在 XML 中编写对应的 SQL 映射
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectUserById" resultType="com.example.model.User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

// 3. 在 Java 代码中获取并使用 Mapper(核心步骤)
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 这里获取到的 `userMapper` 是一个动态代理对象,不是我们写的实现类
    UserMapper userMapper = session.getMapper(UserMapper.class);
    // 调用方法,触发代理逻辑
    User user = userMapper.selectUserById(1L);
    System.out.println(user.getName());
}

对比分析与最佳实践

  • 与传统 DAO 模式对比:传统模式需要编写接口 UserDao 和实现类 UserDaoImpl,实现类中充满了 jdbcTemplate.query(...) 等样板代码。MyBatis 模式消除了这些重复代码,让开发者只需关注接口定义和 SQL 本身,实现了更好的 关注点分离
  • 最佳实践
    • 严格对应:确保 namespace 属性与接口的全限定名完全一致,<select> 等标签的 id 属性与接口方法名完全一致。
    • 避免方法重载:Mapper 接口中不建议使用方法重载,因为 XML 中每个 SQL 语句的 ID 是唯一的方法名,无法区分重载方法。这是“约定”的一部分。
    • 结合 Spring:在 Spring 项目中,MyBatis-Spring 组件会扫描 Mapper 接口,并将其代理对象注册为 Spring Bean,我们可以直接使用 @Autowired 注入,这背后依然是动态代理机制。

常见误区

  • 误区一:“我没写实现类,所以 MyBatis 用了反射直接执行 SQL。” —— 错。反射(Method)只是获取方法信息,真正的调度核心是动态代理
  • 误区二:“Mapper 接口里可以写默认方法(default method),所以它需要实现类。” —— 错。JDK 8+ 的默认方法本身提供了实现,动态代理对象会直接继承这个默认实现,不会为其创建代理逻辑。这恰好证明了 MyBatis 设计的灵活性。
  • 误区三:“在单元测试中,我 Mock 这个接口很麻烦。” —— 正因它是接口,配合 Mockito 等框架进行 Mock 测试实际上非常方便,这正是面向接口编程的优势。

总结

MyBatis Mapper 接口无需实现类,其本质是 JDK 动态代理方法签名映射 的经典应用,体现了“约定优于配置”的框架设计思想,极大地简化了数据库操作层的开发。