Mybatis的插件原理

Mybatis的插件原理

一句话总结

MyBatis 插件本质是基于拦截器的动态代理:开发者实现 Interceptor 接口并声明要拦截的目标(ExecutorStatementHandler 等),MyBatis 在创建这些核心组件时通过责任链模式套上一层层代理,方法执行时依次经过所有插件增强逻辑,最后再调用原始方法。常用于 SQL 改写、分页、日志、审计、加解密等横切功能。

详细解析

MyBatis 的插件机制是其核心扩展点之一,通过拦截 + 动态代理 + 责任链把横切逻辑从业务代码里抽离出来,非常适合做 AOP 风格的增强。


一、插件运行原理的核心架构

  1. 拦截器接口 Interceptor

    • 插件必须实现 org.apache.ibatis.plugin.Interceptor 接口,包含三个核心方法:
    java 复制代码
    public interface Interceptor {
        Object intercept(Invocation invocation) throws Throwable;   // 真正的拦截逻辑
    
        Object plugin(Object target);                               // 为目标对象生成代理
    
        void setProperties(Properties properties);                  // 读取配置属性
    }
    • 职责
      • intercept:你要插入的自定义逻辑(打印 SQL、改写 SQL、统计耗时等)写在这里;
      • plugin:通常直接调用 Plugin.wrap(target, this),返回代理;
      • setProperties:读取 mybatis-config.xml<property> 传入的参数(如日志级别、开关等)。
  2. 动态代理机制

    • MyBatis 使用 JDK 动态代理 为四大对象生成代理:
      • Executor
      • StatementHandler
      • ParameterHandler
      • ResultSetHandler
    • 这里的意思可以理解成:
      • 原来你直接用的是"真实对象"(比如某个 Executor 实现类),现在 MyBatis 在外面再套一层"代理对象";
      • 以后你的代码拿到的不是原始 Executor,而是它的"代理壳";
      • 调用过程(invoke → 多个 intercept)可以想象成这样:
        1. 你调用 executor.update(...)
        2. 实际上先调用到的是"代理对象"的 invoke(...) 方法;
        3. 这个 invoke 里面,MyBatis 会把方法、参数等封装成一个 Invocation 对象,然后按顺序把这个 Invocation 传给每一个插件的 intercept(Invocation)
        4. 每个插件的 intercept 里可以先做自己的逻辑(改 SQL、打日志、统计耗时、做分页等),然后调用 invocation.proceed() 把调用继续传下去;
        5. 当所有插件都执行完并都调用了 proceed() 之后,才真正执行到底层原始 Executorupdate 方法。
      • 方法不会直接打到真实对象,而是先经过一层代理,在代理的 invoke 方法里,MyBatis 挨个调用所有插件的 intercept,组成一条调用链。
    • 一句话:动态代理 = 在目标对象外面套一层壳,所有方法调用先经过这层壳,让插件有机会"插一脚"。
  3. 插件链(InterceptorChain,责任链模式)

    • MyBatis 会根据 <plugins> 配置顺序,将多个插件依次包装 到目标对象外层,形成一条调用链:
      • 可以想象成给目标对象一层一层"套娃":最先配置的插件在最外面,后配置的在里面,最中心才是真实对象;
      • 调用方法时,先进入最外层插件的 intercept ,它在合适的时候调用 invocation.proceed() 把调用"传下去"给下一层插件;
      • 下一层插件同样先执行自己的 intercept,再通过 proceed() 继续往里传;
      • 一直传到最里面的真实对象,才真正执行原始方法,然后一层一层返回结果。
    • 那句"最外层插件先执行 → 调 invocation.proceed() → 交给下一个插件 → 直到最内层原始对象"就是说的这个从外到内、逐层传递调用 的过程;这就是经典的责任链模式 ,可以让多个插件按顺序协作增强同一个方法调用

二、插件执行流程(从配置到生效)

  1. mybatis-config.xml 中注册插件
xml 复制代码
<plugins>
  <plugin interceptor="com.example.mybatis.LoggingInterceptor">
    <property name="logLevel" value="DEBUG"/>
  </plugin>
</plugins>
  1. 通过注解声明拦截点
