本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
尼恩说在前面:
最近大厂机会多了, 在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的MyBatis 面试题。
前几天一个小伙面京东,被问 : MyBatis 如何实现 “面向接口” 查询的 ? mybatis 的用接口怎么实现查询的?
答案原文。
昨天 小伙伴面试 阿里,又遇到了这个问题。
但是由于 没有回答好, 小伙伴 被刷了。
小伙伴面试完了之后,来求助尼恩。
那么,遇到 这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增。
4抡暴击,帮助大家逆天翻盘。
可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V175版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书
第1抡暴击:介绍 MyBatis 基础使用
MyBatis 是一个半自动化 ORM 框架,它的核心目标是简化 Java 操作数据库的流程。相比原生 JDBC,它自动处理了连接管理、SQL 执行、结果映射等重复性工作,开发者只需关注两件事:
(1) 定义 Mapper 接口(声明方法)
(2) 编写 SQL 映射配置(XML 或注解)
调用时通过 SqlSession.getMapper() 获取代理对象,直接调用接口方法即可执行数据库操作,无需手动实现类。
原理拆解:为什么能“无类”调用?
MyBatis 利用 JDK 动态代理 技术,在运行时为 Mapper 接口生成代理对象。当你调用接口方法时(如 userMapper.selectById(1L)),MyBatis 会:
- 根据方法名(
selectById)和接口类型(UserMapper.class)定位到对应的 SQL 语句; - 解析参数并执行 SQL;
- 将结果集自动映射成 Java 对象返回。
这就实现了“只写接口 + 写 SQL 配置 = 可调用 DAO”的开发模式。
类比理解:就像你点外卖,只需要说“来一份红烧肉”(调用方法),平台自动找到对应餐厅、下单、配送(执行 SQL 并返回结果),你不用亲自做饭(写 JDBC 代码)。
流程分析:从调用到执行的完整流程
以下是使用 MyBatis 查询一条用户记录的核心流程:
(1) 定义 Mapper 接口
(2) 编写 XML 映射文件
(3) 获取 SqlSession
(4) 通过 getMapper() 获得代理实例
(5) 调用接口方法触发 SQL 执行
示例代码 1. 定义 Mapper 接口
public interface UserMapper {
User selectById(Long id);
}
示例代码 2. 定义映射文件
<!-- 2. UserMapper.xml 映射文件 -->
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
示例代码 3~5. 使用流程
// 3~5. 使用流程
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectById(1L); // 自动执行 SQL 并映射结果
System.out.println(user);
}
基础规则:接口方法→ 到 → sql 语句 的 关联
UserMapper.selectById方法→ 关联 → 对应 XML 中<select id="selectById">#{id}是占位符,自动接收方法参数Long idresultType指定将结果集映射为User类型对象
关键细节:各组件作用解析
| 组件 | 作用 |
|---|---|
| SqlSessionFactory | 创建 SqlSession 的工厂,通常全局唯一,由配置文件构建 |
| SqlSession | 表示一次数据库会话,提供 CRUD 和获取 Mapper 的能力 |
| Mapper 接口 | 约定数据访问方法签名,MyBatis 自动生成实现 |
| XML 映射文件 | 包含 SQL 语句与方法的绑定关系,支持动态 SQL |
| namespace + id | 共同构成唯一标识,用于匹配接口中的方法 |
💡 45岁老架构师尼恩提示:
- 接口全限定名必须与 XML 的
namespace一致- 方法名必须与
<select>、<insert>等标签的id属性一致- 参数通过
#{paramName}注入,避免 SQL 注入
MyBatis 调用流程

