Java Web 开发中的分页与参数校验

Java Web 开发中的分页与参数校验

在 Java Web 开发中,分页和参数校验是两个非常重要的功能。本文将围绕 分页设计参数校验 进行探讨,包括如何设计合理的分页查询参数,以及如何利用 Java 注解 实现参数校验。

1️⃣ 分页设计

❓ 为什么需要分页?

当数据库表数据量较大时,如果直接查询所有数据,可能会导致 查询缓慢 ,甚至造成 内存溢出(OOM) 。分页是一种常见的优化方式,可以 减少数据库负载提升前端渲染速度

✅ 如何设计分页查询参数?

分页通常包含以下几个核心参数:

  • pageNo:当前页码,默认为 1
  • pageSize:每页返回的记录数,默认为 20
  • sortBy:排序字段,如 idcreate_time
  • isAsc:是否升序,默认为 true

我们可以设计一个公共的父类PageQuery来帮助提供默认的参数,同时我们在开发中也会用到mybatisplus,提供出转成page对象的方法

java 复制代码
@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);
    }
}

🚀 设计优点

  1. 默认分页参数,即使前端未传分页参数,也不会报错。
  2. 支持排序 ,可以根据前端传递的 sortByisAsc 进行排序。
  3. 与 MyBatis-Plus 兼容 ,直接转换为 Page<T>,减少重复代码。

📌 使用方式

在实际开发中,我们可以在 Service 层调用 toMpPage() 方法,将 PageQuery 转换为 MyBatis-Plus 的 Page<T> 对象。

java 复制代码
public Page<User> getUserList(PageQuery query) {
    Page<User> page = query.toMpPage();
    return userMapper.selectPage(page, new QueryWrapper<>());
}

2️⃣ 参数校验的艺术:从基础校验到深度防御

❓ 为什么参数校验是系统安全的第一道防线?

在实际开发中,我们常遇到这样的问题:

  1. 用户输入手机号为"1381234abcd"
  2. 订单金额出现负数
  3. 状态字段传入非法数值
  4. 接口被恶意构造异常参数攻击

参数校验如同系统的门卫,负责:

  • 拦截80%以上的常规攻击
  • 保证业务数据的有效性
  • 提高代码可读性和健壮性
  • 降低下游服务的校验压力

💡JSR 380规范的核心武器库

基础校验实战
java 复制代码
@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 正则表达式校验
@Email String 邮箱格式校验
@Future Date 时间必须是未来时间
@Past Date 时间必须是过去时间
@Digits 数值类型 限制整数位数和小数位数

🔧 深度解析参数校验原理

JSR 380 校验流程
  1. HTTP 请求进入 Controller 层,绑定参数到 DTO 对象。
  2. **@Valid** 触发校验机制,调用 Hibernate Validator。
  3. 执行校验逻辑,遍历 DTO 字段并检查注解规则。
  4. 校验失败抛出 **MethodArgumentNotValidException**
  5. 全局异常处理器捕获异常,封装并返回错误信息。
@Valid vs. @Validated 区别
特性 @Valid @Validated
作用范围 单个 DTO DTO + 分组校验
分组支持 不支持 支持
适用场景 基础校验 复杂业务场景
java 复制代码
@PostMapping("/update")
public Result update(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) {
    // 业务逻辑处理
}

📌自定义枚举校验的黑科技

数据库设计的隐痛

我们在设计数据库时通常会使用某个数字来代表某个状态,如:

sql 复制代码
CREATE TABLE coupon (
    status TINYINT COMMENT '0-未激活 1-已生效 2-已过期'
)

传统校验方式:

java 复制代码
if (!Arrays.asList(0,1,2).contains(status)) {
    throw new IllegalArgumentException();
}

缺陷

  • 校验逻辑分散
  • 可维护性差
  • 无法复用
📌自定义注解解决方案

定义枚举校验注解

java 复制代码
@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 {};
}

实现校验逻辑

java 复制代码
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生命周期

  1. 初始化:读取注解配置
  2. 校验时:执行isValid方法
  3. 结果处理:返回布尔值

JSR 380规范实现要点

  • 校验器发现机制:SPI方式加载
  • 级联校验:支持对象嵌套校验
  • 分组校验:实现不同场景的校验规则

💡复杂校验的终极方案

有时,仅仅靠我们的校验比较复杂,这时我们可能需要自己来编写校验逻辑

我们可以通过自定义注解+AOP来帮我们实现

java 复制代码
/**
 * 实现后在接口访问时如果接口实现了这个接口
 * 会被自动自行接口check进行校验
 **/
public interface Checker<T> {

    /**
     * 用于实现validation不能校验的数据逻辑
     */
    default void check(){

    }

    default void check(T data){
    }
}

使用示例

java 复制代码
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("不能出现空章");
        }

    }
}

接口方法参数校验器

java 复制代码
/**
 * 接口方法参数校验器
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ParamChecker {
}

定义切面类

java 复制代码
@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);
                }
            }
        }
    }
}

工具方法

java 复制代码
    /**
     * 集合校验逻辑
     *
     * @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();
        }
    }

注解使用

java 复制代码
  @PostMapping("baseInfo/save")
    @ApiOperation("保存课程基本信息")
    @ParamChecker
    //校验非业务限制的字段
    public CourseSaveVO save(@RequestBody @Validated(CourseSaveBaseGroup.class) CourseBaseInfoSaveDTO courseBaseInfoSaveDTO) {
        return courseDraftService.save(courseBaseInfoSaveDTO);
    }

🚀注意:切面类没有纳入ioc容器管理,如果是单体项目加上component注解即可,如果是多模块项目,使用自动装配功能

责任链模式下的参数校验

责任链模式验证参数

相关推荐
gyeolhada10 分钟前
2025蓝桥杯JAVA编程题练习Day3
java·数据结构·算法·蓝桥杯
钮钴禄·爱因斯晨14 分钟前
赛博算命之 ”梅花易数“ 的 “JAVA“ 实现 ——从玄学到科学的探索
java·开发语言·python
Beekeeper&&P...37 分钟前
BCrypt加密密码和md5加密哪个更好一点///jwt和rsa有什么区别//为什么spring中经常要用个r类
java·spring·r语言
吴声子夜歌1 小时前
Linux运维——文件内容查看编辑
java·linux·运维
m0_748240911 小时前
Spring Boot项目中解决跨域问题(四种方式)
spring boot·后端·dubbo
小锋学长生活大爆炸1 小时前
【教程】docker升级镜像
java·docker·容器·镜像
代码轨迹1 小时前
SpringCloud学习笔记(五)
java·学习·spring cloud
m0_748232922 小时前
使用 java -jar 命令启动 Spring Boot 应用时,指定特定的配置文件的几种实现方式
java·spring boot·jar
雪芽蓝域zzs2 小时前
SpringBoot开发(六)SpringBoot整合MyBatis
spring boot·oracle·mybatis
GGBondlctrl2 小时前
【Spring Boot】Spring 魔法世界:Bean 作用域与生命周期的奇妙之旅
java·spring boot·spring·bean的作用域·bean的生命周期·bean生命周期原码