PageHelper 两种分页写法的坑与原理分析
背景
在 Spring Boot + MyBatis 项目中,PageHelper 是非常常见的分页组件。但在实际开发中,不同的分页写法会带来完全不同的可读性和类型安全体验,甚至出现“泛型写什么都不影响返回结果”的反直觉现象。
本文通过两个接口 /demo/pageQuery1 和 /demo/pageQuery2 的对比,结合源码跟踪,解释:
- 为什么
pageQuery1会出现“泛型失真”的问题 - 为什么
pageQuery2更符合直觉、更利于长期维护 - PageHelper 底层到底是如何工作的
复现步骤
下载代码,启动
download code代码使用java8+SpringBoot2.2.10
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();
}关键点分析
doSelectPageInfo的参数是ISelectISelect#doSelect()没有返回值- PageHelper 并不知道你 select 出来的是什么类型
也就是说:
泛型 E 完全是“你告诉它的”,不是它算出来的
实际数据是从哪里来的?
在 DemoService.pageQuery1 中:
- 即使你声明的是
PageInfo<DemoPageQueryVo> - 实际 SQL 执行结果仍然由 MyBatis 决定
数据真实来源路径
DemoMapper.xml中的selectresultType="DemoPageQueryResp"- MyBatis 执行 SQL
// org.apache.ibatis.executor.BaseExecutor
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); 查出来的 list 本身就是 DemoPageQueryResp
PageHelper 是如何“接管”分页结果的?
ThreadLocal 是关键
调用链核心逻辑:
PageHelper.startPage(...)- 内部调用:
PageMethod.setLocalPage(page);- MyBatis 查询完成后
- 在拦截器中:
PageMethod.getLocalPage();- 将查询结果包装成
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>中的T与select resultType一致
不推荐做法
- 在
doSelectPageInfo中“随意指定泛型” - 依赖 PageHelper 的内部实现细节
一句话总结
PageHelper 并不会帮你校验泛型
doSelectPageInfo更像是一种“语法糖”,而不是类型安全的 API
在追求可维护性和团队协作的项目中:
请选择
/demo/pageQuery2这种“直觉型分页写法”
作者:张三 创建时间:2025-12-19 15:30
最后编辑:张三 更新时间:2026-01-14 17:39
最后编辑:张三 更新时间:2026-01-14 17:39