深入理解 MyBatis 插件机制:原理 + 源码解析 + 实战案例全掌握

共计 3634 个字符,预计需要花费 10 分钟才能阅读完成。

一、为什么要掌握 MyBatis 插件机制?

MyBatis 插件机制允许我们在不改动核心源码的情况下增强 SQL 执行过程,它是企业开发中解决以下问题的利器:

  • ✅ SQL 日志审计
  • ✅ 执行时间统计
  • ✅ SQL 动态加密/解密
  • ✅ 分页封装
  • ✅ 多租户支持

它类似于 Spring AOP,但作用范围仅限 MyBatis 支持的几个核心接口,适合用于非业务逻辑的数据层横切增强

二、插件原理与设计机制

2.1 插件必须实现 Interceptor 接口

你需要定义一个类实现 org.apache.ibatis.plugin.Interceptor 接口:

public interface Interceptor {

  /**
   * 子类拦截器必须要实现的方法,
   * 在该方法对内自定义拦截逻辑
   * @param invocation
   * @return
   * @throws Throwable
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   生成目标类的代理对象
   * 也可以根据需求不返回代理对象,这种情况下这个拦截器将不起作用
   * 无特殊情况使用默认的即可
   * @param target
   * @return
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);   // 这里是插件的入口,MyBatis 每次创建核心组件(如Executor、StatementHandler)时,都会调用 wrapAll(): 
  }

  /**
   * 设置变量
   * 在注册拦截器的时候设置变量,在这里可以获取到
   * @param properties
   */
  default void setProperties(Properties properties) {
    // NOP
  }

}

Plugin.wrap 本质:JDK 动态代理

2.2 插件的四种可拦截对象

MyBatis 设计中仅允许对以下 4 类接口的方法拦截:

类型 方法 说明
Executor query(), update() 执行器,负责增删改查
ParameterHandler getParameterObject(), setParameters() 参数映射
ResultSetHandler handleResultSets() 结果映射
StatementHandler prepare(), parameterize() SQL 预处理与参数绑定

使用 @Intercepts 注解精确声明你的拦截目标。

插件定义方式:

实现 org.apache.ibatis.plugin.Interceptor 接口,并使用 @Intercepts 注解标注要拦截的方法。

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
@Component
public class SqlPerformanceInterceptor implements Interceptor {
    // 插件逻辑略...
}

三、插件开发进阶实战技巧

案例拓展1:查询结果加解密插件

目标:自动解密数据库中的加密字段,如手机号字段。

@Intercepts({
    @Signature(
        type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}
    )
})
public class DecryptResultInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        if (result instanceof List<?>) {
            for (Object obj : (List<?>) result) {
                for (Field field : obj.getClass().getDeclaredFields()) {
                    if (field.getName().equals("phone")) {
                        field.setAccessible(true);
                        String encrypted = (String) field.get(obj);
                        field.set(obj, AESUtil.decrypt(encrypted));
                    }
                }
            }
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

案例拓展2:SQL 性能分析插件

目标:拦截查询 SQL,记录耗时超过阈值的慢 SQL 日志。

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {
            MappedStatement.class,
            Object.class,
            RowBounds.class,
            ResultHandler.class
        }
    )
})
public class SqlPerformanceInterceptor implements Interceptor {

    private long threshold = 500; // 毫秒

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long duration = System.currentTimeMillis() - start;

        if (duration > threshold) {
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            String sql = ms.getBoundSql(invocation.getArgs()[1]).getSql().replaceAll("\\s+", " ");
            System.err.printf("[慢SQL] [%dms] %s\nSQL: %s\n", duration, ms.getId(), sql);
        }

        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this); // 动态代理包裹
    }

    @Override
    public void setProperties(Properties properties) {
        if (properties.containsKey("threshold")) {
            this.threshold = Long.parseLong(properties.getProperty("threshold"));
        }
    }
}

插件之间传递数据的方式(插件上下文)

技巧一:ThreadLocal

public class PluginContext {
    public static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
}

技巧二:MetaObject(操作对象属性)

MetaObject metaObject = SystemMetaObject.forObject(resultObject);
metaObject.setValue("phone", AESUtil.decrypt(metaObject.getValue("phone")));

四、插件执行顺序控制

Spring 注入的顺序 == 插件的执行顺序

想显式控制顺序可使用 @Order 注解(或 Ordered 接口)

@Bean
@Order(1)
public Interceptor pluginA() {}

@Bean
@Order(2)
public Interceptor pluginB() {}

五、插件可能产生的问题

问题 原因与解决方案
插件无效 未注册为 Bean 或未使用 @Intercepts 注解
插件顺序影响执行逻辑 插件有状态,注册顺序不当
反射效率低 建议缓存反射字段,或使用 MetaObject 操作属性
插件方法拦截过多,影响性能 精准拦截必要接口,避免滥用
插件链不生效 被其他中间件(如分页插件)提前消费

六、总结

MyBatis 插件是强大但也容易踩坑的机制,理解其执行原理、注册方式与生命周期,是安全有效使用的基础。


如果本文对你有帮助,欢迎点赞、评论、收藏!我是 李卷卷,专注Java相关知识输出。感谢阅读!

正文完
 0
李卷卷
版权声明:本站原创文章,由 李卷卷 于2025-05-29发表,共计3634字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)