PageHelper 两种分页写法的坑与原理分析

背景

在 Spring Boot + MyBatis 项目中,PageHelper 是非常常见的分页组件。但在实际开发中,不同的分页写法会带来完全不同的可读性和类型安全体验,甚至出现“泛型写什么都不影响返回结果”的反直觉现象。

本文通过两个接口 /demo/pageQuery1/demo/pageQuery2 的对比,结合源码跟踪,解释:

  • 为什么 pageQuery1 会出现“泛型失真”的问题
  • 为什么 pageQuery2 更符合直觉、更利于长期维护
  • PageHelper 底层到底是如何工作的

复现步骤

下载代码,启动

代码使用java8+SpringBoot2.2.10

download code

1. 启动服务

启动 Spring Boot 应用。

2. 发起请求

curl -XPOST -H 'content-type: application/json;charset=UTF-8'   'http://localhost:8000/demo/pageQuery1'   -d '{"pageNum":1, "pageSize": 2}'

curl -XPOST -H 'content-type: application/json;charset=UTF-8'   'http://localhost:8000/demo/pageQuery2'   -d '{"pageNum":1, "pageSize": 2}'

现象说明

/demo/pageQuery1

  • 返回类型:
ResultBody2<PageInfo<DemoPageQueryVo>>
  • DemoPageQueryVo 只有一个字段(id)
  • 但接口返回 JSON 中却包含 多个字段
  • 更离谱的是:
ResultBody2<PageInfo<Integer>>

居然也能正常返回完整对象数据

泛型看起来完全不起作用


/demo/pageQuery2

  • 返回类型:
ResultBody2<PageInfo<DemoPageQueryResp>>
  • DemoPageQueryResp 中的字段
  • select 查询字段 一一对应
  • 返回结果 完全符合直觉

结论(先给结论)

推荐使用 /demo/pageQuery2

原因:

  • 泛型语义准确
  • 数据来源清晰
  • 符合大多数开发者认知
  • 不依赖 PageHelper 的“隐式行为”

/demo/pageQuery1

  • 泛型不可信
  • 可读性差
  • 非常容易误导维护者

两种分页代码对比

写法一:pageQuery1(不推荐)

public PageInfo<DemoPageQueryVo> pageQuery1(DemoPageQueryReq req) {
    PageInfo<DemoPageQueryVo> demoPageQueryVoPageInfo =
        PageHelper.startPage(req.getPageNum(), req.getPageSize())
            .doSelectPageInfo(() -> {
                queryListFromDb(req);
            });
    return demoPageQueryVoPageInfo;
}

写法二:pageQuery2(推荐)

public PageInfo<DemoPageQueryResp> pageQuery2(DemoPageQueryReq req) {
    PageHelper.startPage(req.getPageNum(), req.getPageSize());
    List<DemoPageQueryResp> list = queryListFromDb(req);
    return new PageInfo<>(list);
}

核心问题:为什么 pageQuery1 的泛型会“失效”?

关键源码入口

// com.github.pagehelper.Page#doSelectPageInfo
public <E> PageInfo<E> doSelectPageInfo(ISelect select) {
    select.doSelect();
    return (PageInfo<E>) this.toPageInfo();
}

关键点分析

  1. doSelectPageInfo 的参数是 ISelect
  2. ISelect#doSelect() 没有返回值
  3. PageHelper 并不知道你 select 出来的是什么类型

也就是说:

泛型 E 完全是“你告诉它的”,不是它算出来的


实际数据是从哪里来的?

DemoService.pageQuery1 中:

  • 即使你声明的是 PageInfo<DemoPageQueryVo>
  • 实际 SQL 执行结果仍然由 MyBatis 决定

数据真实来源路径

  1. DemoMapper.xml 中的 select
  2. resultType="DemoPageQueryResp"
  3. MyBatis 执行 SQL
// org.apache.ibatis.executor.BaseExecutor
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

查出来的 list 本身就是 DemoPageQueryResp


PageHelper 是如何“接管”分页结果的?

ThreadLocal 是关键

调用链核心逻辑:

  1. PageHelper.startPage(...)
  2. 内部调用:
PageMethod.setLocalPage(page);
  1. MyBatis 查询完成后
  2. 在拦截器中:
PageMethod.getLocalPage();
  1. 将查询结果包装成 PageInfo

包装发生的位置

// com.github.pagehelper.dialect.AbstractHelperDialect
afterPage(...) {
    Page page = PageMethod.getLocalPage();
    // 封装 PageInfo
}

PageHelper 只是“拿结果 + 包一层”

它:

  • 不关心泛型
  • 不校验类型
  • 不做 VO 转换

为什么 pageQuery2 没有问题?

因为:

List<DemoPageQueryResp> list = queryListFromDb(req);
return new PageInfo<>(list);
  • 泛型由 List<T> 决定
  • 编译期即可校验
  • 不依赖 PageHelper 的隐式行为
  • 此时的list的类型是com.github.pagehelper.Page,所以它有总记录数、每页记录数

这是 Java 开发者最熟悉、最安全的模型


最佳实践总结

推荐做法

  • 使用 startPage + mapper.select + new PageInfo<>(list)
  • 保证 PageInfo<T> 中的 Tselect resultType 一致

不推荐做法

  • doSelectPageInfo 中“随意指定泛型”
  • 依赖 PageHelper 的内部实现细节

一句话总结

PageHelper 并不会帮你校验泛型
doSelectPageInfo 更像是一种“语法糖”,而不是类型安全的 API

在追求可维护性和团队协作的项目中:

请选择 /demo/pageQuery2 这种“直觉型分页写法”

作者:张三  创建时间:2025-12-19 15:30
最后编辑:张三  更新时间:2026-01-14 17:39