PageHelper 是 MyBatis 生态中最著名的分页插件。它的出现彻底解决了“手写 LIMIT 语句”的痛苦。
要理解它的实现机制,你只需要记住三个关键词:ThreadLocal(线程本地变量) 、Interceptor(拦截器) 、SQL 重写。
我把这个过程比喻为**“隐形改卷老师”**,带你一步步看懂它在后台到底干了什么。
1. 核心原理图解
整个过程可以分为三步:存参、拦截、改写。
第一步:存参 (ThreadLocal) —— “贴便签”
你在 Service 层写下这就话时:
Java
PageHelper.startPage(1, 10); // 第1页,查10条
- 发生了什么:PageHelper 会把这两个参数(页码 1,大小 10)存入当前线程的
ThreadLocal变量中。 - 比喻:这就像你在你的任务单上贴了一张便利贴,上面写着:“注意!接下来的那次查询,我要只要前 10 条!”。
- 关键点:
ThreadLocal保证了这个参数只对当前线程有效,不会影响别的用户的请求。
第二步:拦截 (Interceptor) —— “老师检查”
当你调用 Mapper 接口查询时:
Java
List<User> list = userMapper.list(); // 这一行没有任何分页逻辑
- 发生了什么:MyBatis 在执行 SQL 之前,会被 PageHelper 的拦截器 (
PageInterceptor) 截获。 - 比喻:你的卷子(SQL 语句)在交给阅卷系统(数据库)之前,被一个隐形的改卷老师(拦截器)拦下来了。老师看了一眼你身上有没有贴“便利贴”(ThreadLocal 里有没有分页参数)。
第三步:改写 (SQL Rewrite) —— “偷天换日”
如果拦截器发现有分页参数,它会做两件惊天动地的事:
-
自动查询总数:
它会克隆你的 SQL,把 SELECT * 换成 SELECT COUNT(*),先去数据库跑一遍,算出总共有多少条数据。这一步是为了将来给前端展示“共 100 页”用的。
-
修改主 SQL:
它会修改你原本的 SQL 语句,在末尾加上 LIMIT 子句。
- 你的原 SQL:
select * from user - 发送给数据库的 SQL:
select * from user LIMIT 0, 10
- 你的原 SQL:
2. 代码层面对比
为了让你更直观地看到差异,我们对比一下“你写的”和“实际执行的”。
| 阶段 | Java 代码 | 实际执行的 SQL (MyBatis Log) |
|---|---|---|
| 无 PageHelper | userMapper.list() | select * from user (如果有 100 万条数据,全查出来,内存爆炸) |
| 有 PageHelper | PageHelper.startPage(1, 10); userMapper.list() | 第一句:SELECT count(0) FROM user 第二句:select * from user LIMIT 0, 10 |
3. 一个致命的注意事项(面试常问)
由于 PageHelper 使用 ThreadLocal 存储分页参数,这就要求“设置分页”和“执行查询”必须紧挨着,且中间不能有异常中断。
❌ 错误写法(危险):
Java
PageHelper.startPage(1, 10);
if (parameterError) {
throw new Exception("参数错误"); // 报错退出了,查询没执行
}
userMapper.list();
- 后果:因为查询没执行,
ThreadLocal里的分页参数没被消费掉(清除) 。线程回到线程池后,下一次复用这个线程处理别人的请求时,可能会莫名其妙地给别人的 SQL 加上LIMIT,导致严重的生产事故(ThreadLocal 污染)。
✅ 正确写法(安全):
Java
// 紧挨着写,或者放在 try-finally 块中(PageHelper 内部其实帮你做了清理,但紧挨着写最安全)
PageHelper.startPage(1, 10);
List<User> list = userMapper.list();
4. 总结
PageHelper 的实现机制就是:
- 利用 ThreadLocal 在线程中暂存分页参数。
- 利用 MyBatis 拦截器机制 劫持你的 SQL 请求。
- 动态拼装 LIMIT 语句实现物理分页。
- 查询结束后自动清除 ThreadLocal 里的数据。