简述 MyBatis 的插件运行原理,以及如何编写一个插件?
- 标签:后端、MyBatis
🏛 业务架构师
MyBatis 插件的本质是对四大核心对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)做 JDK 动态代理拦截:实现 Interceptor 接口,@Signature 声明拦截哪个对象的哪个方法,MyBatis 在创建这些对象时用 Plugin.wrap 把拦截器织成代理链,方法调用时依次穿过。
编写三步:实现 org.apache.ibatis.plugin.Interceptor(intercept 写增强逻辑、plugin 决定是否代理、setProperties 拿配置)→ @Intercepts/@Signature 声明拦截点 → 注册(mybatis-config 或 Spring Boot 的 @Bean)。
我真正想讲的是插件的业务用途——它是公司级数据治理的标准挂载点:
- 分页:PageHelper 就是插件实现,SQL 自动改写加 limit——全员免写分页 SQL;
- 慢 SQL 打标与审计:拦截 query/update,记录执行耗时、SQL 指纹、调用方,慢 SQL 治理的数据源;
- 数据权限/多租户:按租户自动拼
tenant_id = ?,业务代码零感知——SaaS 业务的安全底线靠它收口,而不是靠每个开发记得写条件(总有忘了的); - 加解密与脱敏:写入自动加密、查询自动解密,手机号、身份证字段的合规要求在基础设施层落地;
- 读写分离/影子库:压测流量自动路由影子表。
风险提示:插件是全局代理,写错影响所有 SQL——必须放在公司统一的 starter 里灰度发布、有开关可降级;插件链每层都有微小开销,数量要克制。插件用好了是治理杠杆,用散了是全局隐患,我的原则是"插件只进 starter,不进业务代码"。
🏗 系统架构师
MyBatis 允许拦截四大对象的代理,插件本质是 责任链 + JDK 动态代理:
可拦截点:
- Executor(update/query,SQL 执行总入口);
- StatementHandler(语句预处理、参数设置);
- ParameterHandler(参数写入);
- ResultSetHandler(结果集映射)。
运行原理:配置解析时,每个插件按声明顺序生成层层嵌套的代理对象(责任链),调用 target 方法时先进入各插件的 intercept(),调用 invocation.proceed() 放行到下一层代理,最终到达真实对象。@Signature 注解声明要拦截的接口方法。
写一个插件(以慢 SQL 监控为例):
java
@Intercepts(@Signature(type = Executor.class,
method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class SlowSqlPlugin implements Interceptor {
public Object intercept(Invocation inv) throws Throwable {
long t = System.currentTimeMillis();
try { return inv.proceed(); }
finally { logCost(inv, System.currentTimeMillis() - t); }
}
}再注册为 Bean 即生效(老版本放 mybatis-config)。
工程落地:PageHelper(改写 SQL 加 limit)、分库分表(ShardingSphere 的 SQL 改写)、审计与数据权限注入,都构建在这套机制上——它是 MyBatis 留给架构师的 AOP 扩展点。
💻 全栈开发专家
原理:MyBatis 允许拦截四大对象的指定方法,拦截器本质是责任链 + JDK 动态代理层层包裹:
text
Executor(增改查入口,缓存/事务)
StatementHandler(SQL 编译与执行)
ParameterHandler(参数填充)
ResultSetHandler(结果映射)插件注册时 InterceptorChain.pluginAll() 把目标对象逐层代理,调用时先过我的 intercept(),再 invocation.proceed() 放行给下一个。
写一个 SQL 耗时监控插件:
java
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class}))
@Component
public class SqlCostPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
StatementHandler sh = (StatementHandler) invocation.getTarget();
if (cost > 500) {
log.warn("slow sql {}ms: {}", cost,
sh.getBoundSql().getSql());
}
}
}
}实际使用的现成插件:PageHelper(分页,改写 SQL + 拦截 Executor)、通用字段自动填充(create_time)。
踩坑:一个对象可插多个插件,顺序会影响执行链;@Signature 的方法名/参数签名必须精确匹配,配错静默不生效——没有报错,最容易踩。