Spring Boot 分页接口怎么设计:pageSize 不设上限会发生什么
工程判断: 分页接口通常只需要 pageNum 和 pageSize 两个参数,但 pageSize 一旦没有上限,分页就可能退化成一次全表读取,并把数据库、网络和 JSON 序列化同时拖入慢请求。
这类风险不能依靠前端"正常传值"来避免,因为脚本、旧客户端和恶意请求都可以绕过页面。企业级分页契约必须统一默认值、最大值、排序稳定性和总数查询成本。
MetaLite 将分页参数校验和 DAO 查询模型收敛到公共入口,避免每个接口重复判断。本文先给出通用分页防线,再结合参数对象与查询代码说明限制应该落在哪一层。
一、pageSize 为什么必须有服务端上限
如果前端把每页大小从 20 改成 200000,一次请求会同时放大:
- 数据库扫描和结果集内存;
- JDBC 对象创建;
- Entity/DTO 转换;
- JSON 序列化;
- 网关与服务间网络传输;
- 接口日志和前端渲染。
前端下拉框限制不是安全边界,调用方可以绕过页面直接请求。上限必须在服务端契约中校验。
MetaLite 默认最多 100 条,是明确的容量保护,而不是由每个列表接口自行决定是否检查。
二、为什么 pageNo 也设置最大值
pageNo <= 1000 可以避免无限深翻页,但它只是保护线,不是深分页优化方案。
传统 offset 分页越往后,数据库可能需要跳过越多记录。真实的大数据列表更适合:
- 基于稳定排序键的游标分页;
- Elasticsearch
search_after; - 限制可查询时间范围;
- 异步导出而不是无限翻页。
最大页码能拒绝极端请求,却不能让第 1000 页变得便宜。
三、业务查询参数为什么继承分页基类
QueryUserParam 继承 PageQueryParam,再增加用户名、手机号、状态和组织条件:
java
public class QueryUserParam extends PageQueryParam {
private String userName;
private String phone;
private Integer status;
}
这样所有分页查询共享页码契约,业务字段仍保持类型明确。比使用一个 Map<String,Object> 更容易生成 OpenAPI 文档、执行 Bean Validation 和进行字段重构。
四、分页返回为什么统一为 total 加 list
PageResultDto<E> 只包含:
java
private long total;
private List<E> list;
前端可以根据 total 计算页数,列表数据又保持泛型类型。统一结构也让 InternalServiceClient.callOneInstanceRtnPageData 能按元素类型恢复 RPC 返回值。
是否额外返回 pageNo、pageSize 和 hasNext,取决于客户端需求。当前源码不返回这些字段,调用方需要保留自己的请求参数。
五、查询为什么先 count 再查列表
SysUserService.listUser 的流程是:
text
构造 Criteria
→ countByCriteria
→ total 为 0 时直接返回
→ 创建 Query 与排序
→ Pageable(pageNo, pageSize)
→ findListByQuery
先 count 可以避免无数据时继续查询列表,也满足精确页数展示。代价是每次分页通常执行两条 SQL;在高吞吐或复杂查询中,count 可能比列表本身更贵。
对于"加载更多"场景,可以查询 pageSize + 1 条只判断 hasNext,不一定需要精确 total。
六、稳定排序是分页正确性的前提
源码按更新时间倒序:
java
query.orderBy(OrderBy.desc(SysUserEntity::getUpdateTime));
如果多个记录更新时间相同,仅按这一列排序可能出现跨页重复或遗漏。更稳妥的排序应追加唯一键:
text
ORDER BY update_time DESC, id DESC
游标分页更需要稳定、唯一且方向一致的排序键。
分页不只是加 limit,还要定义数据变化时的顺序语义。
七、当前空结果还有一个契约细节
PageResultDto 默认构造时 list 为 null。部分 Service 在 total 为 0 时直接返回,因此调用方可能收到:
json
{"total":0,"list":null}
很多前端更希望固定收到空数组:
json
{"total":0,"list":[]}
统一空集合能减少调用方判空分支。当前实现尚未在 DTO 默认值上保证这一点,文章不能把它描述成已经完成的能力。
八、分页校验必须确保嵌套参数被触发
公网请求的业务参数先以明文或密文进入网关,解密后才生成内部 bizParam。真正的 @Min/@Max 校验必须发生在反序列化后的业务对象上。
MetaLite 的 InternalBizParamReq 对 bizParam 使用 @NotNull @Valid,并由入口参数处理器读取校验结果。缺少嵌套 @Valid 时,分页上限写在类上也可能完全不执行。
九、一套分页接口验收清单
发布前至少检查:
- pageNo、pageSize 是否有默认值、下限和上限;
- 排序字段是否稳定并包含唯一键;
- 深分页是否需要游标或时间范围;
- total 是否真的被业务需要;
- 空结果统一返回 null 还是空数组;
- 嵌套 Bean Validation 是否执行;
- 返回 Entity 是否可能暴露敏感字段;
- 大页请求是否会放大接口日志。
MetaLite 已把分页范围和通用返回结构放入工程基座。真正成熟的分页还需要结合数据库规模、排序稳定性和客户端交互继续设计。
十、把分页边界写成可重复执行的契约测试
分页接口至少要固定以下输入和结果:
| 用例 | 输入 | 应验证的行为 |
|---|---|---|
| 正常首页 | pageNo=1,pageSize=20 |
返回稳定排序的前 20 条,total 与同条件 count 一致 |
| 超大页容量 | pageSize 超过上限 |
在进入 DAO 前直接校验失败 |
| 非法页码 | 0、负数或超过约定上限 | 返回明确参数错误,不执行全表查询 |
| 删除中间数据 | 连续请求两页期间删除一条记录 | 明确接受偏移分页漂移,或改用游标方案 |
| 同排序值 | 多条记录业务排序相同 | 必须追加唯一字段排序,避免跨页重复 |
只验证返回 JSON 结构还不够。测试需要同时观察 SQL 是否带 LIMIT、count 与 list 是否使用同一 Criteria,以及空结果时 total/list 的契约是否稳定。
框架简介 MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。
源码基线 JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3,具体组件版本以项目 backend-bom 为准。
作者简介 15 年 Spring 体系企业级开发经验,专注于 Java 微服务架构、工程治理与生产实践。
持续更新 MetaLite 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。
在线演示 演示地址: admin.metalite.top/ 演示账号: guess 演示密码: admin@2026