基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

PageHelper分页插件的原理

知识点图片

PageHelper利用MyBatis拦截器,在SQL执行前通过ThreadLocal获取分页参数,自动将原始SQL改写为物理分页SQL(如LIMIT),实现无侵入的数据库层分页。

我们来详细剖析一下 PageHelper 分页插件的原理。

一句话概括其核心原理就是:利用 MyBatis 提供的插件(Interceptor)接口,通过 AOP(面向切面编程)的思想,在 SQL 语句执行前进行拦截,动态地重写 SQL 语句,为其添加物理分页逻辑,从而实现数据库层面的分页查询。

下面我们分步拆解这个过程,让你彻底理解它的工作流程。

核心流程详解

整个分页过程可以分为以下几个关键步骤:

步骤一:发起分页请求 (PageHelper.startPage())

当我们在业务代码中调用 PageHelper.startPage(pageNum, pageSize) 时,PageHelper 并没有立即去做任何数据库操作。它做了一件非常重要的事情:

  • 将分页参数(页码 pageNum 和每页数量 pageSize)存入一个 ThreadLocal 变量中。

为什么使用 ThreadLocal
ThreadLocal 是 Java 提供的一种线程级别的变量,每个线程都有自己独立的副本,互不干扰。这带来了两个巨大的好处:

  1. 线程安全:在多线程环境下(如 Web 服务器),每个用户的请求都是一个独立的线程。使用 ThreadLocal 可以确保 A 线程的分页参数不会被 B 线程的分页参数覆盖。
  2. 代码解耦/非侵入性:我们只需要在执行 Mapper 查询前调用一次 startPage() 即可,无需将分页参数(pageNum, pageSize)一路传递到 DAO/Mapper 层。这让我们的业务代码和分页逻辑完全分离,非常优雅。

步骤二:MyBatis 插件拦截 (PageInterceptor)

PageHelper 的核心是一个实现了 MyBatis Interceptor 接口的类,通常是 PageInterceptor。这个拦截器会配置在 MyBatis 的配置文件中,告诉 MyBatis:“请在执行某些特定操作时,先通知我。”

PageHelper 主要拦截的是 MyBatis 核心组件 Executorquery 方法。Executor 是 MyBatis 中负责执行 SQL 语句的真正执行者。

java
// PageInterceptor 上的注解,声明了要拦截的目标和方法
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
    // ...
}

当我们的业务代码调用 Mapper 接口的方法时(例如 userMapper.selectAll()),MyBatis 最终会调用 Executorquery 方法。此时,PageInterceptorintercept 方法就会被触发,控制权暂时交给了 PageHelper。

步骤三:SQL 的动态改造(核心)

intercept 方法内部,PageHelper 开始执行真正的分页逻辑:

  1. 检查 ThreadLocal:首先,它会检查当前线程的 ThreadLocal 中是否存在分页参数。

    • 如果不存在:说明这个查询不需要分页,直接放行,执行原始的 SQL,不做任何处理。
    • 如果存在:说明需要分页,开始执行下面的步骤。
  2. 获取原始 SQL:从 MappedStatement 对象中获取到开发者在 XML 文件中写的原始 SQL 语句(例如 SELECT * FROM user WHERE status = 1)。

  3. 生成 Count 查询 SQL:PageHelper 会智能地将原始的查询 SQL 转换为一条计算总记录数的 COUNT(*) SQL。例如,将 SELECT * FROM user WHERE status = 1 ORDER BY create_time 转换为 SELECT COUNT(*) FROM user WHERE status = 1。然后执行这条 count SQL,获取总记录数 total

  4. 生成物理分页 SQL:这是最关键的一步。PageHelper 会根据配置的数据库“方言”(Dialect),将原始 SQL 拼接成对应数据库的物理分页 SQL。

    • MySQL: SELECT * FROM user WHERE status = 1 LIMIT ?, ?
    • Oracle: SELECT * FROM ( SELECT tmp_page.*, ROWNUM rn FROM ( 原SQL ) tmp_page WHERE ROWNUM <= ? ) WHERE rn > ?
    • PostgreSQL: SELECT * FROM user WHERE status = 1 OFFSET ? LIMIT ?
    • ...等等
  5. 替换原始 SQL 并执行:PageHelper 将改造后的物理分页 SQL 替换掉原始的 SQL,然后让 MyBatis 继续执行。这时,数据库收到的已经是带有分页功能的 SQL 了,所以它只会查询并返回那一页的数据(例如 10 条),而不是全部数据。这大大提升了性能。

