MyBatis执行流程与插件机制详解
1. 对应简历段落
这篇文章对应简历中“熟悉 MyBatis 执行流程,处理遗留 XML 兼容、自定义插件、分页改造和 SQL 性能治理”的项目经历。简历可以写成:
深入梳理核心系统 MyBatis 执行链路,完成 Mapper XML 加载校验、动态 SQL 兼容、分页插件顺序治理、自定义拦截器适配和慢 SQL 诊断,支撑 Spring Boot 迁移过程中数百个 Mapper 平稳运行。
面试官会从这段继续追问:Mapper 接口为什么不用实现类也能执行?SqlSession、Executor、StatementHandler、ParameterHandler、ResultSetHandler 分别做什么?插件到底拦截了哪个对象?为什么插件顺序会影响分页和数据权限?一级缓存和二级缓存在哪里生效?MyBatis 和 Spring 事务是怎么结合的?
这类问题不能只背源码类名。更好的回答方式是把一次查询从 Mapper 方法调用开始,讲到 SQL 解析、参数绑定、执行器调用、结果集映射、缓存处理和插件拦截。再结合项目说明为什么理解执行流程能解决真实问题,例如分页 SQL 改写、动态数据权限注入、SQL 日志脱敏、慢查询定位、迁移后 XML 未加载排查。
2. 业务背景
遗留保险系统中,MyBatis 往往承担最核心的数据访问职责。系统可能有几百个 Mapper XML,覆盖客户、保单、订单、产品、渠道、机构、佣金、回访、报表等模块。早期项目大量使用手写 SQL,这是 MyBatis 的优势:可以精细控制复杂查询、Oracle 方言、动态条件、批量操作和存储过程调用。但随着系统演进,问题也会累积。
第一,XML 数量多,命名空间、SQL id、ResultMap、TypeHandler、动态 SQL 依赖复杂。迁移到 Spring Boot 后,只要扫描路径、资源加载或 SqlSessionFactory 配置有差异,就可能出现运行时找不到 statement。第二,分页、数据权限、租户隔离、SQL 日志等横切能力通常靠插件实现,插件顺序一变,最终 SQL 就可能不同。第三,老系统常用 Oracle 特性,例如 ROWNUM、CONNECT BY、序列、存储过程、DECODE,迁移过程中需要保证 SQL 语义不变。第四,保险查询场景复杂,运营后台一个列表可能关联客户、保单、机构、产品、状态流水和标签表,慢 SQL 很常见。第五,事务问题经常被误判为 MyBatis 问题,例如同一事务内查询读到缓存、批量执行未 flush、异常被吞导致不回滚。
因此,理解 MyBatis 执行流程不是为了炫源码,而是为了在迁移和排障时知道每个问题应该从哪一层下手。找不到 SQL,要看 Mapper 注册和 MappedStatement;参数错位,要看 ParameterHandler 和动态 SQL;分页错误,要看插件拦截点和 BoundSql;结果映射异常,要看 ResultSetHandler、TypeHandler 和 ResultMap;事务不生效,要看 Spring 代理、SqlSession 绑定和事务管理器。
3. 核心原理
MyBatis 的核心对象可以按执行链路理解。
Configuration 是全局配置中心,保存环境、插件、类型别名、TypeHandler、Mapper 注册信息和所有 MappedStatement。每个 XML 中的 select、insert、update、delete 最终都会解析成一个 MappedStatement,里面包含 SQL 源、参数映射、结果映射、缓存配置、超时、语句类型等信息。
SqlSessionFactory 根据 Configuration 创建 SqlSession。在 Spring 集成场景中,开发者通常不直接管理 SqlSession,而是由 SqlSessionTemplate 代理。它会从 Spring 事务上下文中获取或创建 SqlSession,确保同一事务内复用同一个会话,并在事务结束时提交、回滚或关闭。
Mapper 接口通过 JDK 动态代理实现。MapperProxy 拦截接口方法调用,根据接口全限定名和方法名拼出 statement id,例如 com.xxx.PolicyMapper.selectById,再调用 SqlSession.selectOne、selectList、insert、update 或 delete。所以 Mapper 接口不需要实现类,真正的实现是 XML 或注解中的 SQL。
Executor 是执行器,负责缓存、事务相关的执行入口和数据库操作调度。常见实现有 SimpleExecutor、ReuseExecutor、BatchExecutor 和 CachingExecutor。一级缓存位于 SqlSession 级别,默认在同一个会话中生效;二级缓存位于 namespace 级别,需要显式开启,并通过 CachingExecutor 参与。
StatementHandler 负责创建 JDBC Statement、预编译 SQL、设置查询超时和分页参数等。ParameterHandler 负责把 Java 参数绑定到 PreparedStatement。ResultSetHandler 负责把 JDBC ResultSet 映射为 Java 对象。插件机制最常拦截的就是 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四类对象。
MyBatis 插件基于责任链和动态代理。插件实现 Interceptor 接口,通过 @Intercepts 和 @Signature 声明拦截哪个类型、哪个方法、哪些参数。MyBatis 在创建目标对象时调用 interceptorChain.pluginAll(target),每个插件都有机会包装目标对象。执行方法时进入插件的 intercept 方法,插件可以读取参数、改写 BoundSql、记录日志、加权限条件,然后调用 invocation.proceed() 继续链路。
4. 项目落地
在 Spring Boot 迁移项目中,MyBatis 落地首先要保证配置一致。旧系统可能在 XML 中配置了 typeAliases、typeHandlers、插件、驼峰映射、懒加载、缓存、数据库厂商标识和 Mapper 路径。迁移到 Boot Starter 后,不能只写一个 @MapperScan 就结束,而要逐项对齐。
第二,要建立 Mapper 加载校验。启动时可以遍历 Configuration#getMappedStatementNames(),统计加载的 statement 数量,并和迁移前基线对比。对于核心 Mapper,可以在集成测试中检查关键 statement 是否存在。这样能提前发现 XML 路径遗漏、namespace 写错、资源通配符不兼容等问题。
第三,要治理插件顺序。项目里可能同时存在分页插件、数据权限插件、SQL 日志插件、敏感字段脱敏插件和慢 SQL 插件。顺序不同会导致拦截到的 SQL 不同。例如数据权限应在分页改写前注入业务条件,否则分页插件可能先把 SQL 包成子查询,权限插件再追加条件就会出错。SQL 日志插件要明确记录原始 SQL、改写后 SQL,还是最终 JDBC 参数 SQL。
第四,要规范动态 SQL。老 XML 中常见 ${} 拼接排序字段、表名、机构条件,存在 SQL 注入风险。迁移时可以借机收敛:业务值使用 #{},白名单字段才允许 ${};复杂条件抽公共 sql 片段;分页和数据权限交给插件或统一查询组件处理,减少每个 XML 自己拼接。
第五,要结合慢 SQL 治理。MyBatis 插件可以记录 statement id、耗时、参数摘要、返回行数和 traceId,再进入 ELK。保险后台报表查询慢时,不能只看 SQL 文本,还要知道是哪一个 Mapper 方法、哪个渠道、哪个机构、哪个查询条件触发的。
5. 关键代码或流程
一个典型拦截器如下:
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class SqlTraceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
BoundSql boundSql = ms.getBoundSql(parameter);
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 1000) {
log.warn("slow sql, statementId={}, cost={}ms, sql={}",
ms.getId(), cost, normalize(boundSql.getSql()));
}
}
}
}
一次查询的流程可以概括为:
- 调用 Mapper 接口方法。
MapperProxy根据接口和方法名定位MappedStatement。SqlSessionTemplate获取事务绑定的SqlSession。Executor处理缓存并发起查询。StatementHandler准备 SQL 和 Statement。ParameterHandler绑定参数。- JDBC 执行 SQL。
ResultSetHandler映射结果。- 返回 Mapper 方法声明的对象类型。
Spring Boot 配置中要显式声明扫描路径:
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.company.insurance.domain
type-handlers-package: com.company.insurance.mybatis.handler
configuration:
map-underscore-to-camel-case: true
default-executor-type: simple
启动校验示例:
@Component
public class MyBatisStatementReporter implements ApplicationRunner {
private final SqlSessionFactory sqlSessionFactory;
public MyBatisStatementReporter(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public void run(ApplicationArguments args) {
int count = sqlSessionFactory.getConfiguration().getMappedStatementNames().size();
log.info("MyBatis mapped statement count={}", count);
}
}
6. 常见坑
第一个坑是 Mapper XML 没加载但启动不报错。只有调用到对应方法时才暴露,所以迁移时要做 statement 基线校验。
第二个坑是插件拦截点选错。分页通常拦截 StatementHandler.prepare 或 Executor.query,数据权限可能需要改写 BoundSql,慢 SQL 统计拦截 Executor 更自然。拦错层会导致拿不到足够上下文。
第三个坑是 BoundSql 改写不完整。只改 SQL 字符串,不处理 parameterMappings 或 additionalParameters,动态 SQL 中的参数可能绑定失败。
第四个坑是滥用二级缓存。保险业务数据状态变化频繁,如果缓存失效策略不清晰,容易读到旧状态。多数交易链路不建议随意开启二级缓存。
第五个坑是 ${} 拼接。排序字段、动态表名、权限条件如果没有白名单,会带来 SQL 注入风险。
第六个坑是批处理误解。BatchExecutor 需要 flush,异常定位也更复杂,不适合随便作为全局默认执行器。
7. 面试追问
- Mapper 接口为什么没有实现类也能执行?
MappedStatement保存了哪些信息?- MyBatis 一级缓存什么时候生效,什么时候清空?
- 插件可以拦截哪些核心对象?
StatementHandler和ParameterHandler分别做什么?- 分页插件为什么常常需要改写 BoundSql?
- 插件顺序为什么会影响最终 SQL?
- MyBatis 和 Spring 事务如何绑定同一个
SqlSession? #{}和${}的区别是什么?- 迁移后某个 Mapper 找不到 statement,你怎么排查?
8. 推荐回答
如果问“MyBatis 一次查询怎么执行”,可以回答:
Mapper 接口调用会进入
MapperProxy,它根据接口全限定名和方法名定位MappedStatement,然后通过SqlSession调用 Executor。Executor 先处理缓存,再委托 StatementHandler 创建和预编译 Statement,ParameterHandler 负责参数绑定,JDBC 执行后由 ResultSetHandler 做对象映射。Spring 场景下,SqlSessionTemplate会把 SqlSession 和事务上下文绑定,保证同一事务内会话一致。
如果问“MyBatis 插件机制怎么实现”,可以回答:
插件实现
Interceptor,通过@Signature声明拦截 Executor、StatementHandler、ParameterHandler 或 ResultSetHandler 的具体方法。MyBatis 创建这些核心对象时会经过interceptorChain.pluginAll,插件用动态代理包装目标对象。方法执行时进入intercept,可以读取 MappedStatement、BoundSql 和参数,也可以改写 SQL 或记录日志,最后调用invocation.proceed()继续执行链路。
如果问“迁移时 MyBatis 最容易出什么问题”,可以回答:
最常见是 XML 没加载、插件顺序变化、TypeHandler 缺失和动态 SQL 行为不一致。我会先对齐
mapper-locations、typeAliasesPackage、typeHandlersPackage和 MyBatis configuration,再启动时统计 mapped statement 数量。插件方面会明确顺序,比如数据权限先于分页,日志明确记录改写后 SQL。核心查询用生产脱敏请求做结果对比。
9. 延伸学习路线
第一阶段掌握基础使用,包括 Mapper XML、动态 SQL、ResultMap、TypeHandler、批量操作和 Spring Boot Starter 配置。
第二阶段阅读执行链路源码,重点看 MapperProxy、SqlSessionTemplate、Executor、StatementHandler、ParameterHandler、ResultSetHandler。
第三阶段深入插件机制,自己实现慢 SQL、分页、数据权限或审计插件,理解 BoundSql、MappedStatement 和参数映射。
第四阶段学习事务和缓存,弄清一级缓存、二级缓存、Spring 事务绑定、批处理 flush 和回滚行为。
第五阶段结合真实业务做 SQL 治理,掌握索引、执行计划、分页优化、动态条件优化和可观测日志。资深面试中,能把源码、插件和业务排障连起来讲,含金量最高。