MyBatis 调用流程 要点 小结
- 两个必须:接口 + 映射配置(XML/注解)
- 一次代理:
getMapper()返回的是 JDK 动态代理对象 - 三者绑定:接口方法名 ↔ XML 中的 id ↔ 实际 SQL 语句
- 自动映射:结果集字段按名称匹配 Java 对象属性(可自定义规则)
此模式让 Java 代码与 SQL 彻底解耦,既保留了 SQL 的灵活性,又提升了开发效率,适合对 SQL 有精细控制需求的场景。
✅尼恩 总结 第1抡暴击 路径
【这个 阶段覆盖 核心点】:
要点1、清晰阐述了 MyBatis 作为半自动化 ORM 的核心价值,准确指出其相较于原生 JDBC 在连接管理、SQL 执行和结果映射上的封装优势
要点2、正确描述了 Mapper 接口与 XML 映射文件之间的绑定关系,包括 namespace 与接口全限定名的对应、方法名与 SQL id 的匹配规则
【 这个 阶段的欠缺和不足】:
停留在“是什么”和“怎么用”的表层叙述,未触及 MyBatis 如何实现 “无实现类调用” 的底层机制,缺乏 深挖
技术思维 停留 在使用者层面,尚未进化 的架构认知高度
【面试官的表情变化】:
面试官 短暂露出笑意(情绪缓和),但 很快消失 。
整体处于“还算满意,但不惊喜”的状态,距离拍案叫绝、主动追问细节还差一大截
【开始 组织 下一阶段 暴击升级】:
提升方向1:必须由外而内,切入 JDK 动态代理的核心机制,讲清楚 MapperProxy 如何成为所有方法调用的统一入口
提升方向2:深入 MapperMethod 的作用,说明它如何封装 SqlCommand 和 MethodSignature,完成 SQL 定位与参数解析的双重职责

第2抡暴击:介绍 MyBatis 核心原理, 动态代理与映射绑定
MyBatis 的核心机制: 通过 Java 动态代理为 Mapper 接口生成代理对象,结合 SQL 映射配置,实现接口调用自动转化为数据库操作。
大家写的 userMapper.selectById(1) 并没有实际实现类,而是由 MyBatis 自动生成的代理对象完成整个数据库访问流程。
这一过程 背后, 依赖几个关键组件协同工作:
- MapperProxy:代理控制器,拦截方法调用
- MapperMethod:封装了 SQL 指令和执行逻辑
- SqlSession:对外交互的入口
- Executor:真正的 SQL 执行引擎
原理拆解:从接口调用到数据库查询
我们以一个典型的调用为例:
User user = userMapper.selectById(1);
虽然 UserMapper 是一个接口,但能成功执行,是因为它被 MyBatis 包装成了代理对象。
整个流程可分为四个阶段:
1. 拦截调用 —— 调用的是代理对象的方法
当你获取 userMapper 时(例如通过 sqlSession.getMapper(UserMapper.class)),MyBatis 使用 Java 动态代理 创建了一个代理实例。
这个代理对象的类型是 MapperProxy,它实现了你定义的 Mapper 接口,并在 invoke() 方法中统一处理所有方法调用。
2. 映射定位 —— 找到对应的 SQL 语句
代理对象拿到方法调用后,会根据以下信息作为唯一标识去查找 SQL 配置:
接口全限定名 + 方法名
如:com.example.UserMapper.selectById
这个 ID 会匹配 XML 中的 <select> 标签或注解上的 SQL:
<!-- UserMapper.xml -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
或者使用注解方式:
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
MyBatis 在启动时, 将这些映射关系加载到内存中(保存在 Configuration 对象里),所以可以快速查找到对应 SQL 和返回类型。
3. 参数转换与 SQL 执行 —— 准备并执行数据库命令
找到 SQL 后,MyBatis 将 Java 方法参数 , 按照规则填充进 SQL 。
这一步由 MapperMethod 完成参数解析和包装。
然后,MapperMethod 调用 sqlSession 的增删改查方法,sqlSession 最终交由 Executor 执行。
Executor 是 MyBatis 的执行引擎,负责:
- 管理事务
- 一级缓存处理
- 调用 JDBC 的
PreparedStatement和ResultSet
4. 结果映射 —— 把数据库记录转成 Java 对象
SQL 执行完成后,JDBC 返回 ResultSet。
MyBatis 根据你在 <select> 中指定的 resultType 或 resultMap,自动将结果集字段映射到 Java 对象属性上。
比如:
<resultMap id="UserResultMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
</resultMap>
支持基本类型、POJO、嵌套对象等多种映射方式。
一次查询的完整步骤流程分析:
以下是上述过程的清晰分步说明:
| 步骤 | 操作内容 | 对应组件 |
|---|---|---|
| 1 | 调用 userMapper.selectById(1) | 开发者代码 |
| 2 | MapperProxy.invoke() 拦截调用 | MapperProxy |
| 3 | 构造 MapperMethod,查找 SQL 映射 | MapperMethod |
| 4 | 解析参数,构建 SQL 执行指令 | MapperMethod |
| 5 | 通过 SqlSession 调用执行器 | SqlSession |
| 6 | Executor 执行 JDBC 操作 | Executor |
| 7 | 处理 ResultSet,映射为 User 对象 | DefaultResultSetHandler |
| 8 | 返回结果给调用方 | —— |
MapperProxy/MapperMethod/SqlSession/Executor 四个类的关系
四个 核心角色:
- MapperProxy:代理控制中心
- MapperMethod:SQL 指令封装
- SqlSession:外部调用入口
- Executor:底层执行引擎
MyBatis核心执行流程中这几个关键类的协作关系图。

