一、序列化的核心价值与分页流程设计
1. 序列化的核心作用
在分页场景中,序列化(如 PageSerializable
)不仅是简单的数据格式转换,更是系统性能与安全的重要保障。其核心价值体现在以下方面:
-
数据精简与传输优化
通过
PageSerializable
或自定义的PageVO
,过滤掉分页插件内部生成的中间数据(如countSql
、分页参数对象),仅保留total
(总条目数)和list
(当前页数据)。
示例对比:- 未序列化时返回的
PageInfo
对象包含 10+ 字段(如pageNum
、pageSize
、pages
、prePage
等),但前端通常仅需total
和list
。 - 序列化后响应数据量减少 50% 以上,显著降低网络传输开销。
- 未序列化时返回的
-
安全性与隐私保护
避免直接暴露 SQL 执行细节(如
orderBy
字段、count
语句),防止潜在的信息泄露风险。 -
标准化接口规范
统一所有分页接口的响应格式(如
code
、msg
、data
包裹PageVO
),提升前后端协作效率。
2. 分页全流程解析
完整的分页流程可分为 参数接收、校验、SQL 拦截、分页查询、结果封装 五个阶段,以下是详细步骤分解:
-
参数接收
- 前端通过 HTTP 请求传递分页参数(
pageNum
、pageSize
)和排序规则(columns
、orders
)。 - 后端通过
PageDTO
对象接收参数,并添加@Valid
注解触发自动校验。
- 前端通过 HTTP 请求传递分页参数(
-
参数校验
javapublic class PageDTO implements IPage { @NotNull(message = "pageNum 不能为空") private Integer pageNum; @NotNull(message = "pageSize 不能为空") @Max(value = 500, message = "每页大小不能超过500") private Integer pageSize; }
- 校验内容 :
- 非空校验(
pageNum
、pageSize
必填)。 - 数值范围校验(如
pageSize <= 500
防止内存溢出)。
- 非空校验(
- 校验方式 :
通过 Spring Validation 或自定义工具类实现。
- 校验内容 :
-
SQL 拦截与改写
PageHelper 通过 MyBatis 插件机制拦截 SQL 执行流程,核心步骤如下:
-
Step 1: 设置分页参数
javaPageHelper.startPage(pageDTO.getPageNum(), pageDTO.getPageSize());
将分页参数存储到 ThreadLocal 中,供后续插件读取。
-
Step 2: 解析原始 SQL
拦截Executor.query()
方法,获取待执行的 SQL 语句。 -
Step 3: 动态拼接 SQL
-
拼接
LIMIT
子句 :根据pageNum
和pageSize
计算偏移量。sql-- 原始 SQL SELECT * FROM user WHERE shop_id = 1; -- 改写后 SQL SELECT * FROM user WHERE shop_id = 1 LIMIT 0, 10;
-
自动生成
COUNT
查询 :用于计算总条目数。sqlSELECT COUNT(*) FROM user WHERE shop_id = 1;
-
-
-
分页查询执行
- 主查询 :执行带有
LIMIT
的 SQL,获取当前页数据。 - COUNT 查询 :自动执行并填充总条目数(
total
)。
- 主查询 :执行带有
-
结果封装
javapublic class PageVO<T> { private Long total; // 总条目数 private List<T> list; // 当前页数据 }
- 将
PageHelper
返回的Page
或PageSerializable
转换为前端友好的PageVO
。 - 关键计算 :总页数
pages = ceil(total / pageSize)
。
- 将
二、核心代码实现与原理剖析
1. DTO 与 VO 的详细设计
-
PageDTO
:分页请求参数载体javapublic class PageDTO implements IPage { @NotNull(message = "pageNum 不能为空") @ApiModelProperty(value = "当前页", required = true) private Integer pageNum; @NotNull(message = "pageSize 不能为空") @ApiModelProperty(value = "每页大小", required = true) @Max(value = 500, message = "每页大小不能超过500") private Integer pageSize; @ApiModelProperty(value = "排序字段数组,用逗号分割") private String[] columns; @ApiModelProperty(value = "排序字段方式,用逗号分割,ASC正序,DESC倒序") private String[] orders; // 实现 IPage 接口方法(供 PageHelper 调用) @Override public Integer getPageNum() { return this.pageNum; } @Override public Integer getPageSize() { return this.pageSize; } }
- 作用:统一接收分页参数,支持动态排序规则。
- 校验扩展 :可通过
@Pattern
校验columns
和orders
的合法性。
-
PageVO
:分页响应结构javapublic class PageVO<T> { @ApiModelProperty("总页数") private Integer pages; @ApiModelProperty("总条目数") private Long total; @ApiModelProperty("结果集") private List<T> list; // 计算总页数 public static Integer calcPages(Long total, Integer pageSize) { if (total == 0 || pageSize == 0) return 0; return (int) Math.ceil((double) total / pageSize); } }
- 扩展性 :可添加
pageNum
、pageSize
字段,满足部分前端需求。
- 扩展性 :可添加
2. 分页工具类 PageUtil 实现
java
public class PageUtil {
/**
* 执行分页查询并封装结果
* @param pageDTO 分页参数
* @param select 查询逻辑(Lambda 或匿名类)
*/
public static <T> PageVO<T> doPage(PageDTO pageDTO, ISelect select) {
// 1. 参数校验
validatePageSize(pageDTO.getPageSize());
// 2. 启动分页(PageHelper 在此处拦截 SQL)
Page<T> page = PageHelper.startPage(pageDTO.getPageNum(), pageDTO.getPageSize());
// 3. 动态拼接排序(防止 SQL 注入)
applyOrderBy(pageDTO);
// 4. 执行查询(触发分页逻辑)
PageSerializable<T> simplePageInfo = page.doSelectPageSerializable(select);
// 5. 封装结果
return buildPageVO(simplePageInfo, pageDTO);
}
// 校验每页大小合法性
private static void validatePageSize(Integer pageSize) {
if (pageSize > PageDTO.MAX_PAGE_SIZE) {
throw new IllegalArgumentException("每页大小不能超过 " + PageDTO.MAX_PAGE_SIZE);
}
}
// 动态拼接 ORDER BY 子句
private static void applyOrderBy(PageDTO pageDTO) {
if (pageDTO.getColumns() == null || pageDTO.getOrders() == null) return;
// 校验字段与排序方式数量一致
if (pageDTO.getColumns().length != pageDTO.getOrders().length) {
throw new IllegalArgumentException("排序字段与排序方式数量不匹配");
}
// 拼接排序语句(示例:name ASC, age DESC)
String orderBy = IntStream.range(0, pageDTO.getColumns().length)
.mapToObj(i -> {
String column = safeColumn(pageDTO.getColumns()[i]);
String order = safeOrder(pageDTO.getOrders()[i]);
return column + " " + order;
})
.collect(Collectors.joining(", "));
PageHelper.orderBy(orderBy);
}
// 防止 SQL 注入(字段白名单校验)
private static String safeColumn(String column) {
Set<String> allowedColumns = Set.of("name", "age", "create_time");
if (!allowedColumns.contains(column)) {
throw new IllegalArgumentException("非法排序字段: " + column);
}
return column;
}
// 校验排序方式合法性
private static String safeOrder(String order) {
if (!"ASC".equalsIgnoreCase(order) && !"DESC".equalsIgnoreCase(order)) {
throw new IllegalArgumentException("非法排序方式: " + order);
}
return order;
}
// 构建 PageVO
private static <T> PageVO<T> buildPageVO(PageSerializable<T> pageInfo, PageDTO pageDTO) {
PageVO<T> pageVO = new PageVO<>();
pageVO.setList(pageInfo.getList());
pageVO.setTotal(pageInfo.getTotal());
pageVO.setPages(PageVO.calcPages(pageInfo.getTotal(), pageDTO.getPageSize()));
return pageVO;
}
}
3. 关键代码详解:ISelect 接口与 Lambda 实现
-
ISelect 接口源码
java@FunctionalInterface public interface ISelect { void doSelect(); }
- 函数式接口 :仅有一个抽象方法
doSelect()
,可用 Lambda 表达式替代匿名类。
- 函数式接口 :仅有一个抽象方法
-
Lambda 表达式本质
以下两种写法完全等价:
java// 写法1:Lambda 表达式 PageUtil.doPage(pageDTO, () -> userMapper.selectByShopId(shopId)); // 写法2:匿名内部类 PageUtil.doPage(pageDTO, new ISelect() { @Override public void doSelect() { userMapper.selectByShopId(shopId); } });
- 执行时机 :
doSelect()
方法在doSelectPageSerializable()
中触发,确保分页参数已设置到 ThreadLocal。
- 执行时机 :
三、PageHelper 的 SQL 拦截机制
1. 核心原理
PageHelper 基于 MyBatis 的 插件(Interceptor) 机制,通过动态代理修改 SQL 执行流程:
- 拦截目标方法 :
拦截Executor.query()
,在 SQL 执行前修改语句。 - ThreadLocal 传递参数 :
PageHelper.startPage()
将分页参数存储到ThreadLocal<Page>
中,插件从该变量读取参数。 - SQL 解析与改写 :
- 使用 JSqlParser 解析原始 SQL,生成语法树。
- 插入
LIMIT
子句和ORDER BY
子句(如指定排序)。
- 执行 COUNT 查询 :
自动生成SELECT COUNT(*) FROM (...)
语句获取总条目数。
2. 拦截流程示例
原始 SQL:
sql
SELECT id, name, age FROM user WHERE shop_id = 1
拦截后流程:
-
生成 COUNT 查询 :
sqlSELECT COUNT(*) FROM (SELECT id, name, age FROM user WHERE shop_id = 1) tmp_count
-
改写主查询 :
sqlSELECT id, name, age FROM user WHERE shop_id = 1 LIMIT 0, 10
四、最佳实践与扩展建议
1. 分页性能优化
- 禁用 COUNT 查询 :
对于极大数据量场景,可通过PageHelper.startPage(1, 10, false)
跳过 COUNT 查询。 - 手动优化 COUNT SQL :
在 Mapper 中自定义@SelectProvider
生成高效的 COUNT 语句。
2. 复杂排序支持
- 多字段排序 :
前端传递columns=name,age&orders=ASC,DESC
,后端拼接为ORDER BY name ASC, age DESC
。 - 嵌套排序 :
使用PageHelper.orderBy("(age - 10) DESC")
支持表达式排序。
3. 异常处理
- 全局异常拦截 :
捕获IllegalArgumentException
并返回标准错误响应。 - SQL 注入防御 :
通过白名单校验columns
字段(如safeColumn()
方法)。
五、总结
通过 DTO 参数规范、VO 响应封装、PageUtil 工具类 的分层设计,结合 PageHelper 的 SQL 拦截机制,开发者可以:
- 减少重复代码:统一分页逻辑,Service 层只需关注业务查询。
- 提升安全性:参数校验、排序字段白名单、SQL 注入防御层层把关。
- 优化性能:自动化的 SQL 改写与结果封装,降低开发复杂度。
最终实现 高效、安全、易维护 的分页功能,满足大多数企业级应用的需求。