Java Web开发中的分页与参数校验举例详解
作者:sjsjsbbsbsn
前言
在 Java Web 开发中,分页和参数校验是两个非常重要的功能。本文将围绕 分页设计 和 参数校验 进行探讨,包括如何设计合理的分页查询参数,以及如何利用 Java 注解 实现参数校验。
分页设计
为什么需要分页?
当数据库表数据量较大时,如果直接查询所有数据,可能会导致 查询缓慢,甚至造成 内存溢出(OOM)。分页是一种常见的优化方式,可以 减少数据库负载 并 提升前端渲染速度。
如何设计分页查询参数?
分页通常包含以下几个核心参数:
pageNo
:当前页码,默认为1
。pageSize
:每页返回的记录数,默认为20
。sortBy
:排序字段,如id
、create_time
。isAsc
:是否升序,默认为true
。
我们可以设计一个公共的父类PageQuery
来帮助提供默认的参数,同时我们在开发中也会用到mybatisplus,提供出转成page对象的方法
@Data @ApiModel(description = "分页请求参数") @Accessors(chain = true) public class PageQuery { public static final Integer DEFAULT_PAGE_SIZE = 20; public static final Integer DEFAULT_PAGE_NUM = 1; @ApiModelProperty(value = "页码", example = "1") @Min(value = 1, message = "页码不能小于1") private Integer pageNo = DEFAULT_PAGE_NUM; @ApiModelProperty(value = "每页大小", example = "5") @Min(value = 1, message = "每页查询数量不能小于1") private Integer pageSize = DEFAULT_PAGE_SIZE; @ApiModelProperty(value = "是否升序", example = "true") private Boolean isAsc = true; @ApiModelProperty(value = "排序字段", example = "id") private String sortBy; public int from(){ return (pageNo - 1) * pageSize; } public <T> Page<T> toMpPage(OrderItem ... orderItems) { Page<T> page = new Page<>(pageNo, pageSize); // 是否手动指定排序方式 if (orderItems != null && orderItems.length > 0) { for (OrderItem orderItem : orderItems) { page.addOrder(orderItem); } return page; } // 前端是否有排序字段 if (StringUtils.isNotEmpty(sortBy)){ OrderItem orderItem = new OrderItem(); orderItem.setAsc(isAsc); orderItem.setColumn(sortBy); page.addOrder(orderItem); } return page; } public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc) { if (StringUtils.isBlank(sortBy)){ sortBy = defaultSortBy; this.isAsc = isAsc; } Page<T> page = new Page<>(pageNo, pageSize); OrderItem orderItem = new OrderItem(); orderItem.setAsc(this.isAsc); orderItem.setColumn(sortBy); page.addOrder(orderItem); return page; } public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() { return toMpPage(Constant.DATA_FIELD_NAME_CREATE_TIME, false); } }
设计优点
- 默认分页参数,即使前端未传分页参数,也不会报错。
- 支持排序,可以根据前端传递的
sortBy
和isAsc
进行排序。 - 与 MyBatis-Plus 兼容,直接转换为
Page<T>
,减少重复代码。
使用方式
在实际开发中,我们可以在 Service 层调用 toMpPage()
方法,将 PageQuery
转换为 MyBatis-Plus 的 Page<T>
对象。
public Page<User> getUserList(PageQuery query) { Page<User> page = query.toMpPage(); return userMapper.selectPage(page, new QueryWrapper<>()); }
参数校验的艺术:从基础校验到深度防御
为什么参数校验是系统安全的第一道防线?
在实际开发中,我们常遇到这样的问题:
- 用户输入手机号为"1381234abcd"
- 订单金额出现负数
- 状态字段传入非法数值
- 接口被恶意构造异常参数攻击
参数校验如同系统的门卫,负责:
- 拦截80%以上的常规攻击
- 保证业务数据的有效性
- 提高代码可读性和健壮性
- 降低下游服务的校验压力
JSR 380规范的核心武器库
基础校验实战
@PostMapping("/create") public Result createCoupon(@Valid @RequestBody CouponFormDTO dto) { // 业务逻辑 } @Data public class CouponFormDTO { @NotNull(message = "优惠券类型不能为空") private Integer couponType; @Range(min=1, max=10, message="限领数量超出范围") private Integer limitCount; @EnumValid(enumeration = {0,1}, message="领取方式非法") private ReceiveEnums receiveType; }
常用注解矩阵:
注解 | 适用类型 | 适用场景 |
---|---|---|
@NotNull | 任意对象 | 确保字段不能为空 |
@NotBlank | String | 确保字符串不能为空 |
@NotEmpty | 集合/数组 | 确保列表有数据 |
@Size | String/集合 | 限制长度或元素数量 |
@Min/@Max | 数值类型 | 限制最小/最大值 |
@Pattern | String | 正则表达式校验 |
String | 邮箱格式校验 | |
@Future | Date | 时间必须是未来时间 |
@Past | Date | 时间必须是过去时间 |
@Digits | 数值类型 | 限制整数位数和小数位数 |
深度解析参数校验原理
JSR 380 校验流程
- HTTP 请求进入 Controller 层,绑定参数到 DTO 对象。
**@Valid**
触发校验机制,调用 Hibernate Validator。- 执行校验逻辑,遍历 DTO 字段并检查注解规则。
- 校验失败抛出
**MethodArgumentNotValidException**
。 - 全局异常处理器捕获异常,封装并返回错误信息。
@Valid vs. @Validated 区别
特性 | @Valid | @Validated |
---|---|---|
作用范围 | 单个 DTO | DTO + 分组校验 |
分组支持 | 不支持 | 支持 |
适用场景 | 基础校验 | 复杂业务场景 |
@PostMapping("/update") public Result update(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) { // 业务逻辑处理 }
自定义枚举校验的黑科技
数据库设计的隐痛
我们在设计数据库时通常会使用某个数字来代表某个状态,如:
CREATE TABLE coupon ( status TINYINT COMMENT '0-未激活 1-已生效 2-已过期' )
传统校验方式:
if (!Arrays.asList(0,1,2).contains(status)) { throw new IllegalArgumentException(); }
缺陷:
- 校验逻辑分散
- 可维护性差
- 无法复用
自定义注解解决方案
定义枚举校验注解:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = EnumValidator.class) public @interface EnumValid { int[] value() default {}; String message() default "非法枚举值"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
实现校验逻辑:
public class EnumValidator implements ConstraintValidator<EnumValid, Integer> { private Set<Integer> allowedValues = new HashSet<>(); @Override public void initialize(EnumValid constraintAnnotation) { Arrays.stream(constraintAnnotation.value()) .forEach(allowedValues::add); } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { if (value == null) return true; return allowedValues.contains(value); } }
原理深度解析
ConstraintValidator生命周期:
- 初始化:读取注解配置
- 校验时:执行isValid方法
- 结果处理:返回布尔值
JSR 380规范实现要点:
- 校验器发现机制:SPI方式加载
- 级联校验:支持对象嵌套校验
- 分组校验:实现不同场景的校验规则
复杂校验的终极方案
有时,仅仅靠我们的校验比较复杂,这时我们可能需要自己来编写校验逻辑
我们可以通过自定义注解+AOP来帮我们实现
/** * 实现后在接口访问时如果接口实现了这个接口 * 会被自动自行接口check进行校验 **/ public interface Checker<T> { /** * 用于实现validation不能校验的数据逻辑 */ default void check(){ } default void check(T data){ } }
使用示例
Data @ApiModel(description = "章节") public class CataSaveDTO implements Checker { @ApiModelProperty("章、节、练习id") private Long id; @ApiModelProperty("目录类型1:章,2:节,3:测试") @NotNull(message = "") private Integer type; @ApiModelProperty("章节练习名称") private String name; @ApiModelProperty("章排序,章一定要传,小节和练习不需要传") private Integer index; @ApiModelProperty("当前章的小节或练习") @Size(min = 1, message = "不能出现空章") private List<CataSaveDTO> sections; @Override public void check() { //名称为空校验 if(type == CourseConstants.CataType.CHAPTER && StringUtils.isEmpty(name)) { throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_NULL); }else if(StringUtils.isEmpty(name)){ throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_NULL2); } //名称长度问题 if (type == CourseConstants.CataType.CHAPTER && name.length() > 30){ throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_SIZE); }else if(name.length() > 30) { throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_SIZE2); } if(CollUtils.isEmpty(sections)){ throw new BadRequestException("不能出现空章"); } } }
接口方法参数校验器
/** * 接口方法参数校验器 **/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ParamChecker { }
定义切面类
@Aspect @Slf4j @SuppressWarnings("all") public class CheckerAspect { @Before("@annotation(paramChecker)") public void before(JoinPoint joinPoint, ParamChecker paramChecker) { Object[] args = joinPoint.getArgs(); if(ArrayUtils.isNotEmpty(args)){ //遍历方法参数,参数是否实现了Checker接口 for (Object arg : args){ if(arg instanceof Checker) { //调用check方法,校验业务逻辑 ((Checker)arg).check(); }else if(arg instanceof List){ //如果参数是一个集合也要校验 CollUtils.check((List) arg); } } } } }
工具方法
/** * 集合校验逻辑 * * @param data 要校验的集合 * @param checker 校验器 * @param <T> 集合元素类型 */ public static <T> void check(List<T> data, Checker<T> checker){ if(data == null){ return; } for (T t : data){ checker.check(t); } } /** * 集合校验逻辑 * * @param data 要校验的集合 * @param <T> 集合元素类型 */ public static <T extends Checker<T>> void check(List<T> data){ if(data == null){ return; } for (T t : data){ t.check(); } }
注解使用
@PostMapping("baseInfo/save") @ApiOperation("保存课程基本信息") @ParamChecker //校验非业务限制的字段 public CourseSaveVO save(@RequestBody @Validated(CourseSaveBaseGroup.class) CourseBaseInfoSaveDTO courseBaseInfoSaveDTO) { return courseDraftService.save(courseBaseInfoSaveDTO); }
注意:切面类没有纳入ioc容器管理,如果是单体项目加上component注解即可,如果是多模块项目,使用自动装配功能
总结
到此这篇关于Java Web开发中的分页与参数校验举例详解的文章就介绍到这了,更多相关Java Web分页与参数校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!