(1) 入口(MapperProxy):所有对Mapper接口的方法调用,首先被MapperProxy这个动态代理拦截。
(2) 封装(MapperMethod):代理将 方法调用 封装成一个MapperMethod。这个对象内部依赖SqlCommand(知道要执行什么SQL)和MethodSignature(知道方法的参数和返回类型)。
(3) 路由(SqlSession):MapperMethod根据SqlCommand中的SQL类型(SELECT/INSERT等),决定调用SqlSession对应的CRUD方法(如selectOne)。SqlSession扮演统一门户,接收指令。
(4) 执行(Executor):SqlSession并不真正处理数据库,而是将工作委托给Executor(真正的执行引擎)。Executor负责管理缓存、通过Statement执行SQL、处理结果集映射。
(5) 返回:数据库返回的结果沿着原路返回,最终由代理对象MapperProxy将结果返回给业务调用方。
这个流程清晰展示了MyBatis如何将一個接口方法的调用,优雅地转换为一条数据库SQL的执行。
MapperProxy /MapperMethod /SqlSession /Executor 四个类的关键细节
(1)MapperProxy:动态代理的核心
源码片段参考(简化版):
public class MapperProxy<T> implements InvocationHandler {
private SqlSession sqlSession;
private Class<T> mapperInterface;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只处理接口中的方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 封装成 MapperMethod 并执行
final MapperMethod mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
return mapperMethod.execute(sqlSession, args);
}
}
这段代码说明:所有 Mapper 接口方法都会被
MapperProxy拦截,并封装为MapperMethod执行。
(2)MapperMethod:SQL 指令的封装者
MapperMethod 内部有两个重要内部类:
- SqlCommand:存储 SQL 语句类型(SELECT/INSERT等)和实际 SQL 字符串
- MethodSignature:描述方法参数、返回类型等元信息
执行时根据 SQL 类型决定调用 sqlSession.selectOne() 还是 insert() 等方法。
(3)SqlSession:统一入口
SqlSession 提供了所有数据库操作的 API,但它不直接执行,而是委托给 Executor。
可以把 SqlSession 看作是一个“前台接待”,接收请求后转交给后台厨房(Executor)去做菜(执行 SQL)。
SqlSession 通过 “门面模式” 整合分散功能,既简化使用又保障框架内聚性。
SqlSession 管理缓存 生命周期 / 生效范围,保证缓存一致性。
(4)Executor:真正的执行引擎
常见的实现有:
- SimpleExecutor:默认,每次重新创建 Statement
- ReuseExecutor:缓存 Statement,重用
- BatchExecutor:用于批量操作
Executor 还管理一级缓存,不过 一级缓存 生命周期 SqlSession强绑定 :
- 当创建 SqlSession 时,Executor 管理的 localCache 缓存随之初始化;
- 执行查询后,结果会存入该 Executor 的localCache 缓存中;
- 若同一 SqlSession 内执行相同查询(参数、SQL 一致),直接从缓存获取,无需重复访问数据库;
- 当 SqlSession 执行 commit、rollback 或关闭时,Executor 管理的 localCache 缓存被清空,避免事务内数据不一致。
- SqlSession 管理 Executor 缓存 localCache 的生命周期 / 生效范围,保证缓存一致性。
- 也就是说,虽然Executor 管理的 localCache,但是 localCache的生命周期 与 SqlSession 强绑定。
MapperProxy /MapperMethod /SqlSession /Executor 四个类的 核心流程图

