MyBatis 插件原理(Interceptor)

MyBatis 允许用插件(Interceptor)在 SQL 执行的关键节点"插入"自己的逻辑,PageHelper 分页、慢 SQL 日志、数据权限过滤都是这么实现的。想理解插件,先认识它要拦截的四大核心对象,再掌握注解声明的拦截点。

四大核心对象

MyBatis 执行一次 SQL 会依次经过四个对象(由 Configuration 创建):

对象职责典型方法
Executor执行器:调度缓存、事务与 SQL 执行(最顶层)update、query、commit、rollback
StatementHandler语句处理器:创建 PreparedStatement、设置参数、执行prepare、parameterize、update、query
ParameterHandler参数处理器:把参数绑定进 SQLsetParameters
ResultSetHandler结果处理器:ResultSet 转成 Java 对象handleResultSets

插件只能拦这四个对象,别的类(如 Mapper 接口)拦不了。

Interceptor 长什么样

实现 org.apache.ibatis.plugin.Interceptor 接口,重写三个方法,再用 @Intercepts + @Signature 声明"拦谁、拦哪个方法、方法参数长什么样":

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;

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

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return invocation.proceed();   // 放行,执行真正的 SQL
        } finally {
            System.out.println("耗时:" + (System.currentTimeMillis() - start) + " ms");
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);  // 标准写法:生成代理
    }

    @Override
    public void setProperties(Properties properties) {
        // 读取 <plugin> 下配置的 <property>
    }
}

注意:@Signature 里的方法参数列表必须与真实方法签名完全一致,否则拦截不生效。Executor.query 有多个重载,注册时参数类型顺序要核对(以官方文档/源码为准)。

注册插件

<plugins>
    <plugin interceptor="com.example.plugin.SqlLogPlugin">
        <property name="threshold" value="1000"/>
    </plugin>
</plugins>

Spring/Spring Boot 集成时通过 SqlSessionFactoryBean 的 plugins 属性注册(见第 15 章)。

工作原理

  1. MyBatis 创建四大对象时,会检查有没有匹配的插件。
  2. 有匹配则用 Plugin.wrap 生成 JDK 动态代理,把对象"包一层"。
  3. 多个插件时层层包裹成责任链:A 代理 → B 代理 → 真实对象
  4. 调用对象方法时,只有命中了 @Signature 声明的方法才进入拦截器;intercept 里调用 invocation.proceed() 放行给链上下一环。
mapper 调用 → Executor 代理 → 命中? → 你的 intercept() → proceed() → 真实 Executor

常用场景与忠告

  • 应用:物理分页(PageHelper 就是 Executor.query 拦截器)、SQL 审计、慢查询统计、逻辑删除改写。
  • 忠告:插件会改动 MyBatis 核心执行路径,拦截器越少越清晰;不要在拦截器里依赖内部类与未公开 API(升级版本易碎,行为以官方文档为准);要改写 SQL 需处理 BoundSql 与参数映射,属于高阶操作,务必充分测试。

小结

插件 = 拦截 Executor/StatementHandler/ParameterHandler/ResultSetHandler 四大对象的切面;@Intercepts/@Signature 精确声明拦截点,Plugin.wrap 生成代理、责任链放行。分页、日志、审计都建在这套机制上。

笔记加载中…