MyBatis 底层原理。京东面试:MyBatis 如何实现 “面向接口” 查询的 ?
本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
尼恩说在前面:
最近大厂机会多了, 在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的面试题:
MyBatis 如何实现 “面向接口” 查询的 ?
昨天面试京东问 : mybatis 的用接口怎么实现查询的,Java 线程有几种状态[破涕为笑]
前几天 小伙伴面试 京东,遇到了这个问题。但是由于 没有回答好,导致面试挂了。
小伙伴面试完了之后,来求助尼恩。
那么,遇到 这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V175版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书
京东面试:MyBatis 如何实现 “面向接口” 查询的 ?
MyBatis 的接口怎么实现 面向查询的?
这是一个关于 MyBatis 的接口绑定(Interface Binding)机制的问题,也是 MyBatis 相比传统 DAO 模式最方便的特性之一。
核心答案
MyBatis 通过JDK 动态代理技术,在运行时, 动态为 Mapper 接口 生成代理实现类(MapperProxy
)。
MyBatis JDK 动态代理 的目标: 实现 “仅定义接口、无需手动编写实现类” , 完成数据库操作。
“面向接口” 的核心机制 与 实现步骤
1. 核心机制:JDK 动态代理
MyBatis 是“面向接口” 查询, 接口的实现类是 JDK 动态代理创建 的。 或者说, MyBatis 依赖 JDK 原生动态代理技术实现接口与数据库操作的绑定,而非手动编写接口实现类。
当定义好 Mapper 接口后,MyBatis 会 为该接口创建一个 MapperProxy
代理对象,并将其注册到 Spring 容器(若整合 Spring)中。
MapperProxy
代理对象 在应用初始化阶段(如 Spring 容器启动时)创建。 后续注入接口时,实际获取的是这个代理对象,接口方法的调用会被转发到代理对象的逻辑中。
2. “面向接口” 实现步骤
我们先来看一张核心流程图,帮助你建立整体认知:
这张图就是整个 MyBatis 接口查询的核心流程。下面我们一层一层地展开讲。
第一步:定义 Mapper 接口
创建 Java 接口,接口中的方法签名(方法名、参数类型、返回类型)需与后续 SQL 操作的配置对应,方法名将作为匹配 SQL 的核心标识。
public interface UserMapper {
// 单条查询:根据ID查用户
User selectUserById(Long id);
// 批量查询:查询所有用户
List<User> selectAllUsers();
// 插入操作:新增用户
int insertUser(User user);
}
第二步:配置 SQL(XML 式或注解式)
MyBatis 支持两种 SQL 配置方式,需确保 SQL 与接口方法绑定:
第一种SQL 配置方式: XML 式:
创建与接口同名的 XML 文件(如 UserMapper.xml
),通过 namespace
指定接口全限定名,id
与接口方法名完全一致。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace必须是Mapper接口的全限定名 -->
<mapper namespace="com.jd.dao.UserMapper">
<!-- id与接口方法名一致,resultType指定返回实体类型 -->
<select id="selectUserById" resultType="com.jd.model.User">
SELECT * FROM user WHERE id = #{id}
</select>
<select id="selectAllUsers" resultType="com.jd.model.User">
SELECT * FROM user
</select>
</mapper>
第一种SQL 配置方式: 注解式
直接在接口方法上通过 @Select
/@Insert
等注解编写 SQL,无需 XML 文件,SQL 与方法直接绑定。
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectUserById(Long id);
}
第三步:注入接口并使用
在 Service 层通过 @Autowired
(或 @Resource
)注入 Mapper 接口(实际注入的是 MyBatis 生成的 MapperProxy
代理对象),直接调用接口方法即可触发 SQL 执行。
@Service
public class UserService {
// 注入的是MyBatis生成的代理对象,而非接口本身
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
// 调用接口方法 → 实际触发代理对象的invoke()逻辑
return userMapper.selectUserById(id);
}
}
3. 执行过程(工作原理拆解)
当调用 userMapper.selectUserById(1L)
时,底层执行流程如下:
(1) 代理对象拦截调用:
方法调用被 MapperProxy
代理对象的 invoke()
方法拦截;
(2) 生成 SQL 唯一标识:
invoke()
方法根据 “接口全限定名 + 方法名” 生成唯一标识(如 com.jd.dao.UserMapper.selectUserById
);
(3) 匹配 MappedStatement
通过唯一标识从 MyBatis 全局配置(Configuration
)中找到对应的 MappedStatement
(封装了 SQL 语句、参数映射、结果映射等信息);
(4) 执行 SQL 会话流程:
MyBatis 借助 SqlSession
完成数据库交互:获取数据库连接 → 预编译 SQL → 填充参数 → 执行 SQL → 处理结果集(自动映射为接口声明的返回类型,如 User
对象);
(5) 返回结果:
将映射后的结果返回给调用方(如 Service 层)。
“面向接口” 查询的 案例介绍:
当调用 selectUserById(1L)
时发生了什么?
我们进入最核心的环节。
userMapper.selectUserById(1L);
虽然你是在调接口,但由于它是代理对象,所以会被拦截。具体步骤如下:
Step 1:代理拦截(invoke 方法触发)
MapperProxy.invoke()
被调用,开始处理请求。
Step 2:生成唯一标识
拼出一个全局唯一的 key:
com.example.dao.UserMapper.selectUserById
格式是:接口全限定名 + 方法名
类似于身份证号,确保每个方法都有唯一身份。
Step 3:查找 MappedStatement
拿着这个 key 去 MyBatis 的“中央仓库”(Configuration
对象)里找对应的 SQL 信息包 —— MappedStatement
。
这个包里包含了:
- 实际要执行的 SQL 语句
- 参数怎么映射(#{id} 对应哪个参数)
- 返回值怎么封装成 User 对象
Step 4:执行 SQL(交给 SqlSession)
MyBatis 拿到 MappedStatement
后,交给 SqlSession
去干活:
sqlSession.selectOne("com.example.dao.UserMapper.selectUserById", 1L);
内部流程:
(1) 获取数据库连接
(2) 预编译 SQL(防止 SQL 注入)
(3) 设置参数(把 1L 填进 #{id})
(4) 执行查询
(5) 把结果集自动映射成 User
对象(靠反射 + 字段名匹配)
Step 5:返回结果
最后把查到的 User
对象原路返回给你。
深度思考(关键问题解析)
深度思考 1:接口和 SQL(XML / 注解)如何关联?
MyBatis 在初始化阶段通过以下两种方式完成绑定:
(1) XML 与接口关联:
通过 @MapperScan
注解扫描指定包下的 Mapper 接口,同时读取配置的 XML 文件路径(如 mybatis.mapper-locations=classpath:mapper/*.xml
),将 XML 中 namespace
(接口全限定名)与接口匹配,id
(方法名)与接口方法匹配;
(2) 注解与接口关联:
扫描接口时直接解析方法上的 @Select
/@Insert
等注解,无需 XML 文件,SQL 通过注解直接与方法绑定,无需额外的 namespace
配置。
深度思考 2:Mapper 接口能不能进行方法重载?
不能。
原因是 MyBatis 通过 “方法名” 作为核心标识匹配 SQL(XML 中的 id
或注解绑定的方法),若存在重载方法(方法名相同、参数不同),会导致 SQL 标识冲突,MyBatis 无法区分对应的 SQL,启动时会抛出异常(如 BindingException: Invalid bound statement
)。
接口的 动态代理对象 MapperProxy 创建 流程
mybatis 为每个 Mapper 接口创建一个 MapperProxy
(代理对象) 。
MapperProxy
是 MyBatis 实现 “接口即实现” 的核心。
MapperProxy 创建发生在应用初始化阶段(若整合 Spring Boot,则在 @MapperScan
扫描时;若单独使用 MyBatis,则在 SqlSessionFactory
初始化时)。
我们可以把MapperProxy 的创建 过程分成 四个阶段,像工厂流水线一样层层推进:
阶段 | 名称 | 类比 |
---|---|---|
第1步 | 扫描所有 Mapper 接口 | 工厂开始清点要生产的“模具清单” |
第2步 | 注册接口到注册中心 | 把每个模具登记进管理系统,准备造代理工厂 |
第3步 | 创建代理对象(MapperProxy) | 按照模具造出能干活的“机器人替身” |
第4步 | 放入 Spring 容器供调用 | 把机器人送到岗位上,随时听候差遣 |
(1) 扫描 Mapper 接口(@MapperScan)
在 Spring Boot 启动类或配置类上添加 `@MapperScan("com.example.mapper")`,Spring 会扫描指定包下的所有接口,筛选出符合 Mapper 特征的接口(无特殊注解要求,仅需是接口)。
(2) 注册接口到 MapperRegistry
MyBatis 的核心配置类 `Configuration` 内部维护 `MapperRegistry`(Mapper 注册中心),扫描到的每个 Mapper 接口会被注册到这里:
核心步骤分解
MyBatis 内部有个“注册中心”叫 MapperRegistry
,它的任务是:
“每一个 Mapper 接口,我都给你配一个专属的‘代理生成工厂’。”
这个工厂的名字叫:MapperProxyFactory
public class MapperRegistry {
// 缓存:接口Class → MapperProxy工厂
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
// 仅处理接口(排除普通类)
if (type.isInterface()) {
// 避免重复注册
if (hasMapper(type)) return;
// 为接口创建MapperProxy工厂并缓存
knownMappers.put(type, new MapperProxyFactory<>(type));
// 解析SQL:读取XML或注解中的SQL,封装为MappedStatement
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
}
}
}
关键:为每个 Mapper 接口创建一个 MapperProxyFactory
(代理工厂),后续通过工厂生成代理对象。
1、MapperProxyFactory 生成代理对象
MapperProxyFactory
是生产 MapperProxy
的工厂类,核心逻辑是通过 JDK 动态代理生成接口实现:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface; // 目标Mapper接口
public T newInstance(SqlSession sqlSession) {
// 1. 创建InvocationHandler实例(核心代理逻辑封装在MapperProxy中)
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
// 2. 生成JDK动态代理对象:实现Mapper接口,委托逻辑到MapperProxy
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(), // 接口类加载器
new Class[] { mapperInterface }, // 实现的接口(仅目标Mapper接口)
mapperProxy // 代理逻辑处理器
);
}
}
2、MapperRegistry解析 SQL
parser.parse()
这一步很重要:它会去解析你在 XML 或注解里写的 SQL,比如:
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
并把这些 SQL 包装成 MappedStatement
对象,存在 MyBatis 全局配置中。
3、注册代理对象到 Spring 容器
当 Spring 需要把 UserMapper
注入到 Service 中时,它不会new一个接口 ,而是让对应的 MapperProxyFactory
动手造一个“假实现”。
这个“假实现”就是 MapperProxy
,它是 JDK 动态代理的产物。
再看原始代码:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy
);
}
}
生成的 MapperProxy
代理对象(实现了 Mapper 接口)会被注册为 Spring Bean。
当 Service 层通过 @Autowired
注入 Mapper 接口时,Spring 会返回这个MapperProxy
代理对象,而非接口本身。
关键技术点:
- 使用的是 JDK 动态代理,要求目标必须是接口。
Proxy.newProxyInstance(...)
会生成一个新对象,实现了UserMapper
接口。- 所有方法调用都会被拦截,交给
mapperProxy.invoke()
处理(这是InvocationHandler
的规则)。
Demo代码:模拟一下动态代理的工作原理
// 模拟 Mapper 接口
interface UserMapper {
User findUserById(int id);
}
// 模拟 MapperProxy 的作用:统一处理所有方法调用
class SimpleMapperProxy implements InvocationHandler {
private SqlSession sqlSession;
public SimpleMapperProxy(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("👉 正在执行 SQL: " + method.getName());
System.out.println("🔧 参数是: " + Arrays.toString(args));
// 实际上会根据 method 名查找 MappedStatement,然后执行 SQL
// 这里简化返回一个 mock 数据
return new User((int)args[0], "张三");
}
}
// 测试使用
public class ProxyDemo {
public static void main(String[] args) {
SqlSession sqlSession = null; // 简化省略初始化
InvocationHandler handler = new SimpleMapperProxy(sqlSession);
UserMapper mapper = (UserMapper) Proxy.newProxyInstance(
UserMapper.class.getClassLoader(),
new Class[]{UserMapper.class},
handler
);
User user = mapper.findUserById(1001); // 输出拦截日志
System.out.println("查询结果:" + user);
}
}
输出效果:
正在执行 SQL: findUserById
参数是: [1001]
查询结果:User{id=1001, name='张三'}
你看,根本没写实现类,但调用成功了!
这就是 MapperProxy
的本质。
动态代理对象 MapperProxy 的核心工作原理
...................由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址