MyBatis 如何实现“面向接口”查询

MyBatis 如何实现"面向接口"查询(Mapper Interface 的底层原理)

你写的是 UserMapper.selectById(1),但你没写实现类。

MyBatis 做的事是:启动时注册 Mapper 接口 → 运行时用动态代理生成实现 → 方法调用时定位到 MappedStatement → Executor 执行 SQL → 结果映射返回

下面把这条链路拆开讲清楚。


1. "面向接口"到底指什么?

在 MyBatis 里,"面向接口"通常就是:

  • 你只定义一个 Mapper 接口
  • 不写实现类
  • 直接注入/获取这个接口,然后调用它的方法完成查询

例子:

java 复制代码
public interface UserMapper {
    User selectById(Long id);
    List<User> listByStatus(Integer status);
}

调用方:

java 复制代码
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User u = mapper.selectById(1L);

这玩意之所以能跑,是因为 mapper 其实是 动态代理对象


2. MyBatis 面向接口查询的整体链路(从调用到 SQL)

一次 mapper.selectById(1L),大致经历:

  1. SqlSession.getMapper(UserMapper.class)
  2. MapperRegistry 找到这个接口对应的 MapperProxyFactory
  3. MapperProxyFactory 创建 JDK 动态代理(实现 UserMapper)
  4. 调用 selectById 进入 InvocationHandlerMapperProxy
  5. MapperProxy 把方法包装成 MapperMethod(缓存)
  6. MapperMethod 根据方法签名定位到 MappedStatement
  7. 通过 SqlSession.selectOne / selectList 进入 Executor.query
  8. StatementHandler 生成 PreparedStatement + 绑定参数
  9. JDBC 执行 SQL
  10. ResultSetHandler 做结果映射,返回 Java 对象

一句话:接口只是"门面",真正干活的是动态代理 + MappedStatement + Executor。


3. 关键点一:Mapper 接口是怎么"注册"的?

MyBatis 需要先知道:哪些接口是 Mapper。

3.1 原生 MyBatis:你手动注册

java 复制代码
Configuration config = sqlSessionFactory.getConfiguration();
config.addMapper(UserMapper.class);

3.2 Spring Boot:@MapperScan 自动扫描注册(最常见)

java 复制代码
@MapperScan("com.example.mapper")
@SpringBootApplication
public class App {}

Spring 会扫描包,把接口交给 MyBatis 注册进 MapperRegistry


4. 关键点二:getMapper() 到底返回了什么?

SqlSession#getMapper 最终走到:

  • Configuration.getMapper(type, sqlSession)
  • MapperRegistry.getMapper(type, sqlSession)
  • MapperProxyFactory.newInstance(sqlSession)

返回的不是实现类,而是 JDK Proxy

  • 运行时生成一个实现了 UserMapper 的代理类
  • 每个方法调用都转发到 MapperProxy#invoke

所以:Mapper 必须是接口(JDK 动态代理只能代理接口;当然也可以用 CGLIB,但 MyBatis 默认用 JDK)。


5. 关键点三:方法是怎么定位到对应 SQL 的?

MyBatis 用 statementId 来定位 SQL:
statementId = namespace + "." + methodName

5.1 XML 写法:namespace = 接口全限定名(强约定)

xml 复制代码
<mapper namespace="com.example.mapper.UserMapper">
  <select id="selectById" resultType="com.example.domain.User">
    select * from user where id = #{id}
  </select>
</mapper>

那么这条 SQL 的 statementId 就是:

复制代码
com.example.mapper.UserMapper.selectById

当你调用 UserMapper.selectById,MyBatis 就用同样的规则拼出来 statementId,去 Configuration 里拿到对应的 MappedStatement

5.2 注解写法:SQL 直接挂在方法上

java 复制代码
public interface UserMapper {
  @Select("select * from user where id = #{id}")
  User selectById(@Param("id") Long id);
}

注解在启动时也会被解析成 MappedStatement,同样放进 Configuration


6. 关键点四:MapperProxy 在 invoke 里做了什么?

