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();
相关推荐
a努力。1 小时前
得物Java面试被问:Netty的ByteBuf引用计数和内存释放
java·开发语言·分布式·python·面试·职场和发展
Mcband1 小时前
Spring Boot 整合 ShedLock 处理定时任务重复执行的问题
java·spring boot·后端
REDcker1 小时前
C86 架构详解
数据库·微服务·架构
大只鹅2 小时前
Java集合框架-Collection
java·开发语言
悟空码字2 小时前
Spring Cloud 集成 Nacos,全面的配置中心与服务发现解决方案
java·nacos·springcloud·编程技术·后端开发
小冷coding2 小时前
【Java】基于Java的线上贷款分发业务技术栈设计方案
java·开发语言
星火开发设计2 小时前
循环结构进阶:while 与 do-while 循环的适用场景
java·开发语言·数据结构·学习·知识·循环
重生之绝世牛码2 小时前
Linux软件安装 —— JDK安装
java·大数据·linux·运维·jdk