[特殊字符] 深入理解 PageHelper 分页原理:从 startPage 到 SQL 改写全过程

在日常开发中,分页几乎是后端系统中最常见的功能之一。

我们在使用 MyBatis + PageHelper 时,只需要一句:

java 复制代码
PageHelper.startPage(pageNum, pageSize);
List<User> list = userMapper.selectUserList();

就能实现分页查询。

看似简单,但你是否想过:PageHelper 是如何知道要分页的?
SQL 中的 LIMIT 是在哪里拼接的?

今天我们就来深入分析一下 PageHelper 的工作原理。


🧩 一、分页从哪开始?------ startPage()

分页逻辑的入口就在:

java 复制代码
PageHelper.startPage(pageNum, pageSize);

来看它的源码(简化版):

java 复制代码
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);

    Page<E> oldPage = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }

    setLocalPage(page);
    return page;
}

这段代码其实做了三件事:

  1. 创建一个 Page 对象,保存分页参数(页码、每页数量、是否统计总数等);
  2. 检查旧的 Page 是否存在(如果只设置了排序规则,就继承它);
  3. 把当前 Page 对象存入 ThreadLocal 中

🧠 二、ThreadLocal:分页的关键所在

分页之所以能"自动生效",关键在于 PageHelper 使用了 ThreadLocal

每个线程都会维护一个自己的分页上下文对象(Page),

这意味着每次请求(通常一个 HTTP 请求对应一个线程)都可以拥有独立的分页参数,不会互相干扰。

简而言之:

startPage() 只是把分页参数存到了当前线程中。

当我们执行 userMapper.selectUserList() 时,
MyBatis 拦截器 会从 ThreadLocal 中取出这个 Page 对象,

在 SQL 执行前,自动为 SQL 拼接上分页语句。


🧩 三、拦截器如何改写 SQL?

PageHelper 注册了一个 MyBatis 拦截器:

java 复制代码
@Intercepts({@Signature(
    type = StatementHandler.class,
    method = "prepare",
    args = {Connection.class, Integer.class}
)})

每当 MyBatis 准备执行 SQL 时,这个拦截器会被触发。

它会:

  1. 判断当前线程是否存在 Page(也就是我们在 startPage() 放进去的);
  2. 如果存在,就改写 SQL,拼上分页语句;
  3. 执行一次 count 查询,用于获取总记录数;
  4. 最终返回结果列表 + 分页信息。

例如,原 SQL:

sql 复制代码
SELECT * FROM sys_user WHERE status = 1;

经过 PageHelper 改写后变为:

sql 复制代码
SELECT * FROM sys_user WHERE status = 1 LIMIT 0, 10;

同时还会额外执行:

sql 复制代码
SELECT COUNT(1) FROM sys_user WHERE status = 1;

🧩 四、分页结果是怎么返回的?

分页查询返回后,我们一般会调用:

java 复制代码
return getDataTable(list);

而若依框架中的 getDataTable() 方法是这样写的:

java 复制代码
protected TableDataInfo getDataTable(List<?> list)
{
    TableDataInfo rspData = new TableDataInfo();
    rspData.setCode(HttpStatus.SUCCESS);
    rspData.setRows(list);
    rspData.setTotal(new PageInfo(list).getTotal());
    return rspData;
}

其中 PageInfo 内部会读取 PageHelper 存在 ThreadLocal 中的分页信息,

包括:

  • 总记录数 total
  • 当前页码 pageNum
  • 每页数量 pageSize
  • 总页数 pages

最终生成 JSON 响应:

json 复制代码
{
  "code": 200,
  "rows": [ ...用户数据... ],
  "total": 153
}

⚙️ 五、参数说明与高级特性

PageHelper 的分页控制非常灵活,它的 startPage() 还有以下参数:

参数名 含义
pageNum 当前页码
pageSize 每页数量
count 是否统计总记录数
reasonable 是否启用页码合理化(超出范围时自动调整)
pageSizeZero 是否允许 pageSize=0 返回所有记录

例如:

java 复制代码
PageHelper.startPage(1, 0, true, true, true);

→ 代表:第一页,不分页(返回全部数据),执行 count 查询。


🧩 六、为什么要用 ThreadLocal?

使用 ThreadLocal 的核心目的,是为了在 不修改原 SQL 的情况下实现分页。

因为 MyBatis 执行 SQL 的过程是多层封装的,

分页插件无法直接知道当前的分页参数。

因此 PageHelper 采用 ThreadLocal 方案:

  • Controller 调用 startPage() 时,将分页信息保存到线程上下文;
  • MyBatis 拦截器在执行 SQL 前,从线程中读取分页参数;
  • 执行结束后自动清理 ThreadLocal,防止污染其他线程。

💣 七、常见问题:分页内存泄漏?

由于 PageHelper 使用了 ThreadLocal,如果使用不当,也可能造成内存泄漏。

例如线程池复用时,如果分页对象没有清理干净,旧的分页参数可能残留。

解决方法:

PageHelper 内部已在 finally 块中清理 ThreadLocal;

如果是自定义 ThreadLocal,请务必在使用后调用:

java 复制代码
threadLocal.remove();

✅ 八、总结

步骤 说明
startPage() 在当前线程中创建分页上下文(Page对象)
MyBatis 拦截器 在执行 SQL 前读取 Page 信息并改写 SQL
count 查询 自动统计总记录数
PageInfo 封装分页结果
ThreadLocal 实现线程级隔离,保证分页参数安全传递

一句话总结:

PageHelper 通过 ThreadLocal + MyBatis 拦截器 实现了"无侵入式分页"。

开发者无需在 SQL 中写 LIMIT,就能优雅地实现数据库层分页。


💬 九、写在最后

分页是每个后端工程师都会接触的功能,但理解它的底层原理,

不仅能帮助我们更好地使用 PageHelper,也能在设计自己的分页组件时少走弯路。

下次你再写:

java 复制代码
startPage();
List<User> list = userMapper.selectUserList();

就能清楚知道:这不仅仅是分页,而是一套完整的 线程上下文 + SQL 拦截体系 在默默为你工作。


相关推荐
养乐多072215 小时前
【Java】IO流
java
俊男无期15 小时前
超效率工作法
java·前端·数据库
小信啊啊15 小时前
Go语言数组与切片的区别
开发语言·后端·golang
中国胖子风清扬15 小时前
SpringAI和 Langchain4j等 AI 框架之间的差异和开发经验
java·数据库·人工智能·spring boot·spring cloud·ai·langchain
计算机学姐15 小时前
基于php的摄影网站系统
开发语言·vue.js·后端·mysql·php·phpstorm
月明长歌15 小时前
【码道初阶】牛客TSINGK110:二叉树遍历(较难)如何根据“扩展先序遍历”构建二叉树?
java·数据结构·算法
Java水解15 小时前
【SpringBoot3】Spring Boot 3.0 集成 Mybatis Plus
spring boot·后端
whoops本尊15 小时前
Golang-Data race【AI总结版】
后端
墨守城规15 小时前
线程池用法及原理
后端
用户21903265273515 小时前
Spring Boot + Redis 注解极简教程:5分钟搞定CRUD操作
java·后端