讲一下Mybatis的插件原理

MyBatis 的插件机制是其核心扩展点之一,基于 动态代理责任链模式 实现,允许开发者在 SQL 执行的关键节点插入自定义逻辑(如性能监控、SQL 改写、数据加密等)。以下从原理到实践全面解析:

一、核心机制:动态代理 + 拦截器链

  1. 动态代理(JDK Proxy) MyBatis 通过动态代理为四大核心组件生成代理对象:

    • Executor:执行 SQL(增删改查、事务)

    • StatementHandler:处理 SQL 语法(预编译、参数设置)

    • ParameterHandler:处理 SQL 参数

    • ResultSetHandler:处理结果集映射插件通过代理对象拦截目标方法,在方法执行前后插入自定义逻辑。

  2. 拦截器链(InterceptorChain)

    • 所有配置的插件存储在 InterceptorChain 中,按配置顺序形成责任链。

    • 创建四大对象时,调用InterceptorChain.pluginAll(),对原始对象层层包装代理(类似“洋葱模型”):

      // 简化代码:Configuration 中创建 Executor
      public Executor newExecutor(Transaction tx) {
          Executor executor = new SimpleExecutor(this, tx);
          executor = interceptorChain.pluginAll(executor); // 生成代理链
          return executor;
      }

      每个插件的plugin()方法调用Plugin.wrap()生成代理对象。

  1. 拦截器接口(Interceptor) • 定义:插件需实现Interceptor接口,包含三个核心方法:

intercept(Invocation invocation):拦截目标方法,执行自定义逻辑。

plugin(Object target):生成代理对象,包装目标对象。

setProperties(Properties properties):设置插件属性(通过 XML 配置传递参数)。

• 作用:拦截器是插件的核心逻辑载体,决定何时触发拦截及如何处理。

二、可拦截的组件与方法

拦截点拦截场景典型方法
ExecutorSQL 执行流程(最常用)update(), query(), commit(), rollback()
StatementHandlerSQL 预编译与执行prepare(), parameterize()(设置参数)
ParameterHandler参数处理setParameters()(填充 PreparedStatement 参数)
ResultSetHandler结果集映射handleResultSets()(将 ResultSet 转为 Java 对象)

⚠️ 注意:仅能拦截这四类接口的方法,其他组件无法拦截。

三、插件实现:Interceptor 接口

自定义插件需实现 Interceptor 接口,并标注拦截签名:

@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 前置逻辑(如记录 SQL 开始时间)
        System.out.println("Before method: " + invocation.getMethod().getName());
        
        // 2. 执行原始方法(必须调用!)
        Object result = invocation.proceed();
        
        // 3. 后置逻辑(如计算 SQL 耗时)
        System.out.println("After method");
        return result;
    }
​
    @Override
    public Object plugin(Object target) {
        // 生成代理对象(固定写法)
        return Plugin.wrap(target, this);
    }
​
    @Override
    public void setProperties(Properties properties) {
        // 读取配置文件中的属性(可选)
    }
}
  • @Intercepts:声明拦截目标方法签名(类、方法名、参数类型)。

  • intercept():核心方法,通过Invocation.proceed()调用原始逻辑。

  • Plugin.wrap():检查目标对象是否匹配拦截条件,匹配则生成代理对象。

四、工作流程

  1. 初始化阶段:

    • 解析mybatis-config.xml中的<plugins>,将插件加入InterceptorChain

  2. 代理对象生成:

    • 创建四大对象时,调用interceptorChain.pluginAll(),按顺序应用所有插件的plugin()方法。

  3. 方法执行时:

    • 代理对象方法被调用 → 触发插件的intercept()→ 执行自定义逻辑 → 调Invocation.proceed()进入下一层代理或原始方法。

五、插件开发示例

  1. 日志记录插件

    @Intercepts({
      @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
    })
    public class SqlLogInterceptor implements Interceptor {
      @Override
      public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        String sql = ms.getBoundSql(parameter).getSql();
        System.out.println("Executing SQL: " + sql);
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        System.out.println("Execution time: " + (System.currentTimeMillis() - start) + "ms");
        return result;
      }
     
      @Override
      public Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    }
  2. 分页插件(简化版)

    @Intercepts({
      @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
    })
    public class PaginationInterceptor implements Interceptor {
      @Override
      public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        MappedStatement ms = handler.getMappedStatement();
        Object parameter = handler.getParameterHandler().getParameterObject();
     
        // 动态改写 SQL 添加分页逻辑(如 LIMIT/OFFSET)
        BoundSql boundSql = handler.getBoundSql();
        String originalSql = boundSql.getSql();
        String pageSql = originalSql + " LIMIT 0, 10"; // 示例分页
     
        // 通过反射修改 BoundSql 中的 SQL
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, pageSql);
     
        return invocation.proceed();
      }
    }

    六、应用场景总结

    场景拦截点典型功能关键 API
    SQL 性能监控StatementHandler.query/update记录执行时间、慢 SQL 告警System.currentTimeMillis()
    SQL 重写StatementHandler.prepare添加注释、分页优化、数据权限过滤BoundSql.getSql() + 反射修改
    参数预处理ParameterHandler.setParameters参数加密、格式转换PreparedStatement 设参
    结果集后处理ResultSetHandler.handleResultSets数据脱敏、缓存结果、日志统计强转结果为 List<?> 后处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tsxchen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值