MyBatis 面试连环炮:从源码原理到实战避坑,彻底拿下 Offer 通关秘籍
导读:在Java后端面试中,MyBatis 往往是面试官手中的一把“双刃剑”。初级开发者只会 CRUD,而高级开发者则需要深谙其缓存机制、插件原理和动态代理。本文将带你从面试现场的“连环追问”切入,结合核心源码与实战场景,助你彻底征服这一板块。
无论是初级工程师还是高级架构师,MyBatis 都是 Java 后端开发中绕不开的核心组件。面试官对它的考察早已不再局限于“怎么写 SQL”,而是深入到了 SQL 注入防护、缓存穿透/脏读、分页插件底层原理 等核心深度。
为了帮你“吊打”面试官,本文将从以下 5 个维度为你深度拆解:
-
1. #{}与${}的生死抉择 —— 防止 SQL 注入的底线 -
2. 动态 SQL 的优雅与陷阱 —— 什么时候该用 where,什么时候必须用trim -
3. 缓存失效的真相 —— 一级缓存与二级缓存的爱恨情仇 -
4. 分页插件的黑魔法 —— 物理分页 vs 逻辑分页的性能博弈 -
5. Mapper 接口的秘密 —— 为什么没有实现类也能运行?
🔍 第一回合:面试现场 —— #{} 与 ${} 的区别
🎯 面试官提问
“我们在写 SQL 时,
#{}和${}到底有什么区别?什么时候必须用${}?”
💡 核心解析
这不仅仅是语法的区别,更是安全与性能的区别。
1. 核心对比表
|
|
#{}
|
${}
|
|---|---|---|
| 处理机制 | 预编译 (Prepared Statement)
|
字符串替换
|
| SQL 注入 |
|
|
| 执行性能 |
|
|
| 类型转换 |
|
|
| 典型场景 |
|
|
2. 深度场景剖析:为什么有时候非得用 ${}?
虽然 #{} 很安全,但在某些场景下它是“无能为力”的。
-
• 场景一:动态表名如果你的业务需要根据时间分表(如 user_2024,user_2025),表名是 SQL 的结构部分,预编译占位符?不允许出现在表名位置。<!-- 必须使用 $ {} --><select id="findUserByTable" resultType="User"> SELECT * FROM $ {tableName} WHERE id = #{id}</select>避坑指南:如果必须用
${},请务必在 Java 代码层做白名单校验,绝对不能直接拼接用户输入! -
• 场景二:动态排序 (ORDER BY) <!-- 动态按不同列排序 --><select id="findUser" resultType="User"> SELECT * FROM user ORDER BY $ {sortColumn} $ {sortOrder}</select>
🪄 第二回合:动态 SQL —— 让代码更聪明
🧩 核心标签实战
MyBatis 的动态 SQL 是基于 OGNL 表达式实现的,它能让你的 XML 像编程语言一样灵活。
1. 解决“多余关键字”的神器:<where> 与 <set>
-
• 痛点:在拼接 AND或OR时,很容易出现语法错误(如WHERE AND name = 'xxx')。 -
• 方案:使用 <where>标签,它会智能判断:如果内部标签没有返回任何内容,它就不生成WHERE子句;如果第一个条件带AND,它会自动去除。
<select id="findUser" resultType="User"> SELECT * FROM user <!-- <where> 会自动处理第一个 and/or --> <where> <if test="name != null and name != ''"> AND name = #{name} </if> <if test="age != null"> AND age = #{age} </if> </where></select>
2. 批量操作:<foreach>
这是面试中考察并发和性能的常见点。
<!-- IN 查询 --><select id="selectByIds" resultType="User"> SELECT * FROM user WHERE id IN <foreach collection="list" item="id" open="(" separator="," close=")"> #{id} </foreach></select><!-- 批量插入 --><insert id="batchInsert"> INSERT INTO user (name, age) VALUES <foreach collection="list" item="user" separator=","> (#{user.name}, #{user.age}) </foreach></insert>
🧠 第三回合:缓存机制 —— 一级与二级缓存的博弈
⚖️ 缓存对比全景图
|
|
|
|
|---|---|---|
| 作用域 | SqlSession
|
Mapper
|
| 默认状态 |
|
|
| 数据共享 |
|
SqlSession 共享 (应用级) |
| 脏读风险 |
|
有
|
| 底层实现 | PerpetualCache
|
PerpetualCache
|
🚨 经典面试题:二级缓存的“脏读”怎么解决?
场景模拟:假设你有两个 Mapper:
-
1. UserMapper:查询用户信息。 -
2. UserRoleMapper:负责给用户分配角色(更新操作)。
问题:UserMapper 开启了二级缓存。当 UserRoleMapper 修改了用户的角色后,UserMapper 的缓存并没有失效!下次查询用户时,读到的还是旧的角色信息 —— 这就是脏读。
解决方案:
-
1. 方案 A (粗暴) :直接禁用二级缓存(推荐在分布式环境下使用 Redis 替代)。 -
2. 方案 B (优雅) :使用 <cache-ref>标签,让两个 Mapper 引用同一个缓存区域。<!-- 在 UserRoleMapper.xml 中添加 --><cache-ref namespace="com.demo.mapper.UserMapper"/>这样,当
UserRoleMapper执行增删改时,会刷新UserMapper的缓存,从而保持一致性。
📄 第四回合:分页插件原理 —— 拒绝内存溢出
📊 分页方式大比拼
|
|
|
|
|
|---|---|---|---|
| 逻辑分页 | RowBounds
|
|
极慢且危险
|
| 物理分页 | LIMIT
ROWNUM |
高性能
|
|
🛠️ PageHelper 插件是如何工作的?
PageHelper 的核心原理是利用了 MyBatis 的 Interceptor (拦截器) 机制。
执行流程图解:
-
1. 入口: PageHelper.startPage(1, 10)—— 将分页参数存入ThreadLocal(保证线程安全)。 -
2. 拦截:拦截 Executor.query()方法。 -
3. 改写:从 ThreadLocal取出参数,将原 SQL 改写为带LIMIT的物理分页 SQL。 -
4. 统计:自动生成并执行 COUNT(*)查询,获取总记录数。 -
5. 封装:将数据和总数封装成 PageInfo对象返回。
避坑指南:
PageHelper依赖ThreadLocal,如果在startPage后执行了多个查询,后面的查询会被“污染”。最佳实践是紧跟着startPage写唯一的查询语句,或者手动调用clearPage()。
🧙♂️ 第五回合:Mapper 映射原理 —— 无中生有的实现类
🤔 灵魂拷问
“为什么我们的 Mapper 只是一个接口,没有任何实现类,却能直接注入并调用?”
⚙️ 底层真相:JDK 动态代理
MyBatis 在启动时,会扫描所有的 Mapper 接口,并利用 JDK 动态代理 为它们生成代理对象(Proxy)。
调用链路:
-
1. 解析:解析 XML 或注解,生成 MappedStatement对象,并存入Configuration。 -
2. 代理:当调用 sqlSession.getMapper(UserMapper.class)时,生成MapperProxy。 -
3. 执行:调用 userMapper.selectById(1)时,代理对象会根据 接口全限定名 + 方法名 拼接成statementId(如com.demo.mapper.UserMapper.selectById)。 -
4. 映射:根据 statementId从Configuration中找到对应的 SQL 和参数,执行数据库操作。
为什么 Mapper 接口不能重载方法?因为 statementId 仅由 接口名 + 方法名 组成,不包含参数列表。如果重载,会导致多个方法对应同一个 statementId,从而引发冲突。
📝 总结与避坑指南
为了方便记忆,我为你整理了这份面试速记表:
|
|
|
|
|---|---|---|
| 参数占位符 |
|
${},但要防注入 |
| 一级缓存 |
|
|
| 二级缓存 |
|
<cache-ref> 解决脏读 |
| 分页插件 |
|
RowBounds 导致的内存溢出 |
| 动态代理 |
|
|
📚 关注《卷毛的技术笔记》
👋 我是卷毛,一名热爱分享技术干货的后端工程师。
在这里,你将获得:
• 硬核实战:拒绝空谈,只讲生产环境能落地的架构方案。 • 避坑指南:我踩过的坑,帮你填平。 • 面试突击:大厂高频面试题深度解析。 关注我,带你少加班,多升职!
夜雨聆风