java 复制代码
@Intercepts({
    @Signature(
        type = Executor.class,                     // 要拦截的接口
        method = "update",                         // 要拦截的方法
        args = {MappedStatement.class, Object.class} // 方法参数列表
    )
})
public class LoggingInterceptor implements Interceptor {
    // ...
}
  1. MyBatis 初始化时包装目标对象

    • 启动 / 创建 SqlSessionFactory 过程中,MyBatis 会:
      • 读取 <plugins> 配置,创建各个 Interceptor 实例;
      • 针对上述四大核心接口,依次调用每个插件的 plugin(target) 方法;
      • 如果当前插件声明要拦截这个 target,就用 Plugin.wrap 返回代理,否则直接返回原对象。
  2. 执行 SQL 时的调用顺序

    • 业务代码调用 SqlSession.update() / select()
    • 调用链进入最外层插件代理对象的 invoke
    • 触发该插件的 intercept(Invocation)
    • 插件内部可以:
      • 读取 / 修改参数:invocation.getArgs()
      • 在调用前后做日志、计时、监控;
      • 决定是否、何时调用 invocation.proceed()
    • invocation.proceed() 会把调用交给下一个插件或最终原始对象。

三、插件开发示例

  1. SQL 日志与耗时统计插件
java 复制代码
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
    ),
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.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];
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql();

        long start = System.currentTimeMillis();
        System.out.println("[MyBatis] SQL: " + sql);

        Object result = invocation.proceed();

        long cost = System.currentTimeMillis() - start;
        System.out.println("[MyBatis] Cost: " + cost + " ms");
        return result;
    }

    @Override
    public Object plugin(Object target) {
        // 统一交给 MyBatis 提供的 Plugin 工具来创建代理
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以从 properties 中读取自定义配置,比如日志级别
    }
}
  1. 简化版分页插件示意
java 复制代码
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class SimplePaginationInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();

        String originalSql = boundSql.getSql();
        // 示例:强行拼一个 LIMIT 子句,真实场景会从 ThreadLocal / 参数中取 pageNum、pageSize
        String pageSql = originalSql + " LIMIT 0, 10";

        // 通过反射修改 BoundSql 中的 sql 字段
        Field field = BoundSql.class.getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, pageSql);

        return invocation.proceed();
    }

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

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

上面只是原理 Demo,真正生产级分页通常使用 PageHelper、MyBatis-Plus 等成熟插件。


四、常见插件应用场景

场景 拦截接口 / 方法 典型插件 / 功能
SQL 日志记录 Executor.update / Executor.query 自定义日志插件、p6spy、Logback 适配
分页查询 StatementHandler.prepare PageHelper、MyBatis-Plus 分页插件
性能监控 包裹所有 SQL 执行接口 统计耗时、慢查询告警
数据加密/解密 ParameterHandler.setParameters / ResultSetHandler 字段加解密、脱敏
审计与埋点 Executor.update / commit / rollback 记录操作人、时间、数据快照

五、面试回答模板(可直接复述)

  • 问:MyBatis 插件原理是什么?能拦截哪些地方?
    • 答:"MyBatis 插件基于拦截器 + JDK 动态代理 + 责任链模式。我们实现 Interceptor 接口并用 @Intercepts / @Signature 声明要拦截的接口和方法,MyBatis 在创建 ExecutorStatementHandlerParameterHandlerResultSetHandler 时按 <plugins> 配置顺序一层层用 Plugin.wrap 包装成代理对象。调用这些方法时会依次进入各个插件的 intercept,在里面可以做 SQL 改写、日志、分页、加解密等增强逻辑,最后通过 invocation.proceed() 调回原始方法。"
相关推荐
诚思报告YH2 小时前
视频面试软件市场洞察:2026 - 2032年复合年均增长率(CAGR)为10.3%
面试·职场和发展
绝无仅有2 小时前
计算机网络核心面试知识深入解析
后端·面试·架构
Lee川2 小时前
从异步探索者到现代信使:JavaScript数据请求的进化之旅
javascript·面试
NEXT063 小时前
React 核心揭秘:虚拟 DOM 原理与 Diff 算法深度解析
前端·react.js·面试
李云龙炮击平安线程3 小时前
Python中的接口、抽象基类和协议
开发语言·后端·python·面试·跳槽
Moment4 小时前
此 KFC 不是肯德基,Kafka、Flink、ClickHouse 怎么搭、何时省掉 Flink
前端·后端·面试
绝无仅有4 小时前
Java多线程并发问题解决方案全解析
后端·面试·架构
Fox爱分享4 小时前
字节三面:千万级订单对账,怎么保证“一分钱不错”?答不出“流式比对+缓冲池”,基本就挂了
面试·程序员·架构
Fox爱分享4 小时前
拼多多面试: 设计“砍一刀”算法,怎么防止被刷破产?90% 的人死在了“最后 0.01 元”
后端·算法·面试