MyBatis插件开发指南:拦截原理与实战应用
MyBatis插件开发深度解析:从拦截原理到实战应用
MyBatis插件机制是其架构中最具扩展性的部分之一,通过拦截器(Interceptor)我们可以对MyBatis的核心组件进行功能增强。本文将深入剖析插件实现原理,并展示典型应用场景的实现方案。
一、Interceptor核心原理
1.1 拦截点与责任链模式
MyBatis允许在4个核心组件上设置拦截点:
拦截执行顺序:Executor → StatementHandler → ParameterHandler → ResultSetHandler
1.2 注解式拦截配置
通过@Intercepts
和@Signature
定义拦截目标:
@Intercepts({
@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class QueryInterceptor implements Interceptor {
// 实现方法...
}
参数说明:
type
:拦截的接口类型method
:拦截的方法名args
:方法参数类型列表(用于精确匹配重载方法)
1.3 插件加载机制
MyBatis通过InterceptorChain
管理所有插件,形成代理链:
// 简化后的核心处理逻辑
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
实践建议:
- 拦截器应按功能顺序配置(如:先分页后缓存)
- 避免在拦截器中修改输入参数,可能导致链式调用异常
二、典型插件实现方案
2.1 分页插件实现
以简化版PageHelper为例:
public class PageInterceptor implements Interceptor {
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (LOCAL_PAGE.get() != null) {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 修改SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String newSql = boundSql.getSql() + " LIMIT ?, ?";
resetSql(ms, boundSql, newSql);
// 修改参数
Object newParam = wrapParameter(parameter);
args[1] = newParam;
}
return invocation.proceed();
}
// 其他工具方法...
}
关键点:
- 使用ThreadLocal保持分页参数线程安全
- 通过反射修改BoundSql中的SQL内容
- 重组参数对象添加分页偏移量
2.2 SQL监控插件
实现执行时间统计与慢SQL告警:
public class SqlMonitorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 1000) { // 慢SQL阈值
log.warn("Slow SQL detected: {}ms", cost);
}
// 记录到监控系统...
}
}
}
增强建议:
- 添加SQL指纹(去除参数值后的SQL)
- 集成Micrometer上报指标
- 区分读写操作分别统计
2.3 动态数据源路由
基于注解的读写分离实现:
@Intercepts(@Signature(type = Executor.class, method = "update", args = {...}))
public class DataSourceRouterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) {
Method method = getMethod(invocation);
if (method.isAnnotationPresent(ReadOnly.class)) {
DynamicDataSourceHolder.markRead();
} else {
DynamicDataSourceHolder.markWrite();
}
return invocation.proceed();
}
}
路由策略扩展:
- 按方法名前缀路由(如findXXX走从库)
- 基于事务注解判断
- 表级别分库策略
三、高级开发技巧
3.1 元数据访问技巧
通过MappedStatement
获取SQL信息:
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Configuration config = ms.getConfiguration();
SqlSource sqlSource = ms.getSqlSource();
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
String originalSql = boundSql.getSql();
3.2 插件执行顺序控制
- 配置文件中声明顺序决定包装顺序
- 使用
@AutoConfigureAfter
(Spring环境下) - 通过
@Order
注解调整优先级
3.3 性能优化建议
- 避免在拦截器中创建大量临时对象
- 高频调用方法中使用缓存(如SQL解析结果)
- 减少反射操作,必要时缓存Method对象
四、常见问题排查
问题1:插件不生效
- 检查是否配置到mybatis-config.xml
- 确认@Signature匹配了正确的方法签名
- 验证插件被正确加载(查看日志)
问题2:出现重复代理
- 检查是否重复配置相同插件
- 确认插件链没有循环调用
问题3:修改SQL无效
- BoundSql可能是不可变对象,需要重置
- 注意参数位置与SQL占位符对应关系
五、最佳实践总结
- 单一职责:每个插件只处理一个特定功能
- 轻量级:避免在插件中实现复杂业务逻辑
- 可观测:添加足够的日志和监控指标
- 文档完善:明确插件的使用约束和兼容性说明
通过合理使用插件机制,可以在不改动MyBatis源码的情况下实现各种增强功能,但需注意过度使用会影响代码可维护性。建议优先考虑是否可以通过其他方式(如AOP)实现相同功能。