Skip to content

MyBatis 插件机制

  MyBatis 插件(Interceptor)是一个强大的扩展机制,允许你在 SQL 执行过程中拦截并修改执行行为。PageHelper 分页插件就是最经典的应用。

一、插件原理

MyBatis 插件基于 责任链模式 + 动态代理

插件拦截的四大对象:
  Executor          → 执行器(update/query/commit/rollback)
  ParameterHandler  → 参数处理器(设置参数)
  ResultSetHandler  → 结果集处理器(处理查询结果)
  StatementHandler  → 语句处理器(构建 SQL 语句)
执行流程与拦截点:

  Executor

    ├── [拦截点 1] Executor.update/query


  StatementHandler

    ├── [拦截点 2] StatementHandler.prepare(创建 Statement)
    ├── [拦截点 3] StatementHandler.parameterize(设置参数)


  ParameterHandler

    ├── [拦截点 4] ParameterHandler.setParameters


  ResultSetHandler

    └── [拦截点 5] ResultSetHandler.handleResultSets

二、自定义插件

2.1 实现步骤

java
// 步骤一:实现 Interceptor 接口
@Intercepts({
    @Signature(
        type = StatementHandler.class,   // 拦截目标
        method = "prepare",              // 拦截方法
        args = {Connection.class, Integer.class}  // 方法参数类型
    )
})
public class SqlLogInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("========== 拦截开始 ==========");

        // 执行原始方法
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        long end = System.currentTimeMillis();

        System.out.println("SQL 执行耗时:" + (end - start) + "ms");
        System.out.println("========== 拦截结束 ==========");

        return result;
    }

    @Override
    public Object plugin(Object target) {
        // 用 Plugin.wrap 创建代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 读取插件配置参数
    }
}

步骤二:注册插件

xml
<plugins>
    <plugin interceptor="com.example.plugin.SqlLogInterceptor">
        <!-- 可选:配置参数 -->
        <property name="debug" value="true"/>
    </plugin>
</plugins>

2.2 拦截 SQL 并修改

java
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class SqlRewriteInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取 StatementHandler
        StatementHandler handler = (StatementHandler) invocation.getTarget();

        // 获取绑定的 SQL(通过反射)
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");

        // 修改 SQL
        String newSql = originalSql + " /* 添加的注释 */";
        metaObject.setValue("delegate.boundSql.sql", newSql);

        System.out.println("原始 SQL:" + originalSql);
        System.out.println("修改后 SQL:" + newSql);

        return invocation.proceed();
    }

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

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

2.3 分页拦截器(简化版)

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

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];

        // 如果没有分页参数,直接执行
        if (rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        }

        // 获取原始 SQL
        BoundSql boundSql = ms.getBoundSql(parameter);
        String originalSql = boundSql.getSql();

        // 拼接分页 SQL(MySQL 语法)
        String pageSql = originalSql + " LIMIT " + rowBounds.getOffset() + ", " + rowBounds.getLimit();

        // 替换 SQL
        MetaObject metaObject = SystemMetaObject.forObject(boundSql);
        metaObject.setValue("sql", pageSql);

        // 清空分页参数,避免重复处理
        args[2] = RowBounds.DEFAULT;

        return invocation.proceed();
    }

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

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

三、PageHelper 分页原理

PageHelper 是 MyBatis 最流行的分页插件,核心原理:

  ① 调用 PageHelper.startPage(pageNum, pageSize)
     → 通过 ThreadLocal 设置分页参数

  ② 拦截 Executor.query() 方法
     → 从 ThreadLocal 获取分页参数

  ③ 获取原始 SQL
     → 通过 BoundSql 获取

  ④ 拼接分页 SQL
     → MySQL:原始 SQL + LIMIT offset, limit
     → Oracle:嵌套查询 + ROWNUM

  ⑤ 执行分页 SQL 获取数据

  ⑥ 执行 COUNT 查询获取总数
     → 包装原始 SQL 为 SELECT COUNT(*) FROM (原始SQL) tmp

  ⑦ 封装 PageInfo 对象返回
java
// PageHelper 使用
PageHelper.startPage(1, 10);  // 第 1 页,每页 10 条
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);

System.out.println("总页数:" + pageInfo.getPages());
System.out.println("总条数:" + pageInfo.getTotal());
System.out.println("当前页:" + pageInfo.getPageNum());

四、多个插件的执行顺序

多个插件形成责任链:

  请求


  插件 A 拦截


  插件 B 拦截


  插件 C 拦截


  目标方法执行


  插件 C 后处理


  插件 B 后处理


  插件 A 后处理


  返回结果

执行顺序:按配置文件中 <plugins> 的顺序
如果多个插件拦截同一个方法,先配置的先执行前置处理,后执行后置处理

五、拦截点选择指南

拦截对象适用场景可拦截方法
Executor分页、缓存、事务update, query, flushStatements, commit, rollback
StatementHandlerSQL 改写、加密prepare, parameterize, batch, update, query
ParameterHandler参数加密、脱敏setParameters, getParameterObject
ResultSetHandler结果加密、脱敏handleResultSets, handleOutputParameters
java
// StatementHandler 拦截(最常用,可获取完整 SQL)
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})

// Executor 拦截(分页、缓存)
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})

// ResultSetHandler 拦截(结果处理)
@Intercepts({
    @Signature(
        type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}
    )
})

六、MetaObject(反射工具)

MyBatis 内置的反射工具类,用于操作对象的属性

常用方法:
  getValue("属性路径")    → 递归获取属性值
  setValue("属性路径", 值)  → 递归设置属性值
java
// 获取 BoundSql 中的 SQL
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
String sql = (String) metaObject.getValue("delegate.boundSql.sql");

// 获取参数值
Object parameterObject = metaObject.getValue("delegate.boundSql.parameterObject");

// 设置新 SQL
metaObject.setValue("delegate.boundSql.sql", newSql);

七、速记总结

插件核心三要素:
  ① 实现 Interceptor 接口
  ② @Intercepts + @Signature 注解指定拦截目标
  ③ 通过 Plugin.wrap() 创建代理

四大拦截对象:
  Executor          → 执行器(update/query/commit/rollback)
  ParameterHandler  → 参数处理器(setParameters)
  ResultSetHandler  → 结果集处理器(handleResultSets)
  StatementHandler  → 语句处理器(prepare/parameterize)

PageHelper 原理:
  ThreadLocal 存分页参数 → 拦截 Executor.query() → 改写 SQL 拼接 LIMIT

速记口诀:
  四大对象都能拦,注解指定要有三,
  type 定对象,method 定方法,args 定参数,
  Plugin.wrap 做代理,MetaObject 反射取。

八、面试要点

问题答案要点
插件原理?责任链 + 动态代理,拦截四大对象
四大拦截对象?Executor、StatementHandler、ParameterHandler、ResultSetHandler
PageHelper 原理?ThreadLocal 存分页参数,拦截 Executor.query() 改写 SQL
多插件执行顺序?按配置文件顺序,先配置的先执行