面试追问地图
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
#{} vs ${} | PreparedStatement、字符串替换 | 动态列名如何防注入 |
| @Mapper vs @Repository | 代理生成、异常转换、@MapperScan 原理 | 如何免标 @Mapper、PersistenceExceptionTranslationPostProcessor |
| 执行流程 | MapperProxy、SqlSession、Executor、Handler | 插件在哪一层拦截 |
| 一级缓存 | SqlSession 级、本地 Map | 更新后何时清空、跨会话为何无效 |
| 二级缓存 | namespace 级、序列化 | 多表和多实例一致性 |
| 动态 SQL | if/where/foreach | 超大 IN、空集合、SQL 可读性 |
| resultMap | 字段映射、关联和集合 | N+1、JOIN 结果去重 |
| 插件 | 四大对象、动态代理 | 多插件顺序、BoundSql 修改风险 |
| 分页 | count + 方言改写 | 深分页、游标分页 |
| 批处理 | BatchExecutor、flushStatements | 事务大小、部分失败如何处理 |
| MyBatis-Spring 集成 | SqlSessionFactoryBean、MapperFactoryBean、SqlSessionTemplate | @MapperScan 源码、线程安全的 SqlSession |
回答 ORM 题要回到底层 JDBC,说明 MyBatis 帮你封装了什么、没有替你解决什么。
一、MyBatis 核心
# 和 $ 的区别?
#{} | ${} | |
|---|---|---|
| SQL 类型 | 预编译 SQL(? 占位符) | 普通 SQL 字符串拼接 |
| 安全性 | 防止 SQL 注入 | 不防 SQL 注入 |
| 性能 | 预编译,执行效率高 | 每次编译 |
| 用途 | 参数值 | 表名、列名等动态 SQL |
MyBatis 的优势
- SQL 与代码解耦,写在 XML 中便于管理和优化
- 动态 SQL 支持(
<if>、<foreach>等标签) - 自动映射 + 自定义 resultMap
- 插件扩展机制(分页、SQL 改写等)
JDBC 连接步骤
加载驱动 → 建立连接 → 创建 Statement → 执行 SQL → 处理 ResultSet → 关闭资源(ResultSet → Statement → Connection)
@Mapper 和 @Repository 的区别?
频次 ★★★★ · 难度 🟡
@Mapper | @Repository | |
|---|---|---|
| 来源 | MyBatis 自有注解 | Spring 注解(stereotype 体系,同 @Service/@Controller) |
| 作用 | 标识映射器接口,配合 @MapperScan 生成代理 | 标记 DAO 层,让 Spring 自动扫描并注册 Bean |
| 代理生成 | MapperScannerConfigurer 扫描 @Mapper 后通过 MapperFactoryBean 创建 JDK 动态代理 | ClassPathBeanDefinitionScanner 扫到后注册普通 BeanDefinition,真正代理由 MapperFactoryBean 创建(需额外依赖 MyBatis-Spring) |
| 异常转换 | 不处理 | 配合 PersistenceExceptionTranslationPostProcessor 将 MyBatis 异常转为 Spring 的 DataAccessException 体系 |
| 是否必须 | 必须(框架要求) | 可选(Spring 管理的辅助标注) |
为什么常一起用?只用 @Mapper 时 MyBatis 的 org.apache.ibatis.binding.BindingException 等异常直接抛到上层;加上 @Repository 后 Spring 的 PersistenceExceptionTranslationPostProcessor 会拦截 DAO 层的异常并翻译为 DataAccessException 体系,便于和 JPA/JDBC 等其他数据访问技术统一异常处理。所以常见写法是 @Repository + @Mapper 同时标注。
常见追问
@MapperScan是什么原理?→ 注册MapperScannerConfigurer,它实现BeanDefinitionRegistryPostProcessor,在容器启动时扫描指定包下带@Mapper的接口,逐个注册为MapperFactoryBean类型的 BeanDefinition- 不加
@Mapper能用@MapperScan吗?→ 可以。@MapperScan的annotationClass默认是Mapper.class,但可以改为@Repository.class或留空扫所有接口;SpringBoot 中也可以只在启动类上加@MapperScan扫整个 Mapper 包,类上免标@Mapper
二、执行流程
MyBatis 执行一条 SQL 的流程?
难度 🔴
Mapper 接口代理
↓
MapperMethod
↓
SqlSession
↓
Executor
↓
StatementHandler
↓
ParameterHandler / ResultSetHandler
↓
JDBCSqlSessionFactory根据配置创建SqlSession。MapperProxy将 Mapper 接口方法转换为MappedStatement调用。Executor负责缓存、事务和 SQL 执行调度。StatementHandler创建并执行 JDBC Statement。ParameterHandler设置参数。ResultSetHandler将结果集映射为 Java 对象。
MyBatis 中 Executor 有哪些类型?
| 类型 | 特点 |
|---|---|
SimpleExecutor | 每次执行都创建新的 Statement |
ReuseExecutor | 复用相同 SQL 的 Statement |
BatchExecutor | 批量执行更新语句,统一刷新 |
CachingExecutor | 装饰器,负责二级缓存 |
批量执行需要关注错误定位、事务大小和 JDBC 驱动配置,不能只看代码调用次数。
三、缓存机制
一级缓存和二级缓存的区别?
难度 🟡
| 一级缓存 | 二级缓存 | |
|---|---|---|
| 范围 | SqlSession | Mapper namespace |
| 默认状态 | 默认开启 | 需要显式开启 |
| 生命周期 | 会话结束后失效 | 可跨 SqlSession |
| 数据结构 | 本地 Map | 可扩展缓存实现 |
一级缓存可能导致同一会话内读到旧值;执行更新、提交、回滚或清空缓存会使相关缓存失效。
二级缓存使用要谨慎:
- 多表关联查询很难精确失效。
- 分布式环境下本地二级缓存无法天然保持一致。
- 对一致性敏感的数据更适合显式缓存方案。
为什么生产项目经常不用 MyBatis 二级缓存?
因为它按 namespace 管理,缓存失效粒度较粗。复杂业务中一张表可能被多个 Mapper 修改,容易出现一致性问题。Redis 等独立缓存虽然也需要一致性治理,但可观测性、容量和跨实例共享能力更强。
四、动态 SQL 与结果映射
常用动态 SQL 标签有哪些?
<if>:按条件拼接。<choose>/<when>/<otherwise>:多分支选择。<where>:自动处理WHERE和前导AND/OR。<set>:更新语句中自动处理逗号。<foreach>:批量参数和IN查询。<sql>/<include>:复用 SQL 片段。
大集合使用 IN 时要关注数据库参数上限、SQL 长度和执行计划,必要时分批或使用临时表。
resultType 和 resultMap 的区别?
resultType:适合字段和属性可直接映射的简单结果。resultMap:适合字段改名、嵌套对象、一对一和一对多映射。
复杂关联应警惕 N+1 查询。可以使用 JOIN 一次查询、批量查询后组装,或明确控制延迟加载。
五、插件与工程实践
MyBatis 插件原理?
难度 🔴
MyBatis 通过 JDK 动态代理拦截四类核心对象:
ExecutorStatementHandlerParameterHandlerResultSetHandler
插件使用 @Intercepts 和 @Signature 声明拦截目标,可以实现分页、SQL 改写、审计和性能监控。插件会进入执行主链,必须控制耗时并避免修改错误的 BoundSql。
分页插件的基本原理?
拦截查询流程,根据数据库方言:
- 生成统计总数 SQL。
- 给原 SQL 增加
LIMIT/OFFSET等分页语句。 - 重新绑定参数并执行。
深分页仍然可能扫描和丢弃大量记录,不能仅依赖分页插件,应考虑游标或基于索引的 seek 分页。
MyBatis-Plus 和 MyBatis 的关系?
MyBatis-Plus 是 MyBatis 的增强工具,提供通用 CRUD、条件构造器、分页和代码生成等能力,不替代 MyBatis。
使用时需要注意:
- 复杂 SQL 仍应明确编写和评审。
- 条件构造器不能代替索引和执行计划分析。
- 自动填充、逻辑删除和租户插件会隐式改写 SQL。
Mapper 方法传多个参数为什么常用 @Param?
@Param 为参数提供稳定名称,便于 XML 和注解 SQL 引用。否则参数名可能依赖编译选项,或只能通过 param1、arg0 等名称访问,降低可读性和稳定性。
六、MyBatis-Spring 集成
MyBatis 是如何整合进 Spring 的?
频次 ★★★★ · 难度 🔴
MyBatis-Spring 是 MyBatis 和 Spring 之间的桥梁,核心靠三个 SPI 对象把 MyBatis 的组件挂接到 Spring 容器中:
1. SqlSessionFactoryBean — 创建 SqlSessionFactory
实现 FactoryBean<SqlSessionFactory>,在 getObject() 中完成:
- 读取
Configuration(XML 或 Java 配置类) - 扫描 Mapper XML 文件路径注册
MappedStatement - 注册类型别名和类型处理器
- 设置插件(分页、审计等 Interceptor)
- 返回配置好的
SqlSessionFactory
2. MapperFactoryBean — 将 Mapper 接口转为 Spring Bean
每个 Mapper 接口对应一个 MapperFactoryBean,它同样实现 FactoryBean 接口,getObject() 返回 JDK 动态代理(MapperProxy)。代理对象注入到所有需要该 Mapper 的 Spring Bean 中——这就是为什么 @Autowired private UserMapper userMapper 拿到的是 MyBatis 代理。
3. SqlSessionTemplate — 线程安全的 SqlSession 替代品
原生 MyBatis 的 SqlSession 不是线程安全的(它与当前连接绑定在 ThreadLocal)。SqlSessionTemplate 封装了每次从 SqlSessionFactory 获取新 SqlSession、使用后关闭的逻辑,保证每个 DAO 方法都在自己的 SqlSession 中执行。它也是 Spring 事务管理的关键——事务开启时,TransactionSynchronizationManager 会把当前 SqlSession 绑定到当前线程,SqlSessionTemplate 检查到线程已绑定就直接复用,实现事务传播。
三者的装配链路:
@MapperScan
↓ 注册
MapperScannerConfigurer
↓ 扫描 @Mapper 接口
ClassPathMapperScanner
↓ 为每个接口创建 BeanDefinition(beanClass = MapperFactoryBean)
MapperFactoryBean.getObject()
↓
MapperProxy(JDK 动态代理)
↓ 方法调用时委托给
SqlSessionTemplate
↓
SqlSession(线程安全使用)常见追问
- 没有 MyBatis-Spring 能用 Mapper 吗?→ 可以,但每个 Mapper 方法前要手动
sqlSessionFactory.openSession()→session.getMapper()→ 用完session.close(),且没有事务管理 SqlSessionTemplate和SqlSessionManager的区别?→SqlSessionTemplate是 MyBatis-Spring 提供的,必须配 Spring 事务管理器;SqlSessionManager是 MyBatis 原生提供的,只能在单线程中串行使用- 为什么 MyBatis-Spring 是专门一个 jar 包而不是 MyBatis 内置功能?→ 因为 MyBatis 框架本身不假设任何 IoC 容器,集成 Spring 需要的
FactoryBean、BeanDefinitionRegistryPostProcessor都是 Spring SPI,是 MyBatis 适配 Spring,而不是 Spring 适配 MyBatis——同一思路看 RocketMQ 和 Redis 的 Spring Boot Starter