Spring Boot搜索、过滤与分页:Spring 5环境下RestController单端点实现最优方案咨询
嘿,这个需求我之前在项目里刚好落地过,Spring 5结合Spring Data JPA其实有一套非常成熟且优雅的方案,能在单个RestController端点里同时实现分页、过滤和搜索功能。我给你拆解下最优的实现步骤,附完整代码示例:
咱们的核心是用**Spring Data JPA的Pageable处理分页和排序,用Specification**构建动态的过滤/搜索条件,再把请求参数封装成一个DTO来避免接口参数泛滥,这样代码既整洁又易于维护。
首先创建一个DTO类,把分页、搜索、过滤的所有参数都装进去,方便前端传递和后端接收:
import javax.validation.constraints.Max; import javax.validation.constraints.Min; import lombok.Data; @Data public class UserSearchRequest { // 分页参数 @Min(value = 0, message = "页码不能小于0") private Integer page = 0; @Min(value = 1, message = "每页数量不能小于1") @Max(value = 100, message = "每页数量最大不超过100") private Integer size = 10; // 排序参数,格式如 "id,desc" 或 "name,asc" private String sort; // 搜索关键词(比如匹配用户名、邮箱) private String keyword; // 过滤条件(比如用户状态、角色) private Integer status; private String role; }
这里用了lombok的@Data来简化getter/setter,也加了参数校验注解,避免非法参数。
Specification可以帮我们动态拼接SQL的WHERE条件,把搜索和过滤逻辑都放在这里:
import org.springframework.data.jpa.domain.Specification; import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.List; public class UserSpecifications { public static Specification<User> buildSpec(UserSearchRequest request) { return (root, query, criteriaBuilder) -> { List<Predicate> predicates = new ArrayList<>(); // 搜索逻辑:关键词匹配用户名或邮箱 if (request.getKeyword() != null && !request.getKeyword().trim().isEmpty()) { String keyword = "%" + request.getKeyword().trim() + "%"; predicates.add(criteriaBuilder.or( criteriaBuilder.like(root.get("username"), keyword), criteriaBuilder.like(root.get("email"), keyword) )); } // 过滤逻辑:用户状态匹配 if (request.getStatus() != null) { predicates.add(criteriaBuilder.equal(root.get("status"), request.getStatus())); } // 过滤逻辑:用户角色匹配 if (request.getRole() != null && !request.getRole().trim().isEmpty()) { predicates.add(criteriaBuilder.equal(root.get("role"), request.getRole().trim())); } return criteriaBuilder.and(predicates.toArray(new Predicate[0])); }; } }
这里的逻辑很灵活,你可以根据需求随时加新的过滤/搜索条件,而且是参数化查询,完全不用担心SQL注入问题。
让你的Repository继承JpaSpecificationExecutor,这样就能调用带Specification的分页查询方法:
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> { // 不需要额外写方法,继承后就能用findAll(Specification, Pageable) }
把参数转换和查询逻辑封装到Service层,Controller只负责接收请求和返回响应:
import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public Page<User> searchUsers(UserSearchRequest request) { // 构建Pageable(分页+排序) Pageable pageable = buildPageable(request); // 构建Specification(搜索+过滤) Specification<User> spec = UserSpecifications.buildSpec(request); // 执行分页查询 return userRepository.findAll(spec, pageable); } private Pageable buildPageable(UserSearchRequest request) { Sort sort = Sort.unsorted(); if (request.getSort() != null && !request.getSort().trim().isEmpty()) { String[] sortParts = request.getSort().split(","); String field = sortParts[0]; Sort.Direction direction = sortParts.length > 1 && "desc".equalsIgnoreCase(sortParts[1]) ? Sort.Direction.DESC : Sort.Direction.ASC; sort = Sort.by(direction, field); } return PageRequest.of(request.getPage(), request.getSize(), sort); } }
最后写Controller,接收请求参数,调用Service,返回分页结果:
import org.springframework.data.domain.Page; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/search") public Page<User> searchUsers(@Validated UserSearchRequest request) { return userService.searchUsers(request); } }
- 统一响应格式:可以把
Page<User>封装成自定义的响应DTO,比如ApiResponse<Page<User>>,包含状态码、消息和数据,让前端更易处理。 - 排序参数增强:如果需要多字段排序,可以支持类似
sort=id,desc&sort=name,asc的参数格式,只需要在buildPageable里处理多个sort参数。 - 性能优化:如果数据量很大,可以考虑给搜索/过滤的字段加数据库索引,提升查询速度。
- 参数校验:已经加了
@Validated,记得在项目里引入spring-boot-starter-validation依赖,不然校验注解不会生效。
这样下来,你的单个端点就完美支持分页、过滤和搜索了,而且代码结构清晰,扩展性极强——以后要加新的过滤条件,只需要在DTO里加字段,在Specification里加对应的Predicate就行。
内容的提问来源于stack exchange,提问作者N-G

