Java案例如何实现分页封装?从零到一打造高效分页组件
目录导读
为什么需要分页封装?
在实际Java开发中,分页查询是最常见也最容易“写废”的代码场景,没有封装的分页代码往往重复度高、扩展性差,关键是需要重复编写分页参数处理、SQL拼接、结果转换等逻辑,分页封装的本质是为了提高代码复用性、降低耦合度、统一分页接口规范。

举个典型场景:电商系统中商品列表、订单列表、用户列表都需要分页,如果每个模块都各自实现一套分页逻辑,后期维护成本会指数级增长,通过封装一个通用分页组件,开发者只需传入分页参数和查询条件,即可获得标准分页结果。
核心目标:将分页请求参数(当前页码、每页条数)、数据库查询(总记录数、当前页数据)、结果返回(数据列表、总页数、总记录数)三个环节封装为统一模型。
分页封装的底层原理
在深入代码之前,需要理解分页封装的核心数据结构,一个标准分页组件通常包含两个核心类:
分页请求参数类(PageRequest)
public class PageRequest {
private int pageNum; // 当前页码(从1开始)
private int pageSize; // 每页记录数
private String orderBy; // 排序字段
private boolean asc; // 是否升序
}
分页结果类(PageResult)
public class PageResult<T> {
private List<T> list; // 当前页数据
private int totalCount; // 总记录数
private int totalPages; // 总页数
private int currentPage; // 当前页码
private int pageSize; // 每页条数
private boolean hasPrevious; // 是否有上一页
private boolean hasNext; // 是否有下一页
}
底层逻辑:数据库分页通常依赖SQL的 LIMIT offset, pageSize(MySQL)或 OFFSET FETCH(PostgreSQL)语法,封装层需要计算 offset = (pageNum - 1) * pageSize,同时执行两次查询:一次查询总记录数,一次查询当前页数据。
案例实战:基于MyBatis-Plus的分页封装实现
下面以MyBatis-Plus(业界主流ORM框架)为例,展示完整的分页封装流程,MyBatis-Plus本身提供了 IPage 接口,但我们可以进一步封装使其更符合业务规范。
步骤1:创建基础分页泛型类
@Data
public class PageResult<T> implements Serializable {
private long total;
private long pageSize;
private long currentPage;
private long totalPages;
private List<T> records;
private boolean hasNext;
public PageResult(long total, long pageSize, long currentPage, List<T> records) {
this.total = total;
this.pageSize = pageSize;
this.currentPage = currentPage;
this.records = records;
this.totalPages = (long) Math.ceil((double) total / pageSize);
this.hasNext = currentPage < totalPages;
}
}
步骤2:封装分页查询工具方法
@Component
public class PageUtils {
public <T> PageResult<T> executePage(PageRequest request,
Function<Page<T>, Page<T>> mapper) {
// 构建MyBatis-Plus的Page对象
Page<T> page = new Page<>(request.getPageNum(), request.getPageSize());
// 执行查询(由具体业务Service实现查询逻辑)
Page<T> result = mapper.apply(page);
// 转换为统一分页结果
return new PageResult<>(
result.getTotal(),
result.getSize(),
result.getCurrent(),
result.getRecords()
);
}
}
步骤3:业务层使用示例
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper; // MyBatis-Plus自动生成
@Autowired
private PageUtils pageUtils;
@Override
public PageResult<UserVO> getUsersByPage(PageRequest request, String name) {
// 使用Lambda查询条件
return pageUtils.executePage(request, page -> {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
if (StringUtils.isNotBlank(name)) {
wrapper.like(User::getName, name);
}
// MyBatis-Plus自动执行分页和计数
return userMapper.selectPage(page, wrapper);
});
}
}
关键要点:
- 通过
Function<Page<T>, Page<T>>使得分页查询逻辑完全解耦 - MyBatis-Plus的
selectPage内部自动执行了总记录数查询和分页查询 - 排序逻辑可通过
PageRequest中的orderBy字段动态拼接
扩展:手动实现SQL分页(不依赖ORM)
如果公司使用原生JDBC或JPA,可以手动计算offset:
public class PaginationHelper {
public static String buildLimitClause(int pageNum, int pageSize) {
int offset = (pageNum - 1) * pageSize;
return String.format("LIMIT %d OFFSET %d", pageSize, offset);
}
}
分页封装的最佳实践与性能优化
避免深度分页带来的性能问题
当 pageNum 非常大时(如第1000页),OFFSET 为 (1000-1)*20=19980,数据库需要扫描大量无效行,解决方案:
- 限制最大页数:在
PageRequest构造函数中校验pageNum <= 1000 - 使用游标分页(Keyset Pagination):基于上一页最后一条记录的ID或时间戳进行查询,
WHERE id > lastId LIMIT 20
参数校验与默认值
public class PageRequest {
private static final int DEFAULT_PAGE_SIZE = 20;
private static final int MAX_PAGE_SIZE = 100;
public PageRequest(Integer pageNum, Integer pageSize) {
this.pageNum = (pageNum == null || pageNum < 1) ? 1 : pageNum;
this.pageSize = (pageSize == null || pageSize < 1) ? DEFAULT_PAGE_SIZE :
Math.min(pageSize, MAX_PAGE_SIZE);
}
}
结果缓存与计数优化
- 对于数据改动不频繁的分页接口,可将总记录数缓存到Redis,减少
COUNT(*)查询 - 使用
SELECT COUNT(1)代替SELECT COUNT(*),部分数据库下性能更好
封装与框架的兼容性
- 如果前端框架(如Element Plus)要求返回特定字段名,可在
PageResult中通过@JsonProperty或@JsonInclude调整 - 支持通过Spring MVC拦截器自动提取分页参数(配合
HandlerMethodArgumentResolver)
常见问题与解答(FAQ)
Q1:分页封装一定要用MyBatis-Plus吗?
A:不必须,本文以MyBatis-Plus为例是因为其封装度较高,但核心的分页请求/结果类、LIMIT/OFFSET 计算逻辑完全可以独立实现,Java EE场景下,Spring Data JPA的 Pageable 接口同样可用。
Q2:如果数据库总记录数很大,每次都count很慢怎么办?
A:这是高频问题,建议采用以下策略的组合:(1)将总记录数缓存到Redis并设置合理过期时间;(2)限制最大查询页数为100页;(3)业务上允许时使用近似值(如MySQL的 EXPLAIN 预估行数),对于超大表,游标分页更合适。
Q3:如何让前端动态传排序字段?
A:在 PageRequest 中添加 orderBy 字段,并使用白名单校验(防止SQL注入),示例:
// 仅允许预定义的字段排序
if (!ALLOWED_SORT_FIELDS.contains(request.getOrderBy())) {
throw new BizException("非法排序字段");
}
Q4:分页封装后,如何返回额外统计信息?
A:可以泛型化 PageResult,PageResult<UserVO> 之外,再创建 extendFields 字段存放统计数据,或者使用装饰器模式为分页结果添加聚合查询结果。
分页封装是Java后端开发必备的抽象能力,通过本文的案例,我们从底层原理出发,构建了包含 PageRequest、PageResult 和分页执行工具类的完整封装体系,并进一步探讨了性能优化、参数校验和FAQ,高效的分页组件不仅能减少代码重复,更能统一团队开发规范,是构建企业级应用的基石之一。
在真实项目中,建议将分页封装作为基础工具模块引入,并结合业务特点微调(如默认每页条数、最大页数限制),掌握本文的封装思路,即使未来更换ORM框架,核心设计逻辑依然适用。