小节 要点
- 动态代理:
MapperProxy拦截接口调用 - 映射绑定:通过 “接口全限定名.方法名” 找到 SQL
- 四步流程:拦截 → 定位 → 执行 → 映射
这套机制让你只需定义接口和 SQL 映射,就能完成完整的数据访问,既简洁又灵活,是 MyBatis 设计精妙之处。
✅尼恩 总结 第2抡暴击 路径
【这里 阶段覆盖 核心点】:
MyBatis 的动态代理机制、MapperProxy 的拦截作用、MapperMethod 对 SQL 请求的封装、SqlSession 与 Executor 的执行链路、结果映射流程。
清晰地拆解了从接口调用到数据库查询的四个阶段,逻辑链条完整,组件职责分明,展现了扎实的框架认知基础。
【 这 阶段的欠缺和不足】:
没有 源码层级揭示代理创建的时机与注册机制;没有 剖析 MapperRegistry 如何维护接口与工厂的映射关系;
【面试官的表情变化】:
面试官 认可 你的 知识广度,但 未激起情绪峰值,做不到 拍案叫绝 。
【开始 组织 下一阶段 暴击升级】:
深入源码层级揭示代理创建的时机与注册机制;剖析 MapperRegistry 如何维护接口与工厂的映射关系; 将零散知识点升维为“框架初始化→代理生成→方法调度→SQL 执行”的系统性架构思维。

第3抡暴击:进行 MapperProxy 底层源码 解析
MyBatis 能让接口方法直接执行 SQL,靠的不是魔法,而是 动态代理机制。
真正的“执行者”并不是你写的 Mapper 接口实现类(因为你根本没写),而是一个由 MapperProxy 自动生成的代理对象。
这个代理对象在你调用任意 Mapper 方法时,会拦截调用,解析方法意图,并最终转发给 SqlSession 去执行对应的 SQL。
整个流程可以概括为:
获取代理 → 拦截方法 → 封装请求 → 执行SQL
原理拆解:从 getMapper 到方法调用的背后
1. 动态代理的起点:SqlSession.getMapper
当你调用:
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
MyBatis 并不会去实例化 UserMapper 这个接口,因为接口不能被 new。
它通过 JDK 动态代理 创建一个代理对象,该对象实现了 UserMapper 接口,所有方法调用都会被统一处理。
这个过程的核心是 MapperRegistry 和 MapperProxyFactory。
MapperRegistry是一个注册中心,保存了所有 Mapper 接口与其对应工厂的映射。MapperProxyFactory是工厂类,专门负责为某个 Mapper 接口生成代理实例。
2. 工厂创建代理:MapperProxyFactory.newInstance
以下是关键代码片段:
// MapperProxyFactory 关键代码
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy
);
}
这段代码的作用是:
- 使用 JDK 的
Proxy.newProxyInstance方法; - 以
mapperInterface(如 UserMapper.class)作为要实现的接口; - 把
MapperProxy实例作为调用处理器(InvocationHandler);
也就是说,每一个 Mapper 接口的方法调用,都会交给 MapperProxy.invoke 来处理。
一次方法调用的完整路径
来看这样一个调用:
User user = userMapper.selectById(1);
这行代码背后发生了什么?
下面是分步流程:
(1) 调用被 MapperProxy.invoke 拦截;
(2) 根据当前 Method 对象查找或缓存对应的 MapperMethod;
(3) MapperMethod 封装了该方法对应的 SQL 信息(比如 SQL 语句、类型、参数映射等);
(4) 调用 mapperMethod.execute(sqlSession, args) 开始执行;
(5) 内部根据 SQL 类型决定调用 sqlSession.selectOne、update 等具体方法;
(6) 最终由 MyBatis 执行 SQL 并返回结果。
源码 MapperProxy.invoke
// MapperProxy.invoke 方法核心逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果是 Object 自身的方法(如 toString),直接放行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 缓存机制:避免重复创建 MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行命令
return mapperMethod.execute(sqlSession, args);
}
说明:
cachedMapperMethod(method):先查缓存,没有就根据方法签名和 Mapper 配置构建一个新的MapperMethod;mapperMethod.execute(...):进入命令执行阶段,真正触发 SQL 调用。
MapperMethod 如何决定执行方式?
MapperMethod 是一个封装类,它知道每个方法对应的是查询、更新还是其他操作。
它的 execute 方法会根据 SQL 类型和返回值类型,选择合适的 SqlSession 方法:
// 简化版 execute 逻辑示意
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else {
result = executeForObject(sqlSession, args);
}
break;
case UPDATE:
case INSERT:
case DELETE:
result = executeUpdate(sqlSession, args);
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
技术点总结**:
command.getType()来自于 SQL 注解或 XML 中的<select>、<update>等标签;- 返回类型的判断(是否集合、是否 Map、是否有 ResultHandler)决定了调用
selectList还是selectOne; - 整个设计体现了 命令模式(Command Pattern) —— 把一次数据库操作封装成对象来执行。
Mapper 方法调用全过程(Mermaid)流程图:

