MyBatis 作为 Java 持久层最主流的技术栈之一,凭借其灵活可控的 SQL 编写能力和出色的性能表现,已成为后端开发面试中绕不开的高频考点。然而不少学习者长期停留在“会用”层面——知道怎么写 Mapper 文件,却说不清 Executor 与 StatementHandler 的分工;能跑通 demo,却答不出 N+1 查询问题的成因。本文借助


一、痛点切入:为什么 JDBC 时代需要 MyBatis?

先看一段最原始的 JDBC 查询代码:
Connection conn = null;PreparedStatement ps = null; ResultSet rs = null; try { // 1. 注册驱动 + 获取连接 Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url, user, password); // 2. SQL 硬编码 String sql = "SELECT id, username, email FROM user WHERE id = ?"; ps = conn.prepareStatement(sql); ps.setInt(1, 1); // 手动设置参数 // 3. 手动遍历结果集并封装 rs = ps.executeQuery(); List<User> users = new ArrayList<>(); while (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setEmail(rs.getString("email")); users.add(user); } } catch (Exception e) { e.printStackTrace(); } finally { // 4. 手动释放资源 if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close(); }
这段代码暴露了 JDBC 的五大痛点:
| 痛点 | 表现 |
|---|---|
| 硬编码严重 | SQL 语句、驱动类名、URL 都写在 Java 代码中,修改 SQL 需要重新编译 |
| 手动管理连接 | 创建/关闭 Connection 的样板代码大量重复 |
| 手动设置参数 | 每个占位符都需要 setXxx(),参数一多极易出错 |
| 手动封装结果集 | rs.getXxx() 与 setter 重复调用,字段变更要改多处 |
| 无缓存机制 | 相同的查询每次都要走数据库,毫无优化余地 |

二、核心概念讲解:MyBatis 的核心组件
2.1 什么是 MyBatis?
MyBatis(原名 iBatis)是一款半自动对象关系映射(ORM,Object Relational Mapping)框架,专注于简化 JDBC 操作,将 SQL 与 Java 代码解耦,同时保留 SQL 的完全可控性。-22
生活化类比:把 MyBatis 想象成一家餐厅——JDBC 是自己买菜、洗菜、切菜、炒菜、洗碗的全流程;而 MyBatis 是提供标准化服务的后厨,你只需要写好“菜谱”(XML 配置),后厨自动帮你采购食材、烹饪、装盘上桌。
2.2 核心组件一览
| 组件 | 作用 |
|---|---|
| SqlSessionFactoryBuilder | 解析配置文件,构建 SqlSessionFactory |
| SqlSessionFactory | 单例,负责创建 SqlSession |
| SqlSession | 与数据库的一次会话,包含执行 SQL 的方法(线程不安全) |
| Configuration | 全局配置信息的容器,保存所有 MappedStatement、数据源等 |
| MappedStatement | 封装一条 SQL 语句的全部信息(id、参数映射、结果映射等) |
| Executor | 执行器,真正负责 SQL 执行与缓存管理 |
| StatementHandler | 封装 JDBC Statement 操作 |
| ParameterHandler | 处理 SQL 占位符参数绑定 |
| ResultSetHandler | 处理结果集到 Java 对象的映射 |
| TypeHandler | Java 类型与 JDBC 类型之间的转换器 |
三、关联概念讲解:Executor 执行器的三种实现
Executor 是 MyBatis 的“发动机”,它有三种实现,分别对应不同的执行策略:-21
| 执行器 | 策略 | 适用场景 |
|---|---|---|
| SimpleExecutor | 每执行一次 SQL 就创建一个 Statement,用完即关 | 默认选择,适合大多数常规场景 |
| ReuseExecutor | 以 SQL 为 key 缓存 Statement 对象,重复使用 | 同一会话中执行多条相似 SQL |
| BatchExecutor | 将 SQL 加入批处理队列,统一执行(不支持 SELECT) | 批量 insert/update |
注意:当启用二级缓存时,MyBatis 会用 CachingExecutor 装饰底层 Executor,先查二级缓存再委派给具体执行器。-13
四、概念关系总结:一句话速记
SqlSession 是门面,Executor 是发动机,StatementHandler 是翻译官,MappedStatement 是施工图。
SqlSession 是面向用户的 API,真正干活的是 Executor(外观模式)。
Executor 执行 SQL,过程中依次委托 StatementHandler(封装 Statement 操作)、ParameterHandler(设参)、ResultSetHandler(结果映射)。
MappedStatement 承载着 SQL 语句、参数映射、结果映射等全部元信息。
五、代码示例:一次完整的查询生命周期
// 1. 初始化:读取配置文件,构建 SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 2. 获取会话 try (SqlSession session = sqlSessionFactory.openSession()) { // 3. 获取 Mapper 代理对象(动态代理) UserMapper mapper = session.getMapper(UserMapper.class); // 4. 执行查询 User user = mapper.selectUserById(1); System.out.println(user); }
Mapper.xml 配置:
<mapper namespace="com.example.mapper.UserMapper"> <select id="selectUserById" resultType="com.example.entity.User"> SELECT id, username, email FROM user WHERE id = {id} </select> </mapper>
底层执行链路:
mapper.selectUserById(1) ↓ MapperProxy.invoke() — 动态代理 ↓ 根据方法找到 MappedStatement(SQL + 参数映射 + 结果映射) ↓ SqlSession.selectList() ↓ CachingExecutor(二级缓存)→ BaseExecutor(一级缓存) ↓ 创建 StatementHandler + ParameterHandler 设参 ↓ JDBC PreparedStatement 执行 ↓ ResultSetHandler 映射结果 ↓ 返回 User 对象
六、底层原理支撑:动态代理与反射
MyBatis 的 Mapper 接口之所以不需要实现类,底层依赖 JDK 动态代理。当你调用 sqlSession.getMapper(UserMapper.class) 时,MyBatis 通过 MapperProxyFactory 生成一个 MapperProxy 代理对象,方法调用被拦截后,根据接口方法名从 Configuration 中查找对应的 MappedStatement,再委托给 SqlSession 执行。-13
这一机制高度依赖 反射:MyBatis 通过反射读取实体类的字段、调用 getter/setter、构建结果对象,是整个框架能够“自动”完成映射的底层基石。
七、动态 SQL 与缓存机制
7.1 动态 SQL:告别字符串拼接噩梦
MyBatis 提供了一套 XML 标签体系,让 SQL 可以根据业务条件动态拼接:
| 标签 | 作用 | 示例 |
|---|---|---|
<if> | 条件判断,按需拼接 | WHERE 1=1 <if test="name != null">AND name = {name}</if> |
<choose> | 多选一,类似 switch | 优先级条件筛选 |
<where> | 智能处理 WHERE,自动剔除多余 AND/OR | 动态 WHERE 条件 |
<set> | 智能处理 UPDATE,自动剔除多余逗号 | 部分字段更新 |
<foreach> | 遍历集合 | 批量插入、IN 条件查询 |
动态 SQL 执行流程:XML 中的动态标签被解析为 SqlNode 对象(如 IfSqlNode),运行时通过 OGNL(Object Graph Navigation Language,对象图导航语言) 计算表达式条件,决定哪些 SQL 片段被最终拼接。-33
7.2 缓存机制:一级与二级
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession 级别(同一个会话内共享) | Mapper namespace 级别(跨会话) |
| 默认开启 | 是 | 否(需手动配置) |
| 实现原理 | PerpetualCache(基于 HashMap) | 同上,但需序列化 |
| 生命周期 | 与 SqlSession 一致,会话关闭即清空 | 随 namespace 存在,commit/rollback 清空 |
| 适用场景 | 同一会话内的重复查询 | 多会话共享的静态数据 |
源码视角:一级缓存逻辑在 BaseExecutor 中实现,每次查询会先构建 CacheKey(基于 SQL 语句 + 参数),再从 localCache 中查找;若未命中,则执行数据库查询并将结果存入缓存。-33
八、高频面试题与参考答案
面试题 1:请说说 MyBatis 的工作原理?
参考答案(建议背诵):
读取全局配置文件:
mybatis-config.xml,配置数据源、事务等。加载映射文件:解析 XML/注解中的 SQL 语句,生成
MappedStatement对象并注册到Configuration。构造会话工厂:通过
SqlSessionFactoryBuilder构建SqlSessionFactory。创建会话:
SqlSessionFactory.openSession()创建SqlSession。获取 Mapper 代理:
getMapper()通过动态代理生成接口实现。Executor 执行:执行器根据参数生成 SQL,维护缓存。
参数映射与结果映射:
ParameterHandler设参,ResultSetHandler封装结果。-21
面试题 2:{} 和 ${} 的区别是什么?
| 特性 | {} | ${} |
|---|---|---|
| 处理方式 | 预编译(PreparedStatement),使用 ? 占位符 | 字符串直接替换 |
| 安全性 | 防 SQL 注入 | 存在 SQL 注入风险 |
| 使用场景 | 传入参数值 | 表名、列名等动态标识符 |
| 示例 | WHERE id = {id} → WHERE id = ? | ORDER BY ${column} → 直接拼接 |
面试题 3:MyBatis 的缓存机制是怎样的?一级缓存为什么在 Spring 整合后可能失效?
参考答案:
一级缓存:SqlSession 级别,默认开启。在 Spring 整合后,每个事务或每次查询可能使用不同的 SqlSession(由 Spring 管理),导致一级缓存无法共享,表现上“失效”。-21
二级缓存:Mapper namespace 级别,需手动配置
<cache/>,跨会话共享。
面试题 4:什么是 N+1 查询问题?如何解决?
参考答案:
现象:在关联映射中使用
select嵌套查询时,先查主表得到 N 条记录,再逐条执行 N 次子查询,总共产生 N+1 次 SQL,严重影响性能。-48解决方案:
方案一:使用 嵌套结果(resultMap + JOIN),一条 SQL 查出全部数据,由 MyBatis 组装对象。
方案二:调整 懒加载 策略,或使用
fetchType="eager"配合 JOIN。方案三:在业务层手动进行批量查询(如使用
IN查询一次性获取所有关联数据)。
九、结尾总结
回顾本文核心知识点:
| 知识点 | 核心要点 |
|---|---|
| 为什么需要 MyBatis | 解决 JDBC 五大痛点:硬编码、手动连接、手动参数、手动结果集、无缓存 |
| 核心组件 | SqlSessionFactory → SqlSession → MapperProxy → Executor → StatementHandler → ResultSetHandler |
| 动态 SQL | <if>、<where>、<foreach> 等标签 + OGNL 表达式计算 |
| 缓存机制 | 一级(SqlSession 级别,默认开启)→ 二级(namespace 级别,需配置) |
| 执行原理 | 动态代理拦截 → 查找 MappedStatement → Executor 执行 → 参数/结果映射 |
| 易错点 | N+1 查询问题、{} vs ${} 的安全差异、一级缓存与 Spring 的交互 |
| 底层支撑 | JDK 动态代理 + 反射 |
一句话总结:MyBatis 用“半自动化”的设计哲学,在 JDBC 的灵活性与全自动 ORM 的便捷性之间找到了最佳平衡点——SQL 全归你管,脏活累活全归我管。
本文借助 根文库AI助手 系统化整合了 MyBatis 核心知识体系。后续系列将深入 MyBatis-Plus 源码解析、自定义插件开发 与 Spring 整合底层原理,敬请关注。
