跳转至

手写 MyBatis 分页插件思路?

一、原理

MyBatis 插件用 Interceptor 拦截四大对象(ExecutorParameterHandlerResultSetHandlerStatementHandler)。分页插件拦截 StatementHandler#prepare,在 SQL 执行前改写原 SQL,加 LIMIT ?, ?

二、实现步骤

1. 定义分页对象

public class PageParam {
    private int pageNum = 1;
    private int pageSize = 10;
    // getter/setter
}

2. 实现 Interceptor

@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 {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(handler);

        // 1. 拿到 BoundSql 和原始 SQL
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();

        // 2. 判断是否需要分页(从参数中找 PageParam)
        Object parameterObject = boundSql.getParameterObject();
        PageParam page = findPage(parameterObject);
        if (page == null) {
            return invocation.proceed();
        }

        // 3. 改写 SQL:MySQL 用 limit
        String pageSql = sql + " LIMIT " + page.getOffset() + "," + page.getPageSize();

        // 4. 用 MetaObject 写回新 SQL
        metaObject.setValue("boundSql.sql", pageSql);

        return invocation.proceed();
    }

    private PageParam findPage(Object param) {
        if (param instanceof MapperMethod.ParamMap) {
            Map<?, ?> map = (Map<?, ?>) param;
            for (Object v : map.values()) {
                if (v instanceof PageParam) return (PageParam) v;
            }
        }
        return null;
    }

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

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

3. 注册插件

<configuration>
    <plugins>
        <plugin interceptor="com.example.PageInterceptor"/>
    </plugins>
</configuration>

或 Spring Boot:

@Bean
public PageInterceptor pageInterceptor() {
    return new PageInterceptor();
}

三、Count 查询

分页插件通常还需要执行一次 SELECT COUNT(*) 来拿总数。做法:

  1. 拦截 Executor#query
  2. 如果方法上标了 @Pageable,生成 count SQL:
    SELECT COUNT(*) FROM ( SQL) t
    
  3. 先执行 count,再执行分页 SQL。

四、现有轮子

生产环境直接用 PageHelper

PageHelper.startPage(2, 10);
List<User> list = userMapper.selectAll();
PageInfo<User> info = new PageInfo<>(list);
long total = info.getTotal();

面试加分

  • 为什么拦截 StatementHandler#prepare 而不是 Executor?因为此时 SQL 已经解析完毕,参数已绑定,直接改 SQL 字符串最干净。
  • MetaObject 是 MyBatis 提供的反射工具,可以安全地访问私有字段。
  • 方言问题:MySQL 用 LIMIT,Oracle 用 ROWNUM,PostgreSQL 用 LIMIT,要按数据库类型生成分页 SQL。