代码与流程的对应关系
| 流程阶段 | 对应代码位置 | 作用 |
|---|---|---|
| 获取代理 | MapperProxyFactory.newInstance | 创建 JDK 动态代理实例 |
| 拦截调用 | MapperProxy.invoke | 拦截所有接口方法调用 |
| 封装请求 | MapperMethod 构造函数 | 解析方法签名 + SQL 映射信息 |
| 执行命令 | MapperMethod.execute | 根据类型调用 SqlSession 相应方法 |
小结:为什么需要 MapperProxy?
- 接口无法实例化 → 必须用代理生成实现;
- 方法无实现体 → 需要在运行时解析注解/XML 映射;
- 统一调度入口 → 所有调用集中到
MapperProxy.invoke处理; - 解耦 SQL 执行与接口定义 → 通过
MapperMethod封装命令,符合开闭原则。
✅尼恩 总结 第3轮 暴击路径
【此 阶段覆盖 核心点】:
完整的执行链路清晰拆解为“获取代理 → 拦截方法 → 封装请求 → 执行SQL”四个步骤;
源码层级的分析展示了对 JDK 动态代理、Proxy.newProxyInstance 调用逻辑以及方法缓存机制的理解。
【 此 阶段的欠缺和不足】:
技术细节 慢慢, 但是 未能揭示这一整套机制背后所服务的更高层次架构目标,导致技术细节与系统思想脱节,呈现出“知其然不知其所以然”的断层。
【面试官的表情变化】:
从最初的微微点头、身体前倾表现出兴趣,到听到源码解析时轻敲桌面表示认可
再到中期逐渐放缓笔记速度,显露出“这些我都知道,接下来呢?”
【开始 组织 下一阶段 暴击升级】:
接下来, 需要 引入架构视角, 进入 “半自动化ORM”的 整体设计目标的介绍;
揭示“约定优于配置”和“分离关注点” 的架构思想;
完成从执行者到设计者的思维跃迁。

第4抡暴击: 架构升华, 升华到 MyBatis 架构思想
MyBatis 的核心架构思想是:在灵活性与自动化之间取得平衡。
它不是完全隐藏 SQL 的“全自动”ORM(如 Hibernate),也不是像 JDBC 那样事无巨细都要手动处理,而是采用 半自动化的 SQL 映射机制,让开发者写 SQL,框架负责执行流程的自动化。
这种设计实现了:
- SQL 可控、可优化;
- Java 代码简洁、无冗余;
- 开发效率高,同时兼顾性能。
MyBatis 架构思想
MyBatis的这一设计体现了 “约定优于配置” 和 “分离关注点” 的架构思想。
它将“做什么”(接口声明)和“怎么做”(SQL实现)清晰分离,SQL集中管理便于审查和优化,Java代码则保持极致的简洁。
**“架构思想1: 约定优于配置”:**MyBatis 通过默认约定减少配置成本,如 Mapper 接口与 XML 文件同名同路径、方法名与 SQL 标签 id 一致,无需额外配置即可关联,仅在特殊场景才需显式配置,提升开发效率。
“架构思想2:分离关注点”: 将数据操作的 “定义(接口方法)” 与 “实现(SQL 逻辑)” 拆分,Java 代码专注关注业务接口,SQL 在 XML / 注解中独立维护,既保证 Java 代码简洁,又便于 SQL 优化与管理,实现业务与数据访问逻辑的解耦。
MyBatis 设计权衡
-
设计思想:灵活性与自动化之间做出的 权衡
MyBatis 不是一个全自动ORM/ “黑箱” ORM(如Hibernate)。MyBatis 是一个半自动化/半透明的SQL映射框架。这种设计体现了其在 灵活性与自动化之间做出的精妙权衡。它承认SQL的复杂性和重要性,不试图完全封装SQL ,而是将SQL的控制权交还给开发者,同时通过动态代理自动化处理所有重复的模板代码(参数设置、结果映射、资源管理等)。
-
优劣权衡:
相比于全自动ORM,MyBatis需要开发者编写SQL,学习成本稍高,但换来了对SQL的极致控制和优化能力,避免了因框架自动生成SQL而导致的性能问题。相比于纯JDBC,它通过动态代理等机制自动化了繁琐流程,大大提升了开发效率。这种“SQL友好”的设计思想,使其在需要复杂查询和高性能要求的互联网场景中备受青睐。
… 略5000字+
…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

被折叠的 条评论
为什么被折叠?



