Skip to content

PageHelper分页原理源码跟踪

1、分页原理源码跟踪

1、PageHelper.startPage

java
/**
 * 开始分页
 *
 * @param pageNum  页码
 * @param pageSize 每页显示数量
 * @param count    是否进行count查询
 */
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
    return startPage(pageNum, pageSize, count, null, null);
}
java
/**
 * 开始分页
 *
 * @param pageNum      页码
 * @param pageSize     每页显示数量
 * @param count        是否进行count查询
 * @param reasonable   分页合理化,null时用默认配置
 * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
 */
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page<E>(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    //当已经执行过orderBy的时候
    Page<E> oldPage = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    // 把page对象放在ThreadLocal中
    setLocalPage(page);
    return page;
}

我们看PageInterceptorintercept方法

java
@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        //由于逻辑关系,只会进入一次
        if (args.length == 4) {
            //4 个参数时
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            //6 个参数时
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        checkDialectExists();
        //对 boundSql 的拦截处理
        if (dialect instanceof BoundSqlInterceptor.Chain) {
            boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
        }
        List resultList;
        // 1、调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!dialect.skip(ms, parameter, rowBounds)) {
            //开启debug时,输出触发当前分页执行时的PageHelper调用堆栈
            // 如果和当前调用堆栈不一致,说明在启用分页后没有消费,当前线程再次执行时消费,调用堆栈显示的方法使用不安全
            debugStackTraceLog();
            Future<Long> countFuture = null;
            // 2、判断是否需要进行 count 查询
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                if (dialect.isAsyncCount()) {
                    countFuture = asyncCount(ms, boundSql, parameter, rowBounds);
                } else {
                    // 3、查询总数
                    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
            }
            // 4、执行分页查询
            resultList = ExecutorUtil.pageQuery(dialect, executor,
                    ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            if (countFuture != null) {
                Long count = countFuture.get();
                dialect.afterCount(count, parameter, rowBounds);
            }
        } else {
            //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        // 5、封装结果
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if (dialect != null) {
            dialect.afterAll();
        }
    }
}

2、判断是否分页

首先根据PageHelper的skip方法查看是否需要分页,判断条件是ThreadLocal中是否有page对象,因为PageHelper.startPage方法放入到ThreadLocal中放入page对象,因此此处会判断为分页。

3、查询总条数

方法会定位到PageInterceptor的count方法的的代码

java
count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);

方法executeAutoCount方法如下,

首先根据查询语句拼接count语句(select * from table where a ---> select count("0") from table where a)

然后执行SQL

拿到count结果

image-20240708162412590

image-20240708162348429

4、保存总条数

首先从ThreadLocal中获取page对象,然后把总条数count放在page对象中,然后根据总条数和分页条件判断是否有必要查询,比如一共10条记录,你每页10条,你查第2页,那么就没必要去查询,因此11-20条记录不存在。

java
@Override
public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
    Page page = getLocalPage();
    page.setTotal(count);
    if (rowBounds instanceof PageRowBounds) {
        ((PageRowBounds) rowBounds).setTotal(count);
    }
    //pageSize < 0 的时候,不执行分页查询
    //pageSize = 0 的时候,还需要执行后续查询,但是不会分页
    if (page.getPageSizeZero() != null) {
        //PageSizeZero=false&&pageSize<=0
        if (!page.getPageSizeZero() && page.getPageSize() <= 0) {
            return false;
        }
        //PageSizeZero=true&&pageSize<0 返回 false,只有>=0才需要执行后续的
        else if (page.getPageSizeZero() && page.getPageSize() < 0) {
            return false;
        }
    }
    //页码>0 && 开始行数<总行数即可,不需要考虑 pageSize(上面的 if 已经处理不符合要求的值了)
    return page.getPageNum() > 0 && count > page.getStartRow();
}

5、执行分页查询

image-20240708162751956

怎么获取分页的SQL呢?

java
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    if (page.getStartRow() == 0) {
        sqlBuilder.append("\n LIMIT ? ");
    } else {
        sqlBuilder.append("\n LIMIT ?, ? ");
    }
    return sqlBuilder.toString();
}

6、封装结果

把查询到的结果放到TheadLocal中的page对象中,然后返回page对象,此时page对象带有查询对象集合、分页条数、第几页

java
@Override
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
    Page page = getLocalPage();
    if (page == null) {
        return pageList;
    }
    page.addAll(pageList);
    //调整判断顺序,如果查全部,total就是size,如果只排序,也是全部,其他情况下如果不查询count就是-1
    if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
        page.setTotal(pageList.size());
    } else if (page.isOrderByOnly()) {
        page.setTotal(pageList.size());
    } else if (!page.isCount()) {
        page.setTotal(-1);
    }
    return page;
}

7、构造PageInfo对象

首先要明确的是下面代码中的temperatures对象是Page(Page<E> extends ArrayList<E> )类型的,Page集成了ArrayList对象。

