面试追问地图

主问题必讲关键点下一层追问
#{} vs ${}PreparedStatement、字符串替换动态列名如何防注入
@Mapper vs @Repository代理生成、异常转换、@MapperScan 原理如何免标 @Mapper、PersistenceExceptionTranslationPostProcessor
执行流程MapperProxy、SqlSession、Executor、Handler插件在哪一层拦截
一级缓存SqlSession 级、本地 Map更新后何时清空、跨会话为何无效
二级缓存namespace 级、序列化多表和多实例一致性
动态 SQLif/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 吗?→ 可以。@MapperScanannotationClass 默认是 Mapper.class,但可以改为 @Repository.class 或留空扫所有接口;SpringBoot 中也可以只在启动类上加 @MapperScan 扫整个 Mapper 包,类上免标 @Mapper

二、执行流程

MyBatis 执行一条 SQL 的流程?

难度 🔴

Mapper 接口代理

MapperMethod

SqlSession

Executor

StatementHandler

ParameterHandler / ResultSetHandler

JDBC
  1. SqlSessionFactory 根据配置创建 SqlSession
  2. MapperProxy 将 Mapper 接口方法转换为 MappedStatement 调用。
  3. Executor 负责缓存、事务和 SQL 执行调度。
  4. StatementHandler 创建并执行 JDBC Statement。
  5. ParameterHandler 设置参数。
  6. ResultSetHandler 将结果集映射为 Java 对象。

MyBatis 中 Executor 有哪些类型?

类型特点
SimpleExecutor每次执行都创建新的 Statement
ReuseExecutor复用相同 SQL 的 Statement
BatchExecutor批量执行更新语句,统一刷新
CachingExecutor装饰器,负责二级缓存

批量执行需要关注错误定位、事务大小和 JDBC 驱动配置,不能只看代码调用次数。


三、缓存机制

一级缓存和二级缓存的区别?

难度 🟡

一级缓存二级缓存
范围SqlSessionMapper 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 长度和执行计划,必要时分批或使用临时表。

resultTyperesultMap 的区别?

  • resultType:适合字段和属性可直接映射的简单结果。
  • resultMap:适合字段改名、嵌套对象、一对一和一对多映射。

复杂关联应警惕 N+1 查询。可以使用 JOIN 一次查询、批量查询后组装,或明确控制延迟加载。


五、插件与工程实践

MyBatis 插件原理?

难度 🔴

MyBatis 通过 JDK 动态代理拦截四类核心对象:

  • Executor
  • StatementHandler
  • ParameterHandler
  • ResultSetHandler

插件使用 @Intercepts@Signature 声明拦截目标,可以实现分页、SQL 改写、审计和性能监控。插件会进入执行主链,必须控制耗时并避免修改错误的 BoundSql。

分页插件的基本原理?

拦截查询流程,根据数据库方言:

  1. 生成统计总数 SQL。
  2. 给原 SQL 增加 LIMIT/OFFSET 等分页语句。
  3. 重新绑定参数并执行。

深分页仍然可能扫描和丢弃大量记录,不能仅依赖分页插件,应考虑游标或基于索引的 seek 分页。

MyBatis-Plus 和 MyBatis 的关系?

MyBatis-Plus 是 MyBatis 的增强工具,提供通用 CRUD、条件构造器、分页和代码生成等能力,不替代 MyBatis。

使用时需要注意:

  • 复杂 SQL 仍应明确编写和评审。
  • 条件构造器不能代替索引和执行计划分析。
  • 自动填充、逻辑删除和租户插件会隐式改写 SQL。

Mapper 方法传多个参数为什么常用 @Param

@Param 为参数提供稳定名称,便于 XML 和注解 SQL 引用。否则参数名可能依赖编译选项,或只能通过 param1arg0 等名称访问,降低可读性和稳定性。


六、MyBatis-Spring 集成

MyBatis 是如何整合进 Spring 的?

频次 ★★★★ · 难度 🔴

MyBatis-Spring 是 MyBatis 和 Spring 之间的桥梁,核心靠三个 SPI 对象把 MyBatis 的组件挂接到 Spring 容器中:

1. SqlSessionFactoryBean — 创建 SqlSessionFactory

实现 FactoryBean<SqlSessionFactory>,在 getObject() 中完成:

  1. 读取 Configuration(XML 或 Java 配置类)
  2. 扫描 Mapper XML 文件路径注册 MappedStatement
  3. 注册类型别名和类型处理器
  4. 设置插件(分页、审计等 Interceptor)
  5. 返回配置好的 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(),且没有事务管理
  • SqlSessionTemplateSqlSessionManager 的区别?→ SqlSessionTemplate 是 MyBatis-Spring 提供的,必须配 Spring 事务管理器;SqlSessionManager 是 MyBatis 原生提供的,只能在单线程中串行使用
  • 为什么 MyBatis-Spring 是专门一个 jar 包而不是 MyBatis 内置功能?→ 因为 MyBatis 框架本身不假设任何 IoC 容器,集成 Spring 需要的 FactoryBeanBeanDefinitionRegistryPostProcessor 都是 Spring SPI,是 MyBatis 适配 Spring,而不是 Spring 适配 MyBatis——同一思路看 RocketMQRedis 的 Spring Boot Starter