1. MyBatis拦截器机制解析
1.1 拦截器在MyBatis中的定位
MyBatis拦截器是基于JDK动态代理实现的插件化扩展点,它允许我们在SQL执行生命周期的关键节点插入自定义逻辑。通过实现Interceptor接口并标注@Intercepts注解,开发者可以拦截四大核心组件的方法调用:
- Executor (update/query/commit/rollback)
- ParameterHandler (get/setParameterObject)
- ResultSetHandler (handleResultSets/handleOutputParameters)
- StatementHandler (prepare/parameterize/batch/update/query)
重要提示:拦截器执行顺序由
@Intercepts注解中@Signature定义的拦截目标决定,相同类型拦截器按配置顺序执行
1.2 典型拦截场景示例
@Intercepts({ @Signature(type= StatementHandler.class, method="prepare", args={Connection.class, Integer.class}) }) public class SlowQueryInterceptor implements Interceptor { private long threshold = 1000; @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long cost = System.currentTimeMillis() - start; if (cost > threshold) { StatementHandler handler = (StatementHandler) invocation.getTarget(); log.warn("Slow SQL detected: {} \nCost: {}ms", handler.getBoundSql().getSql(), cost); } } } }这个拦截器实现了慢SQL监控功能,通过计算StatementHandler.prepare()方法执行时间,超过阈值则输出警告日志。实际开发中我们常用类似机制实现:
- SQL审计日志
- 分表分库路由
- 敏感数据脱敏
- 动态参数改写
2. MyBatis完整启动流程剖析
2.1 配置加载阶段
graph TD A[SqlSessionFactoryBuilder.build] --> B[XMLConfigBuilder.parse] B --> C[解析mybatis-config.xml] C --> D[环境配置/数据源] C --> E[类型处理器注册] C --> F[插件注册] C --> G[Mapper接口扫描]配置文件解析:
- 优先加载
mybatis-config.xml中的全局配置 - 解析
<plugins>节点时,会实例化所有拦截器并存入InterceptorChain - 每个拦截器会通过
plugin()方法生成代理对象
- 优先加载
Mapper接口处理:
- 扫描
<mappers>配置的接口或XML文件 - 为每个Mapper接口创建
MapperProxyFactory - 动态生成Mapper接口的代理实现类
- 扫描
2.2 运行时拦截链构建
当创建SqlSession时,MyBatis会通过层层代理封装核心组件:
public class Configuration { public ParameterHandler newParameterHandler(...) { ParameterHandler handler = new DefaultParameterHandler(...); return (ParameterHandler) interceptorChain.pluginAll(handler); } // 同理处理其他三大组件 }代理生成过程示例:
- 原始对象:
new SimpleExecutor() - 经过拦截器A包装:
Plugin.wrap(executor, interceptorA) - 经过拦截器B包装:
Plugin.wrap(executor, interceptorB) - 最终形成代理链:B -> A -> 原始Executor
关键发现:拦截器包装顺序与配置文件中的声明顺序相反,最后配置的拦截器最先执行
3. 拦截器与SQL执行流程联动
3.1 查询操作全链路追踪
以SqlSession.selectList()为例的完整调用栈:
MapperProxy.invoke()- 触发Mapper方法调用CachingExecutor.query()- 二级缓存检查BaseExecutor.query()- 一级缓存检查拦截器前置处理- 如分页插件改写SQLStatementHandler.prepare()- 创建PreparedStatementParameterHandler.setParameters()- 参数绑定Statement.execute()- JDBC执行SQLResultSetHandler.handleResultSets()- 结果集映射拦截器后置处理- 如结果集脱敏
3.2 更新操作拦截要点
更新操作的特殊性在于需要处理事务:
@Intercepts({ @Signature(type= Executor.class, method="update", args={MappedStatement.class, Object.class}) }) public class AuditInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; if (ms.getSqlCommandType() == SqlCommandType.INSERT) { auditInsertOperation(invocation.getArgs()[1]); } return invocation.proceed(); } }典型应用场景:
- 数据变更审计
- 乐观锁版本号自增
- 全局租户ID注入
- 逻辑删除标记更新
4. 深度定制实践方案
4.1 动态表名拦截实现
实现动态表名替换的完整方案:
- 定义表名解析器:
public interface TableNameResolver { String resolve(String originTable, Object parameter); }- 实现StatementHandler拦截:
@Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); String newSql = replaceTableNames(boundSql.getSql(), boundSql.getParameterObject()); resetSql(handler, newSql); // 通过反射修改BoundSql.sql字段 return invocation.proceed(); }- 配置多数据源支持:
# 表名规则配置 table.rules.order=ds_${tenantId}.t_order_${month}4.2 性能监控集成方案
结合Micrometer实现全链路监控:
public class MetricsInterceptor implements Interceptor { private final Timer sqlTimer; public MetricsInterceptor(MeterRegistry registry) { this.sqlTimer = Timer.builder("mybatis.sql") .tags("component", "mybatis") .register(registry); } @Override public Object intercept(Invocation invocation) throws Throwable { return sqlTimer.record(() -> { // 记录方法签名作为tag String operation = invocation.getMethod().getName(); return invocation.proceed(); }); } }监控指标建议:
- SQL执行耗时分布
- 异常次数统计
- 并发执行数
- 结果集行数分布
5. 生产环境注意事项
5.1 线程安全实践
拦截器实例管理:
- 推荐方案:拦截器实现无状态设计
- 必须方案:成员变量使用ThreadLocal包装
- 错误示例:直接使用非线程安全集合
连接泄漏检测:
@Intercepts(@Signature(type=Executor.class, method="close")) public class ConnectionLeakInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 记录未关闭的Connection堆栈 ConnectionTracker.reportLeak(); return invocation.proceed(); } }5.2 常见问题排查指南
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 拦截器未生效 | 1. 未正确配置@Intercepts 2. 包扫描未包含拦截器类 | 1. 检查签名参数是否匹配 2. 确认组件被代理 |
| SQL改写异常 | 参数对象类型判断错误 | 使用ParameterMap精确匹配 |
| 性能下降明显 | 拦截器链过长或存在阻塞操作 | 异步化处理/合并拦截器 |
| 事务失效 | 拦截器未正确传递Connection | 确保调用invocation.proceed() |
5.3 最佳实践建议
拦截器设计原则:
- 单一职责:每个拦截器只处理一个关注点
- 快速失败:前置校验不通过应尽早抛出异常
- 轻量操作:避免在拦截器中执行耗时IO
调试技巧:
// 查看代理链结构 if (handler instanceof Plugin) { Plugin plugin = (Plugin) handler; System.out.println(plugin.getTarget()); }性能优化点:
- 高频拦截方法添加缓存
- 批量操作合并处理
- 使用@Signature精确限定拦截范围