MapperProxy#invoke(...) 的核心动作:

  1. 处理 Object 自带方法(toString/equals/hashCode)直接返回
  2. 找到/创建 MapperMethod(并缓存)
  3. 调用 MapperMethod.execute(sqlSession, args)

MapperMethod 内部会判断这是:

  • selectOne / selectList
  • insert/update/delete
  • 是否返回 void / int / List / Map / Cursor / Optional 等

最终落到 SqlSession 的对应方法执行。


7. 参数是怎么绑定的?(为什么 @Param 有时必须写)

你传入 selectById(1L) 这个 1L,MyBatis 需要把它映射成 #{} 里能用的参数名。

规则大概是:

  • 单参数(且不是 Map)时:一般可以直接用 #{param1} / #{value}(不同版本细节略有差异)
  • 多参数时:默认会给你命名成 param1, param2...
  • 你加了 @Param("id"),就能用 #{id},可读性更强,也更稳

例子:

java 复制代码
User select(@Param("id") Long id, @Param("status") Integer status);

SQL:

sql 复制代码
where id = #{id} and status = #{status}

8. 结果是怎么映射回对象的?

查询执行完返回 ResultSet 后:

  • ResultSetHandler 根据 resultType / resultMap
  • 读取列名/别名
  • 用反射或 setter 填充 Java 对象
  • 返回最终结果(User / List 等)

常见坑:

  • 数据库列 user_name vs Java 字段 userName
    • 你可以开 mapUnderscoreToCamelCase=true
    • 或者 SQL 用别名:select user_name as userName ...

9. 为什么这种设计很"面向接口"?

因为它把关注点分开了:

  • 业务只依赖接口:UserMapper
  • SQL 定义在 XML/注解:可替换、可维护
  • 实现由框架运行时生成:不用写模板代码
  • 执行与映射由 MyBatis 统一处理:减少重复

本质上:动态代理 + 配置驱动,让"接口=查询能力"成立。


10. 最常见的 6 个问题(排查方向)

  1. 找不到 statement

    • 报:Invalid bound statement (not found)
    • 检查:XML namespace 是否是接口全名?id 是否等于方法名?XML 是否被加载?
  2. Mapper 没注册

    • 检查:@MapperScan 包路径对不对?接口上有 @Mapper 吗?
  3. 参数取不到

    • 多参数建议都加 @Param
    • 或检查 #{} 名称是否匹配
  4. 结果映射为空/字段没填

    • 检查列名 vs 字段名
    • 开启驼峰映射或加别名
  5. 同名方法重载

    • XML 的 id 只能对应一个 statement
    • Mapper 方法重载很容易把你搞崩(尽量别重载)
  6. Spring 注入 Mapper 失败

    • 看是否依赖了 mybatis-spring-boot-starter
    • 包扫描、配置类、启动类位置是否正确

11. 一句话总结

MyBatis 的"面向接口查询"就是:

启动时把接口方法和 SQL 绑定成 MappedStatement;运行时用动态代理生成接口实现;方法调用时通过 statementId 找到 SQL 并执行。


相关推荐
while(1){yan}1 小时前
图书管理系统(超详细版)
spring boot·spring·java-ee·tomcat·log4j·maven·mybatis
林shir5 小时前
3.6-Web后端基础(java操作数据库)
spring·mybatis
super_lzb18 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
CodeAmaz1 天前
MyBatis 分页插件实现原理(Interceptor 机制 + SQL 改写)
mybatis·分页插件
此剑之势丶愈斩愈烈1 天前
mybatis-plus乐观锁
开发语言·python·mybatis
雨中飘荡的记忆1 天前
MyBatis数据源模块详解
mybatis
heartbeat..1 天前
Java 持久层框架 MyBatis 全面详解(附带Idea添加对应的XML文件模板教程)
java·数据库·intellij-idea·mybatis·持久化
Predestination王瀞潞1 天前
Java EE数据访问框架技术(第三章:Mybatis多表关系映射-下)
java·java-ee·mybatis
刘一说2 天前
2026年Java技术栈全景图:从Web容器到云原生的深度选型指南(附避坑指南)
java·前端·spring boot·后端·云原生·tomcat·mybatis