本文目录导读:

针对Java分页执行流程的规整化,核心在于解耦(将分页逻辑与业务逻辑分离)、统一(使用标准接口与DTO)和高效(避免内存分页,使用数据库原生分页)。
以下是经过业界验证的规整化方案,主要涵盖后端(Spring Boot + MyBatis/MyBatis-Plus)的典型场景。
规整化的核心原则
- 统一参数与返回:所有分页接口统一接收
PageParam(当前页、每页大小),返回PageResult(分页数据、总条数、总页数等)。 - SQL层分页:坚决避免
List.stream().skip().limit()这种方式(性能极差),必须使用数据库的LIMIT/OFFSET或游标分页。 - 使用现成ORM框架:优先使用 MyBatis-Plus / JPA 的分页机制,避免手动拼装
PageHelper或手写RowBounds(容易出问题)。 - DTO与VO分离:数据库实体(DO)不应直接返回前端,需要转换为视图对象(VO)。
标准分页执行流程(MyBatis-Plus为例)
分页入参出参定义(规整的基础)
// 1. 通用分页请求参数
@Data
public class PageParam {
@Min(1) private Integer pageNo = 1;
@Min(1) @Max(500) private Integer pageSize = 20;
// 可扩展排序字段:private String orderBy; private Boolean asc;
}
// 2. 通用分页返回结果
@Data
public class PageResult<T> {
private List<T> records;
private Integer pageNo;
private Integer pageSize;
private Long total;
private Integer pages; // 总页数
public PageResult(List<T> records, Long total, PageParam param) {
this.records = records;
this.total = total;
this.pageNo = param.getPageNo();
this.pageSize = param.getPageSize();
this.pages = (int) Math.ceil((double) total / param.getPageSize());
}
}
Controller层:接收参数并封装
@RestController
public class UserController {
@GetMapping("/users")
// 统一使用 @Validated 校验分页参数
public Result<PageResult<UserVO>> listUsers(@Validated PageParam pageParam,
@RequestParam(required = false) String name) {
// 调用Service层,Controller只负责接收和返回,不做业务处理
PageResult<UserVO> page = userService.queryUserPage(pageParam, name);
return Result.success(page);
}
}
/*
优点:Controller 代码极短,不感知 MyBatis-Plus 的 Page 对象
*/
Service层:转换与装配
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public PageResult<UserVO> queryUserPage(PageParam pageParam, String name) {
// 1. 构建 MyBatis-Plus 的 Page 对象(这一步需要 ORM 框架的分页插件)
// Page<UserDO> 是 MyBatis-Plus 提供的,内部会自动处理 limit
Page<UserDO> mybatisPage = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
// 2. 执行分页查询(传入 Page 对象,无需手动计算 offset)
// Mapper 方法返回 Page<UserDO>,其中已包含 total 和 records
Page<UserDO> resultPage = userMapper.selectUserPage(mybatisPage, name);
// 3. 将 DO 转换为 VO(规整的关键步骤)
List<UserVO> voList = resultPage.getRecords().stream()
.map(this::convertToVO) // 可使用 BeanUtils.copyProperties 或 MapStruct
.collect(Collectors.toList());
// 4. 返回统一的 PageResult (解耦,不暴露 MyBatis 的 Page 给 Controller)
return new PageResult<>(voList, resultPage.getTotal(), pageParam);
}
}
Mapper层:纯 SQL 或 MyBatis-Plus 语法
MyBatis-Plus 方法(推荐,无需写XML)
// Mapper 接口
@Mapper
public interface UserMapper extends BaseMapper<UserDO> {
// 使用 MyBatis-Plus 的 QueryWrapper 分页
default Page<UserDO> selectUserPage(Page<UserDO> page, String name) {
return this.selectPage(page,
new LambdaQueryWrapper<UserDO>()
.like(StringUtils.isNotBlank(name), UserDO::getName, name)
.orderByDesc(UserDO::getCreateTime));
}
}
// 注意:需要在配置类中启用 MyBatis-Plus 分页插件
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
手写 XML(复杂查询时使用)
<!-- UserMapper.xml -->
<select id="selectUserPage" resultType="com.example.entity.UserDO">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY create_time DESC
<!-- 这里不需要写 limit,MyBatis-Plus 分页插件会自动在尾部追加 LIMIT 和 COUNT 查询 -->
</select>
规整化流程的“避坑”与进阶
计算总条数的性能优化(count优化)
- 问题:MyBatis-Plus 默认会执行
SELECT COUNT(*)全表扫描。 - 优化方案:
- 根据业务判断:如果
pageSize很小(如固定20条),且用户极少翻到第1000页,可配置page.setOptimizeCountSql(false)。 - 如果数据量极大(如千万级),弃用传统分页,改用 游标分页(Keyset Pagination):基于上一页最后一条记录的ID查询
WHERE id > ? LIMIT 20,这种方式无需count,且性能稳定O(1)。 - 规整建议:在
PageResult中加入isOptimized标识,前端判断如果总页数过大,不显示具体总条数。
- 根据业务判断:如果
统一处理排序
- 不要让每个Mapper都写死排序字段。
- 规整做法:在
PageParam中增加orderBy和asc字段,Service层统一构建OrderItem后设置到Page对象。
批量操作与分页冲突
- 问题:
PageHelper的ThreadLocal机制在异步或批处理时易导致分页错乱。 - 规整建议:全局统一使用 MyBatis-Plus 的
Page对象作为方法参数(显式传递),彻底弃用PageHelper.startPage()。
最终效果对比
| 场景 | 混乱的写法 | 规整写法 |
|---|---|---|
| Controller方法 | PageInfo<User> 直接返回给前端 |
返回通用的 PageResult<UserVO> |
| 参数校验 | 手动判断 pageNo > 0 |
使用 @Validated + PageParam统一校验 |
| 分页逻辑 | 在Service里自己算 offset + limit | 传入 MyBatis-Plus 的 Page 对象,插件自动处理 |
| 返回数据 | 返回的list包含DO实体,暴露密码等敏感字段 |
VO转换,只返回前端需要的字段 |
| 耦合度 | Service里new PageInfo()依赖了PageHelper |
Service只依赖自己的DTO和MyBatis-Plus的Page,容易替换 |
规整的Java分页流程 = PageParam作为入参 → ORM分页插件执行 → DO转VO → PageResult作为出参。
只要团队遵循这个流程,任何后端成员都能快速看懂、修改分页代码,且不会出现慢SQL或数据泄露问题。