Spring Boot MongoDB 分页工具类封装 (新手指南)
目录
- 引言:为何需要分页工具类?
- [工具类一:
PaginationUtils
- 简化Pageable
创建](#工具类一:PaginationUtils - 简化 Pageable 创建)- 设计目标
- 代码实现 (
PaginationUtils.java
) - [如何使用
PaginationUtils
](#如何使用 PaginationUtils)
- [工具类二:
PageResponse<T>
- 标准化分页响应 DTO](#工具类二:PageResponse<T> - 标准化分页响应 DTO)- 设计目标
- 代码实现 (
PageResponse.java
) - [如何使用
PageResponse<T>
](#如何使用 PageResponse<T>)
- [整合示例:在 Controller 和 Service 中使用工具类](#整合示例:在 Controller 和 Service 中使用工具类)
- [Controller 层 (
UserController.java
)](#Controller 层 (UserController.java)) - [Service 层 (
UserService.java
)](#Service 层 (UserService.java))
- [Controller 层 (
- 重点内容总结
- 结语
1. 引言:为何需要分页工具类?
在之前的笔记中,我们学习了如何使用 Pageable
和 Page
来实现分页。虽然 Spring Data 提供了基础,但在实际项目中,我们经常会重复以下操作:
- 从 HTTP 请求参数(页码、大小、排序字段、排序方向)创建
Pageable
对象。 - 处理 用户友好的 1-based 页码 与 Spring Data 内部 0-based 页码 之间的转换。
- 处理排序参数的解析和
Sort
对象的构建。 - 可能希望 标准化 API 返回的分页信息格式 ,使其更简洁或符合前端特定需求,而不是直接暴露 Spring Data 的
Page
结构。
封装分页工具类可以:
- 减少重复代码:将通用的分页逻辑集中处理。
- 提高可读性:使 Service 和 Controller 层的代码更专注于业务逻辑。
- 统一规范:确保项目中分页参数处理和响应格式的一致性。
- 简化使用:让新手更容易地实现正确的分页。
2. 工具类一:PaginationUtils
- 简化 Pageable
创建
这个工具类的主要职责是接收前端传入的原始分页参数,并安全、便捷地将其转换为 Spring Data 所需的 Pageable
对象。
设计目标
- 接收 1-based 页码,内部转换为 0-based。
- 提供默认的页码和页面大小。
- 支持单个或多个排序字段及方向的解析。
- 处理无效或缺失的参数,提供合理的默认行为(如不排序)。
代码实现 (PaginationUtils.java
)
java
package com.example.yourproject.utils; // 根据你的项目结构调整包名
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.util.CollectionUtils; // Spring 提供的集合工具类
import org.springframework.util.StringUtils; // Spring 提供的字符串工具类
import java.util.ArrayList;
import java.util.List;
/**
* 分页工具类
*
* 帮助从原始请求参数创建 Spring Data Pageable 对象。
* 核心功能:处理 1-based 页码转换、默认值、排序参数解析。
*/
public final class PaginationUtils { // final class, 防止被继承
// 默认页码(用户传入的第一页)
private static final int DEFAULT_PAGE_NUMBER = 1;
// 默认每页大小
private static final int DEFAULT_PAGE_SIZE = 10;
// 默认排序方向
private static final Sort.Direction DEFAULT_SORT_DIRECTION = Sort.Direction.ASC;
// 排序参数中,字段和方向的分隔符(例如 "name,desc")
private static final String SORT_DELIMITER = ",";
/**
* 私有构造函数,防止实例化工具类
*/
private PaginationUtils() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
/**
* 创建 Pageable 对象。
*
* @param pageInput 用户传入的页码 (从 1 开始计数)。如果为 null 或小于 1,则使用默认值 1。
* @param sizeInput 用户传入的每页大小。如果为 null 或小于 1,则使用默认值 10。
* @param sortInput 排序参数列表。每个字符串格式为 "fieldName,direction" (例如 "name,asc", "age,desc")。
* direction 可以省略,默认为升序 (ASC)。
* 如果列表为空或 null,则不进行排序。
* @return 配置好的 Pageable 对象。
*/
public static Pageable createPageable(Integer pageInput, Integer sizeInput, List<String> sortInput) {
// 1. 验证并设置页码 (转换为 0-based)
// 如果 pageInput 为 null 或 < 1,则使用默认页码 1。然后减 1 得到 0-based 页码。
int page = (pageInput == null || pageInput < 1) ? DEFAULT_PAGE_NUMBER - 1 : pageInput - 1;
// 2. 验证并设置页面大小
// 如果 sizeInput 为 null 或 < 1,则使用默认大小 10。
int size = (sizeInput == null || sizeInput < 1) ? DEFAULT_PAGE_SIZE : sizeInput;
// 3. 解析排序参数
Sort sort = parseSort(sortInput);
// 4. 创建并返回 PageRequest 对象
return PageRequest.of(page, size, sort);
}
/**
* 创建带有默认排序的 Pageable 对象(如果未提供排序参数)。
*
* @param pageInput 用户传入的页码 (从 1 开始计数)。
* @param sizeInput 用户传入的每页大小。
* @param sortInput 排序参数列表 (同上)。
* @param defaultSort 默认的 Sort 对象。如果 sortInput 为空或无效,则使用此默认排序。
* @return 配置好的 Pageable 对象。
*/
public static Pageable createPageable(Integer pageInput, Integer sizeInput, List<String> sortInput, Sort defaultSort) {
int page = (pageInput == null || pageInput < 1) ? DEFAULT_PAGE_NUMBER - 1 : pageInput - 1;
int size = (sizeInput == null || sizeInput < 1) ? DEFAULT_PAGE_SIZE : sizeInput;
Sort sort = parseSort(sortInput);
// 如果解析出的 sort 是 unsorted,并且提供了默认 sort,则使用默认 sort
if (sort.isUnsorted() && defaultSort != null && defaultSort.isSorted()) {
sort = defaultSort;
}
return PageRequest.of(page, size, sort);
}
/**
* 解析排序参数字符串列表,构建 Sort 对象。
*
* @param sortParams 排序参数列表,每个元素如 "field,direction" 或 "field"。
* @return 如果参数有效则返回 Sort 对象,否则返回 Sort.unsorted()。
*/
private static Sort parseSort(List<String> sortParams) {
if (CollectionUtils.isEmpty(sortParams)) {
return Sort.unsorted(); // 没有提供排序参数,返回不排序
}
List<Sort.Order> orders = new ArrayList<>();
for (String sortParam : sortParams) {
// 跳过空或空白的排序参数
if (!StringUtils.hasText(sortParam)) {
continue;
}
String[] parts = sortParam.split(SORT_DELIMITER);
// 第一个部分是字段名,必须存在
String property = parts[0].trim();
if (!StringUtils.hasText(property)) {
continue; // 字段名为空,跳过
}
// 获取排序方向,如果没提供或无效,则使用默认方向
Sort.Direction direction = DEFAULT_SORT_DIRECTION;
if (parts.length > 1 && StringUtils.hasText(parts[1])) {
// 尝试从字符串解析方向 (忽略大小写)
try {
direction = Sort.Direction.fromString(parts[1].trim());
} catch (IllegalArgumentException e) {
// 如果方向字符串无效 (例如 "ascend", "descend"), 使用默认值
System.err.println("Invalid sort direction: " + parts[1] + ". Using default: " + DEFAULT_SORT_DIRECTION);
// 这里可以选择打印日志或忽略错误
}
}
// 创建 Sort.Order 对象并添加到列表
orders.add(new Sort.Order(direction, property));
}
// 如果成功解析出至少一个有效的 Order,则创建 Sort 对象,否则返回 unsorted
return orders.isEmpty() ? Sort.unsorted() : Sort.by(orders);
}
}
注释说明:
final class
/private constructor
: 这是工具类的标准写法,表明它不应该被继承或实例化。所有方法都是静态的。- 常量定义: 定义默认值和分隔符,方便修改和维护。
createPageable
方法 :- 接收
Integer
类型的pageInput
和sizeInput
,允许传入null
。 - 核心 :
page = (pageInput == null || pageInput < 1) ? DEFAULT_PAGE_NUMBER - 1 : pageInput - 1;
这行处理了null
值、小于 1 的无效值,并将用户习惯的 1-based 页码转换成 Spring Data 需要的 0-based 页码。 - 调用
parseSort
方法处理排序参数。 - 最后使用
PageRequest.of(page, size, sort)
创建Pageable
实例。
- 接收
createPageable
(重载) : 提供了一个可以传入默认Sort
对象的版本,当用户没有提供任何有效排序时,可以使用这个默认排序。parseSort
方法 :- 处理
null
或空列表的情况,返回Sort.unsorted()
。 - 遍历
sortInput
列表中的每个字符串。 - 使用
split(SORT_DELIMITER)
分割字段名和方向。 - 进行健壮性检查 (字段名是否为空)。
- 解析排序方向,使用
Sort.Direction.fromString()
并包含try-catch
处理无效方向字符串,回退到默认方向。 - 创建
Sort.Order
对象。 - 最后使用
Sort.by(orders)
将所有有效的Order
组合成Sort
对象。如果列表为空,则Sort.by()
返回Sort.unsorted()
。
- 处理
- 依赖 : 使用了 Spring Framework 提供的
StringUtils
和CollectionUtils
进行空值检查,这是推荐的做法。
如何使用 PaginationUtils
在你的 Service 层 或 Controller 层(取决于你处理分页参数的位置,通常在 Service 层更合适),你可以这样调用:
java
// 假设在 Service 层
import com.example.yourproject.utils.PaginationUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.util.List;
// ...
public Page<User> findUsersPaginated(Integer page, Integer size, List<String> sort) {
// 使用工具类创建 Pageable 对象
// 假设我们希望默认按 "createdAt" 降序排序
Sort defaultSort = Sort.by(Sort.Direction.DESC, "createdAt");
Pageable pageable = PaginationUtils.createPageable(page, size, sort, defaultSort);
// 现在可以直接将 pageable 传递给 Repository 方法
return userRepository.findAll(pageable); // 或者 userRepository.findByXXX(..., pageable);
}
3. 工具类二:PageResponse<T>
- 标准化分页响应 DTO
这个类是一个数据传输对象 (DTO),用于包装分页查询的结果,提供一个统一、简洁的 JSON 结构给前端。
设计目标
- 封装当前页的数据列表 (
List<T>
)。 - 提供清晰的分页元数据(当前页码 (1-based)、每页大小、总记录数、总页数、是否首页/末页)。
- 隐藏 Spring Data
Page
对象的内部复杂结构。 - 提供一个静态工厂方法,方便从
Page<T>
对象转换。
代码实现 (PageResponse.java
)
java
package com.example.yourproject.dto; // DTO 通常放在 dto 包下
import org.springframework.data.domain.Page;
import java.util.List;
/**
* 标准化的分页响应体 DTO (Data Transfer Object)
*
* 用于封装分页查询结果,提供给 API 调用者一个简洁一致的结构。
*
* @param <T> 数据的实体类型
*/
public class PageResponse<T> {
private final List<T> content; // 当前页的数据列表
private final int currentPage; // 当前页码 (从 1 开始)
private final int pageSize; // 每页大小
private final long totalElements; // 总记录数
private final int totalPages; // 总页数
private final boolean first; // 是否为第一页
private final boolean last; // 是否为最后一页
/**
* 构造函数 (私有或包私有,推荐使用静态工厂方法创建)
*/
private PageResponse(List<T> content, int currentPage, int pageSize, long totalElements, int totalPages, boolean first, boolean last) {
this.content = content;
this.currentPage = currentPage;
this.pageSize = pageSize;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.first = first;
this.last = last;
}
/**
* 静态工厂方法:从 Spring Data Page 对象创建 PageResponse 对象。
* @param page Spring Data 的 Page 对象
* @param <T> 数据的实体类型
* @return 转换后的 PageResponse 对象
*/
public static <T> PageResponse<T> fromPage(Page<T> page) {
if (page == null) {
// 可以选择返回 null,或者一个表示空的 PageResponse,取决于你的 API 设计
return new PageResponse<>(List.of(), 1, 0, 0L, 0, true, true); // 返回一个空响应示例
// throw new IllegalArgumentException("Page object cannot be null");
}
return new PageResponse<>(
page.getContent(), // 获取数据列表
page.getNumber() + 1, // **核心转换:0-based to 1-based**
page.getSize(), // 获取页面大小
page.getTotalElements(), // 获取总记录数
page.getTotalPages(), // 获取总页数
page.isFirst(), // 是否第一页
page.isLast() // 是否最后页
);
}
// --- Getters ---
public List<T> getContent() {
return content;
}
public int getCurrentPage() {
return currentPage;
}
public int getPageSize() {
return pageSize;
}
public long getTotalElements() {
return totalElements;
}
public int getTotalPages() {
return totalPages;
}
public boolean isFirst() {
return first;
}
public boolean isLast() {
return last;
}
/*
* 预期 JSON 输出示例:
* {
* "content": [ ... data objects ... ],
* "currentPage": 1,
* "pageSize": 10,
* "totalElements": 153,
* "totalPages": 16,
* "first": true,
* "last": false
* }
*/
}
注释说明:
- 泛型
<T>
: 使PageResponse
可以用于任何类型的分页数据。 - 字段 : 只包含前端最关心的分页信息,字段名清晰。
currentPage
特别设计为 1-based。 - 构造函数 : 设为
private
,强制使用静态工厂方法创建实例,这是一种良好的实践。 fromPage
静态工厂方法 :- 接收一个 Spring Data
Page<T>
对象作为输入。 - 做了
null
检查。 - 核心 :
page.getNumber() + 1
将 Spring Data 的 0-based 页码转换为用户友好的 1-basedcurrentPage
。 - 从
Page<T>
对象中提取所有需要的数据,填充到PageResponse
的字段中。
- 接收一个 Spring Data
- Getters: 提供公共的 getter 方法,以便 Jackson 等库能正确地将对象序列化为 JSON。
- JSON 示例: 注释中给出了预期的 JSON 输出格式,方便前后端对接。
如何使用 PageResponse<T>
在你的 Service 层 方法中,当从 Repository 获取到 Page<T>
对象后,使用 PageResponse.fromPage()
进行转换。然后 Controller 层 直接返回这个 PageResponse<T>
对象。
java
// 假设在 Service 层
import com.example.yourproject.dto.PageResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
// ...
public PageResponse<User> findUsersPaginatedAndWrapped(Integer page, Integer size, List<String> sort) {
Sort defaultSort = Sort.by(Sort.Direction.DESC, "createdAt");
Pageable pageable = PaginationUtils.createPageable(page, size, sort, defaultSort);
// 1. 从 Repository 获取 Page<User> 对象
Page<User> userPage = userRepository.findAll(pageable);
// 2. 使用 PageResponse.fromPage() 转换
PageResponse<User> pageResponse = PageResponse.fromPage(userPage);
// 3. 返回转换后的 PageResponse 对象
return pageResponse;
}
4. 整合示例:在 Controller 和 Service 中使用工具类
下面展示如何在 Controller 和 Service 中协同使用这两个工具类。
Controller 层 (UserController.java
)
Controller 负责接收 HTTP 请求参数,并将其传递给 Service 层。注意 @RequestParam
中 sort
参数的处理。
java
package com.example.yourproject.controller;
import com.example.yourproject.dto.PageResponse; // 导入 DTO
import com.example.yourproject.model.User;
import com.example.yourproject.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List; // 需要导入 List
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService; // 假设 UserService 已更新
/**
* 获取用户列表 (使用分页工具类)
*
* @param page API 接收的页码 (通常从 1 开始)。
* @param size 每页大小。
* @param sort 排序参数列表。可以接收多个 sort 参数,
* 例如: /api/users?sort=name,asc&sort=age,desc
* Spring 会自动将其收集到 List<String> 中。
* @return ResponseEntity 包装的 PageResponse<User> 对象。
*/
@GetMapping
public ResponseEntity<PageResponse<User>> getAllUsersPaginated(
@RequestParam(required = false) Integer page, // required=false 让参数可选
@RequestParam(required = false) Integer size,
@RequestParam(required = false) List<String> sort) { // 接收排序参数列表
// 调用更新后的 Service 方法,传入原始参数
PageResponse<User> userPageResponse = userService.findUsersPaginatedAndWrapped(page, size, sort);
// 返回标准化的 PageResponse DTO
return ResponseEntity.ok(userPageResponse);
}
}
关键点:
@RequestParam(required = false)
: 允许page
,size
,sort
参数在请求中不提供,PaginationUtils
会使用默认值。@RequestParam List<String> sort
: Spring MVC 非常智能,如果 URL 中出现多个同名参数 (?sort=name,asc&sort=age,desc
),它能自动将这些值收集到一个List<String>
中。这正好符合我们PaginationUtils
的设计!
Service 层 (UserService.java
)
Service 层使用 PaginationUtils
创建 Pageable
,调用 Repository,然后使用 PageResponse.fromPage()
包装结果。
java
package com.example.yourproject.service;
import com.example.yourproject.dto.PageResponse; // 导入 DTO
import com.example.yourproject.model.User;
import com.example.yourproject.repository.UserRepository;
import com.example.yourproject.utils.PaginationUtils; // 导入工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 分页查询用户,并使用 PageResponse DTO 包装结果。
*
* @param pageInput 来自 Controller 的页码 (1-based)
* @param sizeInput 来自 Controller 的每页大小
* @param sortInput 来自 Controller 的排序参数列表
* @return PageResponse<User> 适合返回给客户端
*/
public PageResponse<User> findUsersPaginatedAndWrapped(Integer pageInput, Integer sizeInput, List<String> sortInput) {
// 定义默认排序(可选,如果希望在用户未指定排序时应用)
Sort defaultSort = Sort.by(Sort.Direction.DESC, "createdAt"); // 例如,默认按创建时间降序
// 1. 使用 PaginationUtils 创建 Pageable
// 将 Controller 传来的原始参数交给工具类处理
Pageable pageable = PaginationUtils.createPageable(pageInput, sizeInput, sortInput, defaultSort);
// 2. 调用 Repository 进行查询
Page<User> userPage = userRepository.findAll(pageable);
// Page<User> userPage = userRepository.findBySomeCondition(..., pageable); // 也可以用于条件查询
// 3. 使用 PageResponse DTO 转换结果
// 这一步将 Page<User> 转换成前端友好的 PageResponse<User>
return PageResponse.fromPage(userPage);
}
}
5. 重点内容总结
PaginationUtils
:- 核心职责 : 将请求参数(1-based 页码、大小、排序字符串列表)转换为
Pageable
对象。 - 关键特性 : 自动处理 1-based 到 0-based 页码转换、参数默认值、复杂排序参数 (
"field,direction"
) 解析。 - 使用位置 : 通常在 Service 层调用,用于准备传递给 Repository 的
Pageable
。
- 核心职责 : 将请求参数(1-based 页码、大小、排序字符串列表)转换为
PageResponse<T>
:- 核心职责 : 作为分页查询结果的标准化 DTO,提供简洁一致的 API 响应结构。
- 关键特性 : 包含数据列表 (
content
) 和清晰的分页元数据(1-basedcurrentPage
,totalElements
,totalPages
等),通过静态工厂方法fromPage(Page<T>)
方便地从 Spring DataPage
对象转换。 - 使用位置 : 在 Service 层将
Page<T>
结果转换为PageResponse<T>
,然后 Controller 直接返回此对象。
- 好处 :
- 简化: 大幅减少 Controller 和 Service 中的样板代码。
- 健壮: 统一处理参数验证和默认值。
- 清晰: 使代码更易读,API 响应更规范。
- 易用: 对新手友好,隐藏了分页实现的细节。
- Controller 参数绑定 : 利用
@RequestParam List<String> sort
可以优雅地接收多个排序参数。
6. 结语
通过封装 PaginationUtils
和 PageResponse<T>
这两个工具类,你可以极大地简化 Spring Boot 项目中 MongoDB(或其他 Spring Data 支持的数据库)的分页实现。这不仅提高了开发效率,也使得代码更加规范和易于维护,尤其是在团队协作或项目规模变大时。希望这份详细的笔记能帮助你轻松掌握并运用这些分页工具!