Java案例如何实现分页封装?

wen java案例 8

Java案例如何实现分页封装?从零到一打造高效分页组件

目录导读

  1. 为什么需要分页封装?
  2. 分页封装的底层原理
  3. 案例实战:基于MyBatis-Plus的分页封装实现
  4. 分页封装的最佳实践与性能优化
  5. 常见问题与解答(FAQ)

为什么需要分页封装?

在实际Java开发中,分页查询是最常见也最容易“写废”的代码场景,没有封装的分页代码往往重复度高、扩展性差,关键是需要重复编写分页参数处理、SQL拼接、结果转换等逻辑,分页封装的本质是为了提高代码复用性、降低耦合度、统一分页接口规范

Java案例如何实现分页封装?

举个典型场景:电商系统中商品列表、订单列表、用户列表都需要分页,如果每个模块都各自实现一套分页逻辑,后期维护成本会指数级增长,通过封装一个通用分页组件,开发者只需传入分页参数和查询条件,即可获得标准分页结果。

核心目标:将分页请求参数(当前页码、每页条数)、数据库查询(总记录数、当前页数据)、结果返回(数据列表、总页数、总记录数)三个环节封装为统一模型。


分页封装的底层原理

在深入代码之前,需要理解分页封装的核心数据结构,一个标准分页组件通常包含两个核心类:

分页请求参数类(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:可以泛型化 PageResultPageResult<UserVO> 之外,再创建 extendFields 字段存放统计数据,或者使用装饰器模式为分页结果添加聚合查询结果。


分页封装是Java后端开发必备的抽象能力,通过本文的案例,我们从底层原理出发,构建了包含 PageRequestPageResult 和分页执行工具类的完整封装体系,并进一步探讨了性能优化、参数校验和FAQ,高效的分页组件不仅能减少代码重复,更能统一团队开发规范,是构建企业级应用的基石之一。

在真实项目中,建议将分页封装作为基础工具模块引入,并结合业务特点微调(如默认每页条数、最大页数限制),掌握本文的封装思路,即使未来更换ORM框架,核心设计逻辑依然适用。

抱歉,评论功能暂时关闭!