MyBatis面试要点:从Mapper代理、执行流程到缓存和插件的系统梳理
前言
MyBatis 面试题通常会从“它和 Hibernate/JPA 有什么区别”开始,继续追问 Mapper 接口为什么不用实现类,SQL 是怎么执行的,#{} 和 ${} 有什么区别,一级缓存和二级缓存怎么生效,动态 SQL 如何拼接,插件拦截哪些对象,分页插件原理是什么,懒加载为什么可能产生 N+1 问题。
如果只把 MyBatis 理解成“写 SQL 的 ORM 框架”,回答很容易停留在使用层。更好的方式是把它理解成一条完整链路:
1 | 调用 Mapper 接口 -> 动态代理生成调用 -> 找到 MappedStatement -> Executor 执行 -> StatementHandler 创建 SQL -> JDBC 查询 -> ResultSetHandler 映射结果 |
本文按面试中最常见的模块梳理 MyBatis 要点,适合作为面试前的复习清单。
MyBatis 整体定位
MyBatis 是一个半自动 ORM 框架。它不像 Hibernate 那样尽量屏蔽 SQL,而是让开发者显式编写 SQL,同时提供参数绑定、结果映射、动态 SQL、缓存和插件扩展能力。
面试中可以这样概括:
MyBatis 的核心价值是把 Java 方法调用和 SQL 执行建立映射关系。开发者控制 SQL,MyBatis 负责 Mapper 代理、参数处理、JDBC 执行、结果集映射和扩展机制。
核心组件
MyBatis 常见核心组件包括:
- SqlSessionFactory:创建 SqlSession 的工厂。
- SqlSession:执行 SQL 和获取 Mapper 的入口。
- MapperProxy:Mapper 接口动态代理。
- MappedStatement:一条 SQL 映射语句的元数据。
- Executor:执行器,负责查询、更新、缓存和事务协调。
- StatementHandler:创建和执行 JDBC Statement。
- ParameterHandler:处理 SQL 参数。
- ResultSetHandler:处理结果集映射。
- TypeHandler:Java 类型和 JDBC 类型转换。
- Configuration:全局配置对象。
flowchart TD
A[Mapper Interface] --> B[MapperProxy]
B --> C[MappedStatement]
C --> D[Executor]
D --> E[StatementHandler]
E --> F[ParameterHandler]
E --> G[JDBC Statement]
G --> H[ResultSetHandler]
H --> I[Java Object]
Mapper 代理原理
Mapper 接口没有实现类也能调用,是因为 MyBatis 为 Mapper 接口创建了 JDK 动态代理。
常见调用方式:
1 | UserMapper mapper = sqlSession.getMapper(UserMapper.class); |
大致流程:
getMapper返回 Mapper 接口代理对象。- 调用接口方法时进入
MapperProxy。 - 根据接口全限定名和方法名找到对应
MappedStatement。 - 判断方法类型是 select、insert、update 还是 delete。
- 调用 SqlSession 执行 SQL。
- 把结果转换为方法返回类型。
SQL id 通常是:
1 | namespace + "." + methodName |
所以 XML 的 namespace 要和 Mapper 接口全限定名对应,SQL 标签 id 要和方法名对应。
SQL 执行流程
MyBatis 查询执行流程可以概括为:
sequenceDiagram
participant App
participant MapperProxy
participant SqlSession
participant Executor
participant StatementHandler
participant DB
participant ResultSetHandler
App->>MapperProxy: selectById(id)
MapperProxy->>SqlSession: selectOne(statement, param)
SqlSession->>Executor: query()
Executor->>StatementHandler: prepare / parameterize
StatementHandler->>DB: execute SQL
DB-->>StatementHandler: ResultSet
StatementHandler->>ResultSetHandler: handleResultSets
ResultSetHandler-->>App: User
关键点:
- MapperProxy 负责把方法调用转换为 statement 调用。
- Executor 负责执行和缓存。
- StatementHandler 负责 JDBC Statement 创建和执行。
- ParameterHandler 负责参数绑定。
- ResultSetHandler 负责结果映射。
#{} 和 ${}
#{} 和 ${} 是 MyBatis 高频面试题。
#{} 使用 PreparedStatement 参数占位:
1 | select * from user where id = #{id} |
最终类似:
1 | select * from user where id = ? |
优点是防 SQL 注入,并能进行类型处理。
${} 是字符串直接拼接:
1 | select * from user order by ${column} |
它不会做预编译参数绑定,存在 SQL 注入风险。
常见原则:
- 普通参数值使用
#{}。 - 表名、列名、排序字段这类 SQL 结构片段如果必须动态拼接,才使用
${}。 - 使用
${}时必须做白名单校验。
动态 SQL
MyBatis 动态 SQL 让 SQL 可以根据条件拼接。
常见标签:
ifchoose / when / otherwisewheresetforeachtrim
示例:
1 | <select id="listUsers" resultType="User"> |
where 标签会自动处理多余的 and 或 or,避免拼接出非法 SQL。
foreach 常用于 in 查询:
1 | <foreach collection="ids" item="id" open="(" separator="," close=")"> |
ResultMap
ResultMap 用于处理结果集到对象的映射。
常见场景:
- 字段名和属性名不一致。
- 一对一关联。
- 一对多集合。
- 嵌套查询。
- 嵌套结果映射。
示例:
1 | <resultMap id="userMap" type="User"> |
如果只是下划线转驼峰,可以开启:
1 | mybatis: |
复杂关联查询要警惕结果行膨胀和 N+1 查询问题。
一级缓存
MyBatis 一级缓存默认开启,作用域是 SqlSession。
同一个 SqlSession 中,执行相同 SQL 和相同参数时,可能直接命中一级缓存。
特点:
- 默认开启。
- SqlSession 级别。
- update、insert、delete 会清空缓存。
- commit、rollback、close 后缓存失效。
常见问题:为什么一级缓存可能读到旧数据?
如果同一个 SqlSession 中先查询,再由其他事务修改了数据库,本 SqlSession 再次查询相同语句可能命中缓存,读到旧结果。通常 Web 应用中 SqlSession 生命周期较短,风险相对可控。
二级缓存
二级缓存作用域是 Mapper namespace。
特点:
- 默认不开启。
- 需要全局开启,并在 Mapper 中配置 cache。
- 多个 SqlSession 可以共享。
- 事务提交后才会写入二级缓存。
- 更新操作会清空对应 namespace 缓存。
启用示例:
1 | <cache/> |
二级缓存面试中要谨慎回答:它不是所有场景都适合。对于频繁更新、多表关联、强一致要求高的业务,二级缓存容易带来一致性问题。实际项目中更常用 Redis 等外部缓存,并显式设计缓存更新策略。
Executor
MyBatis 常见 Executor 类型:
- SimpleExecutor:每次执行都创建新的 Statement。
- ReuseExecutor:复用 Statement。
- BatchExecutor:批处理执行更新。
Executor 负责:
- 执行查询和更新。
- 管理一级缓存。
- 协调事务提交回滚。
- 创建 StatementHandler。
批量插入、批量更新场景可以使用批处理能力,但要注意批量大小、事务提交和数据库压力。
插件机制
MyBatis 插件基于拦截器实现,可以拦截四类核心对象:
- Executor
- StatementHandler
- ParameterHandler
- ResultSetHandler
常见用途:
- 分页。
- SQL 日志。
- 慢 SQL 监控。
- 数据权限。
- 多租户。
- 字段加解密。
插件本质上通过动态代理包装目标对象,在目标方法执行前后插入逻辑。
分页插件常拦截 StatementHandler 或 Executor,在 SQL 执行前改写 SQL,追加 limit,并执行 count 查询。
分页
MyBatis 本身不强制提供物理分页,常见方式:
- 手写
limit offset, size。 - 使用 PageHelper 等分页插件。
- 业务层基于游标或主键分页。
深分页问题:
1 | select * from orders order by id limit 1000000, 20 |
数据库需要扫描并丢弃大量数据,性能较差。
优化方式:
- 使用主键游标分页。
- 使用覆盖索引。
- 限制最大页数。
- 搜索类场景使用搜索引擎。
例如:
1 | select * from orders |
懒加载和 N+1
MyBatis 支持关联对象懒加载。懒加载可以减少不必要查询,但也可能导致 N+1 问题。
例如查询 100 个用户,再懒加载每个用户的订单:
1 | 1 次查询用户列表 + 100 次查询订单 = 101 次 SQL |
优化方式:
- 使用 join 一次查出。
- 分两次批量查询,再在内存中组装。
- 控制懒加载使用范围。
- 通过 SQL 日志发现异常查询次数。
面试中要强调:懒加载不是万能优化,使用不当会把问题从单条 SQL 变成大量小 SQL。
事务
MyBatis 可以独立管理事务,也可以交给 Spring 管理。
在 Spring 项目中,常见方式是通过 @Transactional 管理事务,MyBatis 的 SqlSession 和数据库连接会绑定到当前事务上下文。
常见事务问题:
- 方法没有被 Spring 代理。
- 同类内部调用导致事务失效。
- 异常被捕获未抛出。
- 默认 checked exception 不回滚。
- 多数据源事务未正确配置。
- 批处理时未控制提交边界。
MyBatis 本身执行 SQL,事务边界通常由 Spring 事务管理器控制。
MyBatis 和 JPA 的区别
| 对比项 | MyBatis | JPA/Hibernate |
|---|---|---|
| SQL 控制 | 手写 SQL,控制力强 | 自动生成 SQL 为主 |
| 学习成本 | 相对直接 | 对 ORM 状态管理要求高 |
| 复杂查询 | 适合手写优化 | 复杂查询可能难调优 |
| 对象关系映射 | 半自动 | 更完整的 ORM |
| 性能可控性 | SQL 可控,便于优化 | 依赖 ORM 生成和调优 |
| 适合场景 | 复杂 SQL、报表、业务查询 | 领域模型清晰、CRUD 较多 |
面试中可以总结:MyBatis 更适合 SQL 可控性要求高的项目,JPA 更适合领域模型驱动和标准化 CRUD 较多的项目。
常见性能优化
SQL 层:
- 使用合适索引。
- 避免
select *。 - 避免大范围深分页。
- 避免循环中查询数据库。
- 慢 SQL 使用执行计划分析。
MyBatis 层:
- 合理使用 ResultMap。
- 谨慎使用懒加载。
- 批量操作控制批量大小。
- 避免不必要的二级缓存。
- 插件不要做过重逻辑。
应用层:
- 控制事务范围。
- 避免长事务。
- 数据库连接池设置合理。
- 热点数据使用外部缓存。
- 关键 SQL 打印耗时和参数。
常见排查思路
如果面试官问“MyBatis 查询慢怎么排查”,可以按以下路径回答:
- 打开 SQL 日志,确认真实执行 SQL 和参数。
- 看是否存在 N+1 查询或循环查库。
- 用数据库执行计划检查索引命中情况。
- 看是否使用了深分页、模糊前缀、函数包裹索引列。
- 看返回字段和结果集是否过大。
- 看 ResultMap 是否过于复杂。
- 看分页插件是否额外执行了慢 count。
- 看连接池是否耗尽、事务是否过长。
如果是 Mapper 找不到 SQL,可以重点检查 namespace、方法名、XML 是否被扫描、参数名是否匹配、mapper-locations 配置是否正确。
高频面试题
Mapper 接口为什么不用实现类?
因为 MyBatis 会为 Mapper 接口创建 JDK 动态代理。调用接口方法时,代理根据接口名和方法名找到对应 MappedStatement,再通过 SqlSession 执行 SQL。
#{} 和 ${} 有什么区别?
#{} 使用预编译参数占位,能防 SQL 注入;${} 是字符串拼接,适合表名、列名等 SQL 结构动态拼接,但必须做白名单校验。
一级缓存和二级缓存有什么区别?
一级缓存是 SqlSession 级别,默认开启;二级缓存是 namespace 级别,默认不开启,可以跨 SqlSession 共享,但要注意数据一致性问题。
MyBatis 插件能拦截什么?
能拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四类对象。分页插件、SQL 日志、数据权限、多租户常用插件机制实现。
分页插件原理是什么?
分页插件通常拦截 SQL 执行过程,在真正执行前改写 SQL,追加数据库方言的分页语句,并可能额外执行 count SQL 统计总数。
MyBatis 如何防止 SQL 注入?
普通参数使用 #{},不要用 ${} 拼接用户输入。必须动态拼接表名、字段名、排序字段时,要做白名单校验。
为什么会出现 N+1 查询?
通常是关联对象懒加载或循环中查询导致的。查询一个列表后,又为列表中每条记录单独查询关联数据,最终产生大量 SQL。可以用 join、批量查询或手动组装优化。
总结
MyBatis 面试的主线可以围绕四个问题展开:
- 方法怎么变成 SQL:Mapper 代理、MappedStatement、SqlSession。
- SQL 怎么执行:Executor、StatementHandler、ParameterHandler、ResultSetHandler。
- 性能怎么治理:动态 SQL、分页、缓存、懒加载、N+1。
- 线上怎么排查:真实 SQL、执行计划、连接池、事务、插件和日志。
把这条链路讲清楚,再结合 #{}/${}、缓存、插件、分页和慢 SQL 排查,MyBatis 相关问题就能从“会写 Mapper”升级成“理解 SQL 映射框架运行机制”。


