Skip to content

简述 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 的方法名/参数签名必须精确匹配,配错静默不生效——没有报错,最容易踩。