PageHelper---实现原理

60 阅读3分钟

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) —— “偷天换日”

如果拦截器发现有分页参数,它会做两件惊天动地的事:

  1. 自动查询总数:

    它会克隆你的 SQL,把 SELECT * 换成 SELECT COUNT(*),先去数据库跑一遍,算出总共有多少条数据。这一步是为了将来给前端展示“共 100 页”用的。

  2. 修改主 SQL:

    它会修改你原本的 SQL 语句,在末尾加上 LIMIT 子句。

    • 你的原 SQLselect * from user
    • 发送给数据库的 SQLselect * from user LIMIT 0, 10

2. 代码层面对比

为了让你更直观地看到差异,我们对比一下“你写的”和“实际执行的”。

阶段Java 代码实际执行的 SQL (MyBatis Log)
无 PageHelperuserMapper.list()select * from user (如果有 100 万条数据,全查出来,内存爆炸)
有 PageHelperPageHelper.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 的实现机制就是:

  1. 利用 ThreadLocal 在线程中暂存分页参数。
  2. 利用 MyBatis 拦截器机制 劫持你的 SQL 请求。
  3. 动态拼装 LIMIT 语句实现物理分页。
  4. 查询结束后自动清除 ThreadLocal 里的数据。