Appearance
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;
}
我们看PageInterceptor
的intercept
方法
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结果
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、执行分页查询
怎么获取分页的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最后一页数据
我们可以看到总共只有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 查询方法,这就是安全的。因为 PageHelper
在 finally
代码段中自动清除了 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/
技术的世界就像一片广袤无垠的海洋,充满了无尽的奥秘和挑战。而我们,作为这片海洋上的探索者,需要有无畏的勇气和不屈不挠的精神,才能在波涛汹涌中驶向成功的彼岸。