[特殊字符] 深入理解 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 拦截体系 在默默为你工作。


相关推荐
为java加瓦3 小时前
Lombok @Data 注解在 Spring Boot 项目中的深度应用与实践指南
java·开发语言·数据库
青山撞入怀11143 小时前
sql题目练习-子查询
java·数据库·sql
程序新视界3 小时前
什么是MySQL分区?
数据库·mysql·dba
程序新视界3 小时前
实战技巧:使用冗余查询条件解锁MySQL中的索引
数据库·mysql·dba
番茄Salad3 小时前
Spring Boot项目中Maven引入依赖常见报错问题解决
spring boot·后端·maven
程序员小凯3 小时前
Spring MVC 分布式事务与数据一致性教程
分布式·spring·mvc
Jabes.yang4 小时前
Java求职面试: 互联网医疗场景中的缓存技术与监控运维应用
java·redis·spring security·grafana·prometheus·oauth2·互联网医疗
初级炼丹师(爱说实话版)4 小时前
内存泄漏与内存溢出
java
CryptoRzz4 小时前
越南k线历史数据、IPO新股股票数据接口文档
java·数据库·后端·python·区块链