java
PageHelper.startPage(1,10);
List<GradeModel> list = gradeService.list();
PageInfo<GradeModel> pageInfo = new PageInfo<>(list);
java
/**
 * 包装Page对象
 *
 * @param list          page结果
 * @param navigatePages 页码数量
 */
public PageInfo(List<? extends T> list, int navigatePages) {
    super(list);
    if (list instanceof Page) {
        Page page = (Page) list;
        this.pageNum = page.getPageNum();
        this.pageSize = page.getPageSize();
        // 获取当前第几页
        this.pages = page.getPages();
        // 获取每页大小
        this.size = page.size();
        //由于结果是>startRow的,所以实际的需要+1
        if (this.size == 0) {
            this.startRow = 0;
            this.endRow = 0;
        } else {
            this.startRow = page.getStartRow() + 1;
            //计算实际的endRow(最后一页的时候特殊)
            this.endRow = this.startRow - 1 + this.size;
        }
    } else if (list instanceof Collection) {
        this.pageNum = 1;
        this.pageSize = list.size();

        this.pages = this.pageSize > 0 ? 1 : 0;
        this.size = list.size();
        this.startRow = 0;
        this.endRow = list.size() > 0 ? list.size() - 1 : 0;
    }
    if (list instanceof Collection) {
        calcByNavigatePages(navigatePages);
    }
}

8、总结

  • 首先会把分页参数封装成Page对象放到ThreadLocal
  • 然后根据SQL进行拼接转换(select * from table where a) -> (select count("0") from table where a)和(select * from table where a limit ?,?)
  • 有了total总条数、pageNum当前第几页、pageSize每页大小和当前页的数据,就可以算出分页的其他非必要信息(是否为首页,是否为尾页,总页数)

2、PageHelper最后一页数据

image-20240708160519283

我们可以看到总共只有4页,我们访问第5页的时候返回的是空数组,我们能不能实现超过总页码的返回最后一页数据呢?

在 application.yml 中配置 PageHelper 的 reasonable 参数:

yaml
pagehelper:
  reasonable: true

合理分页

对于大多数应用,建议使用 reasonable: true,提供更友好的分页体验,避免用户因为输入错误页码而无法获取数据。

严格分页

如果你希望对分页参数进行严格控制,避免任何自动调整行为,可以选择 reasonable: false,这对一些需要严格分页逻辑的业务场景可能是必要的。

1、源码跟踪

java
@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        //由于逻辑关系,只会进入一次
        if (args.length == 4) {
            //4 个参数时
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            //6 个参数时
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        checkDialectExists();
        //对 boundSql 的拦截处理
        if (dialect instanceof BoundSqlInterceptor.Chain) {
            boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
        }
        List resultList;
        // 1、调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!dialect.skip(ms, parameter, rowBounds)) {
            //开启debug时,输出触发当前分页执行时的PageHelper调用堆栈
            // 如果和当前调用堆栈不一致,说明在启用分页后没有消费,当前线程再次执行时消费,调用堆栈显示的方法使用不安全
            debugStackTraceLog();
            Future<Long> countFuture = null;
            // 2、判断是否需要进行 count 查询
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                if (dialect.isAsyncCount()) {
                    countFuture = asyncCount(ms, boundSql, parameter, rowBounds);
                } else {
                    // 3、查询总数
                    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
            }
            // 4、执行分页查询
            resultList = ExecutorUtil.pageQuery(dialect, executor,
                    ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            if (countFuture != null) {
                Long count = countFuture.get();
                dialect.afterCount(count, parameter, rowBounds);
            }
        } else {
            //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        // 5、封装结果
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if (dialect != null) {
            dialect.afterAll();
        }
    }
}

我们看步骤3,保存总条数,总条数会保存到ThreadLocal的Page对象中,如图代码所示

java
// AbstractHelperDialect的afterCount方法
@Override
public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
    Page page = getLocalPage();
    page.setTotal(count);
    if (rowBounds instanceof PageRowBounds) {
        ((PageRowBounds) rowBounds).setTotal(count);
    }
    //pageSize < 0 的时候,不执行分页查询
    //pageSize = 0 的时候,还需要执行后续查询,但是不会分页
    if (page.getPageSizeZero() != null) {
        //PageSizeZero=false&&pageSize<=0
        if (!page.getPageSizeZero() && page.getPageSize() <= 0) {
            return false;
        }
        //PageSizeZero=true&&pageSize<0 返回 false,只有>=0才需要执行后续的
        else if (page.getPageSizeZero() && page.getPageSize() < 0) {
            return false;
        }
    }
    //页码>0 && 开始行数<总行数即可,不需要考虑 pageSize(上面的 if 已经处理不符合要求的值了)
    return page.getPageNum() > 0 && count > page.getStartRow();
}

我们跟进Page的setTotal方法

