Mybatis插件原理及分页插件

Mybatis框架允许用户通过自定义拦截器来改变SQL的执行行为,例如在SQL执行追加分页语句、统计SQL执行耗时等。自定义拦截器也被称为Mybatis插件,插件是 MyBatis 扩展核心功能的常用方式。

注:本文中源码来自mybatis 3.4.x版本,地址https://github.com/mybatis/mybatis-3.git

一 插件实现原理

1.1 配置插件信息

在Mybatis主配置文件中,通过<plugins>标签来注册自定义插件。如

XML 复制代码
<plugins>
    <plugin interceptor="com.example.MyCustomPlugin">
        <!-- 可选:传递属性给插件
        <property name="name" value="page"/>
    </plugin>
</plugins>

在XMLConfigBuilder#parseConfiguration中,会解析plugins标签内容,创建Interceptor并注册到Configuration的InterceptorChain interceptorChain。

1.2 声明拦截器

MyBatis 插件本质是基于动态代理实现的,只能拦截四大核心对象的方法:

  • Executor:执行器(负责 SQL 执行、缓存管理)
  • StatementHandler:SQL 语句处理器(负责 SQL 构建、参数设置)
  • ParameterHandler:参数处理器(负责参数解析)
  • ResultSetHandler:结果集处理器(负责结果集映射)

为什么是这4大组件呢?Configuration作为这些组件的创建工厂,会应用拦截器并返回代理对象(以StatementHandler为例)

自定义插件需要实现Interceptor接口

其中,Invocation封装了目标对象、方法及参数信息,通过它来获取上述4个组件对象的所有信息。

java 复制代码
public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;
  // 回调原方法
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }
}

1.3 创建代理对象

为了方便创建Executor等的代理对象,Mybatis提供了一个Plugin工具类。关键代码如下:

Plugin类实现了InvocationHandler接口,即采用JDK动态代理来创建代理对象。当Executor执行某个方法时,如果方法被指定需要增强,就会先执行Interceptor#intercept方法。

Plugin#getSignatureMap方法,会解析Interceptor实现类的Intercepts注解信息,得到拦截范围。