步骤四:结果封装 (Page 对象)

MyBatis 执行完被改造后的分页 SQL 后,会返回一个结果集 List<E>。PageHelper 的拦截器会拿到这个结果集。

但它返回给调用者的并不是一个简单的 List。它会创建一个自定义的 Page<E> 对象(这个类本身继承自 ArrayList,所以它本质上还是一个 List),然后:

  1. 将查询到的当页数据放入 Page 对象中。
  2. 将之前查询到的总记录数 total 也设置到 Page 对象中。
  3. 同时,Page 对象内部还会根据 totalpageNumpageSize 计算出总页数 pages、当前页码 pageNum 等其它分页相关的信息。

所以,Mapper 方法的最终返回值虽然在签名上是 List<User>,但实际运行时它的类型是 Page<User>,这个对象包含了分页的所有信息。

步骤五:清理 ThreadLocal

intercept 方法的 finally 块中,PageHelper 会调用 PageHelper.clearPage() 方法,清空 ThreadLocal 中的分页参数

为什么要清理?
因为 ThreadLocal 的生命周期与线程相同。如果不清理,当这个线程被线程池复用去执行下一个查询时,上一次的分页参数还在,就会导致下一个无关的查询也被错误地分页了。这是一个非常重要的安全措施。

流程总结图

plaintext
业务代码                                     PageHelper 内部                                       MyBatis 核心                                 数据库
   |                                              |                                                      |                                          |
1. PageHelper.startPage(1, 10)  --->  将分页参数(1, 10)存入 ThreadLocal
   |                                              |
2. userMapper.selectAll()
   |                                              |
   +--------------------------------------------> |                                                      |
                                                  |                                                      |
                                          3. PageInterceptor 拦截 Executor.query()
                                                  |
                                          4. 从 ThreadLocal 获取分页参数
                                                  |
                                          5. 生成并执行 Count SQL ---------------------------------------------------------------------------> SELECT COUNT(*) FROM user
                                                  |                                                      |                                          |
                                                  |<-----------------------------------------------------------------------------------------  返回 total=105
                                                  |
                                          6. 根据 "方言" 改造原始 SQL
                                             (SELECT * FROM user -> SELECT * FROM user LIMIT 0, 10)
                                                  |
                                          7. 用新 SQL 执行查询 -----------------------------> Executor.query(新SQL)  ------------------> SELECT * FROM user LIMIT 0, 10
                                                                                                   |                                          |
                                                                                                   |<------------------------------------------  返回 10 条记录 (List<User>)
                                                  |<-----------------------------------------------
                                                  |
                                          8. 将 List<User> 和 total 封装成 Page<User> 对象
                                                  |
                                          9. 在 finally 块中清理 ThreadLocal
                                                  |
   |<--------------------------------------------+
10. 得到 Page<User> 对象
    (内含10条数据和分页信息)

为什么通常使用 PageInfo

虽然 Mapper 返回的是 Page 对象,但我们通常会用 PageInfo 再包装一层:PageInfo<User> pageInfo = new PageInfo<>(userList);

  • Page 对象:是分页查询结果的核心,它继承了 List,主要存储数据和核心分页参数。
  • PageInfo 对象:是一个为视图层(前端)展示而设计的 DTO (Data Transfer Object)。它从 Page 对象中提取信息,并提供了更丰富、更易于使用的属性,如 isFirstPage, isLastPage, hasNextPage, hasPreviousPage, navigatepageNums (导航页码数组,如 [1, 2, 3, 4, 5]) 等。这使得前端在渲染分页导航条时非常方便。

这就是 PageHelper 从调用到返回结果的完整工作原理。它通过一种非常巧妙且对业务代码零侵入的方式,实现了强大的物理分页功能。

00:00
00:00