java
@Override
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
    Page page = getLocalPage();
    if (page == null) {
        return pageList;
    }
    page.addAll(pageList);
    //调整判断顺序,如果查全部,total就是size,如果只排序,也是全部,其他情况下如果不查询count就是-1
    if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
        page.setTotal(pageList.size());
    } else if (page.isOrderByOnly()) {
        page.setTotal(pageList.size());
    } else if (!page.isCount()) {
        page.setTotal(-1);
    }
    return page;
}
java
public void setTotal(long total) {
    this.total = total;
    if (total == -1) {
        pages = 1;
        return;
    }
    if (pageSize > 0) {
        pages = (int) (total / pageSize + ((total % pageSize == 0) ? 0 : 1));
    } else {
        pages = 0;
    }
    //分页合理化,针对不合理的页码自动处理
    if ((reasonable != null && reasonable) && pageNum > pages) {
        if (pages != 0) {
            // 把pageNum设置为最后一页
            pageNum = pages;
        }
        calculateStartAndEndRow();
    }
}

3、PageHelper 安全调用

PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelperfinally 代码段中自动清除了 ThreadLocal 存储的对象。

如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。

但是如果你写出下面这样的代码,就是不安全的用法:

java
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

java
List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

这种写法就能保证安全。

如果你对此不放心,你可以手动清理 ThreadLocal 存储的分页参数,可以像下面这样使用:

java
List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    try{
        list = countryMapper.selectAll();
    } finally {
        PageHelper.clearPage();
    }
} else {
    list = new ArrayList<Country>();
}

1、改造拦截器

java
package com.xx.page.interceptor;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.xx.page.context.PageData;
import com.xx.result.Result;
import com.xx.utils.PageDataUtil;
import com.xx.utils.ServletUtil;
import com.xx.utils.ValidationUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @Author: xueqimiao
 * @Date: 2023/11/8 15:58
 */
@Component
@Aspect
public class PageDataInterceptor {

    public Logger log = LoggerFactory.getLogger(PageDataInterceptor.class);

    // 切入点
    @Pointcut("@annotation(com.xx.page.annotation.NeedPage)")
    public void controllerAspect() {
    }

    @Around("controllerAspect()")
    public Result controllerAround(ProceedingJoinPoint proJoinPoint) throws Throwable {
        log.info("==========> 开始执行PageHelper");
        // 1、获取请求参数
        HttpServletRequest request = ServletUtil.getHttpRequest();
        int currentPage = getIntParameter(request, PageDataUtil.PAGE_CURRENT_PAGE_NO_STR, 1);
        int pageSize = getIntParameter(request, PageDataUtil.PAGE_SIZE_STR, 10);
        // 是否进行count查询
        int count = getIntParameter(request, PageDataUtil.COUNT_FLAG, 1);

        Page<Object> page = PageHelper.startPage(currentPage, pageSize, count == 1);
        try {
            return handlePage(proJoinPoint, page);
        } finally {
            PageHelper.clearPage(); // 清除分页信息
        }
    }

    private Result handlePage(ProceedingJoinPoint proJoinPoint, Page<Object> page) throws Throwable {
        Object retVal = proJoinPoint.proceed();
        if (retVal == null || !(retVal instanceof Result)
                || !((Result) retVal).isSuccess()) {
            return Result.ok(retVal);
        }
        Result returnResult = (Result) retVal;
        if (!(returnResult.getResult() instanceof List)) {
            return returnResult;
        }
        PageData pageData = new PageData();
        Object result = returnResult.getResult();
        pageData.setRecords(result);
        PageInfo pageInfo = new PageInfo<>(page);
        pageData.setSize(pageInfo.getPageSize());
        pageData.setTotal(pageInfo.getTotal());
        pageData.setPages(pageInfo.getPages());
        pageData.setCurrent(pageInfo.getPageNum());
        returnResult.setResult(pageData);
        return returnResult;
    }


    private static int getIntParameter(HttpServletRequest request, String paramName, Integer defaultVal) {
        String paramValue = request.getParameter(paramName);
        if (ValidationUtil.isEmpty(paramValue)) {
            return defaultVal;
        }
        return Integer.parseInt(paramValue);
    }
}
  • 在 controllerAround 方法中,添加了 try-finally 块,确保无论方法是否成功执行,都会调用 PageHelper.clearPage() 清除分页信息。
  • PageHelper.clearPage() 方法用于清除当前线程中的分页信息,以防止其影响后续请求的分页处理。
  • 能够在每次请求处理完毕后正确地清除分页状态,避免潜在的线程安全问题。

写在最后

博客官网: https://xiaoxueblog.com/

技术的世界就像一片广袤无垠的海洋,充满了无尽的奥秘和挑战。而我们,作为这片海洋上的探索者,需要有无畏的勇气和不屈不挠的精神,才能在波涛汹涌中驶向成功的彼岸。