java 复制代码
// @Intercepts 注解:指定拦截的对象和方法(核心)
@Intercepts({
    // @Signature:单个拦截规则
    // type:拦截的核心对象(Executor/StatementHandler 等)
    // method:拦截的方法名(比如 Executor 的 query 方法)
    // args:拦截方法的参数类型(用于区分重载方法)
    @Signature(
        type = Executor.class,
        method = "query",
        args = {org.apache.ibatis.mapping.MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class MyCustomPlugin implements Interceptor {
}

1.4 自定义插件示例

我们只需要实现Interceptor接口,在intercept()中编写增强逻辑,通过plugin()返回一个代理对象,就能自定义一个Mybatis插件了。

java 复制代码
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {org.apache.ibatis.mapping.MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class MyCustomPlugin implements Interceptor {
    private String name;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        System.out.println("SQL 执行参数:" + args[1]);
        Object result = invocation.proceed();
        System.out.println("===== query 方法执行完成 =====");
        return result;
    }

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

    @Override
    public void setProperties(Properties properties) {
        this.name = properties.getProperty("name");
    }
}

二 自定义分页插件

2.1 Mybatis自带分页功能

其实,Mybatis支持分页查询,只可惜是内存分页,仍然会查询全部数据,只是在内存中过滤。

使用RowBounds类,通过 offset(偏移量)和 limit(限制数量)控制所要处理的结果集范围,相当于执行了 resultList.subList(offset, offset + limit)。

java 复制代码
// SqlSession中
<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);

在处理ResultSet时,跳过offset条,限制最多处理limit条。

使用方式如下。RowBounds只适合数据量不大的场景,生产环境通常使用物理分页插件替代。

java 复制代码
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 跳过前10条,返回最多20条记录
RowBounds rowBounds = new RowBounds(10, 20);
List<User> user = mapper.selectByAge(20, rowBounds);

2.2 自定义分页插件

基于我们掌握的插件原理,完全可以自行定义一个分页插件。

首先,我们声明一个Page类来封装分页参数

java 复制代码
public class Page<T> {
  private int pageNo = 1;      // 当前页码
  private int pageSize = 10;    // 每页大小
  private long total;           // 总记录数
  private long totalPage;   	// 总页数
  // 省略getter/setter
}

再创建拦截器,拦截 StatementHandler 的 prepare 方法,改写 SQL 添加分页语句,并额外查询一次总记录数。

java 复制代码
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import java.util.Properties;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;

/**
 * MySQL 分页插件
 * 拦截 StatementHandler 的 prepare 方法,改写 SQL 添加分页语句
 */
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
        StatementHandler delegate = (StatementHandler) SystemMetaObject.forObject(handler).getValue("delegate");
        MappedStatement mappedStatement = (MappedStatement) SystemMetaObject.forObject(delegate).getValue("mappedStatement");

        // 获取 BoundSql
        BoundSql boundSql = delegate.getBoundSql();
        Object parameterObject = boundSql.getParameterObject();

        // 判断参数是否为 Page 对象
        if (parameterObject instanceof Page) {
            Page<?> page = (Page<?>) parameterObject;
            String originalSql = boundSql.getSql();

            // 1. 查询总记录数
            Connection connection = (Connection) invocation.getArgs()[0];
            long total = this.queryTotal(connection, page, mappedStatement);
            page.setTotal(total);
            page.setTotalPage(total % page.getPageSize() == 0 ? total / page.getPageSize() : total / page.getPageSize() + 1);
            
            // 2. 改写 SQL,添加 LIMIT 分页
            String pageSql = originalSql + " LIMIT " + page.getOffset() + ", " + page.getPageSize();

            // 通过反射修改 BoundSql 中的 SQL
            SystemMetaObject.forObject(boundSql).setValue("sql", pageSql);
        }
        return invocation.proceed();
    }

    /**
   * 查询总记录数
   */
  private long queryTotal(Connection connection, Object parameterObject, MappedStatement mappedStatement) throws Exception {
    BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
    String sql = "SELECT COUNT(*) FROM (" + boundSql.getSql() + ") tmp_count";
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
      ps = connection.prepareStatement(sql);
      // 设置参数(如果有)
      if (boundSql.getParameterMappings() != null && !boundSql.getParameterMappings().isEmpty()) {
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        BoundSql countSql = new BoundSql(mappedStatement.getConfiguration(), sql, parameterMappings, parameterObject);
        DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countSql);
        parameterHandler.setParameters(ps);
      }

      rs = ps.executeQuery();
      if (rs.next()) {
        return rs.getLong(1);
      }
      return 0;
    } finally {
      if (rs != null) {
        rs.close();
      }
      if (ps != null) {
        ps.close();
      }
    }
  }


  @Override
  public Object plugin(Object target) {
    // 只拦截 StatementHandler
    if (target instanceof StatementHandler) {
      return Plugin.wrap(target, this);
    }
    return target;
  }

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

在Mybatis主配置中添加插件。

XML 复制代码
<plugins>
    <plugin interceptor="com.example.PageInterceptor">
    </plugin>
</plugins>

创建UserQuery类,封装查询条件。执行查询后,就能从UserQuery中获取total等。

java 复制代码
public class UserQuery extends Page<User> {

  private String name;          // 姓名(模糊查询)
  private Integer minAge;       // 最小年龄
  private Integer maxAge;       // 最大年龄
  private String sex;
  // 省略getter/setter
}
java 复制代码
UserQuery query = new UserQuery();
query.setPageNum(1);
query.setPageSize(20);
query.setName("张三");
query.setMinAge(20);
query.setMaxAge(40);
List<User> users = userMapper.selectByPage(query);
long total = query.getTotal();
long totalPage = query.getTotalPage();
相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
云烟成雨TD1 天前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
行乾1 天前
鸿蒙端 IMSDK 架构探索
架构·harmonyos
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
殷紫川1 天前
深入理解 AQS:从架构到实现,解锁 Java 并发编程的核心密钥
java