Spring Validation

69 阅读6分钟

一,数据校验框架的产生背景

以Web项目为例,用户需要填写表单信息保存提交,页面输入信息需要进行数据格式校验,并且返回对应的错误提示,以此来达到数据校验的目的,从而避免无效数据被保存或者提交,这些检查工作包括必填项检查、数值检查、长度检查、身份证号码、手机号码检查等工作,当请求参数格式不正确的时候,需要程序监测到,对于前后端分离开发过程中,数据校验还需要返回对应的状态码和错误提示信息。

如果将这些字段校验和业务逻辑混合一起写,则会:

  • 代码极其臃肿,且不容易维护
  • 干扰原有逻辑

二,数据校验框架的演变过程

  1. 原始阶段:逐个字段、逐个对象进行硬编码校验

  2. 使用自定义工具类

  3. 标准化阶段:JSR-303 (Bean Validation 1.0)

    为了解决上述问题,Java社区引入了JSR-303(Bean Validation 1.0),这是Java EE 6的一部分。JSR-303定义了一组标准注解和机制,用于声明性地进行参数校验。Hibernate Validator成为了JSR-303的参考实现。

  4. 现代阶段:JSR-380 (Bean Validation 2.0)

    随着Java 8的引入,JSR-380(Bean Validation 2.0)作为JSR-303的改进版被发布,成为Java EE 8的一部分。JSR-380增加了对新的数据类型(如Optional)的支持,并引入了一些新的注解。

  5. 集成框架:Spring Boot和Hibernate Validator

    Spring Boot进一步简化了Bean Validation的使用,通过自动配置和依赖管理,使得在Spring应用中使用JSR-303/JSR-380变得非常简单。

三,SpringBoot参数校验快速入门

  1. 导包

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 创建一个实体类,并在其字段上使用注解来进行校验:

    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    public class User {
        @NotNull(message = "ID不能为空")
        private Long id;
    
        @NotBlank(message = "姓名不能为空")
        @Size(min = 2, max = 30, message = "姓名长度必须在2到30个字符之间")
        private String name;
    
        // getter和setter方法
    }
    
  3. 在Controller中,通过@Valid@Validated注解启用参数校验

        @PostMapping("/valid")
        public Result createUser(@Valid @RequestBody User user) {
            return Result.success(1);
        }
    
  4. 创建全局异常管理器来接受校验失败的异常用来返回统一响应结果

    @ControllerAdvice
    public class GlobalExceptionHandler {
        
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public Result handleValidationExceptions(MethodArgumentNotValidException ex) {
            StringBuilder errors = new StringBuilder();
            for (FieldError error : ex.getBindingResult().getFieldErrors()) {
                errors.append(error.getField()).append(": ").append(error.getDefaultMessage()).append("; ");
            }
            return Result.error(errors.toString());
        }
    
        @ExceptionHandler(ConstraintViolationException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Result handleConstraintViolationException(ConstraintViolationException ex) {
            StringBuilder errors = new StringBuilder();
            ex.getConstraintViolations().forEach(violation ->
                    errors.append(violation.getPropertyPath()).append(": ").append(violation.getMessage()).append("; ")
            );
            return Result.error(errors.toString());
        }
    }
    

四,常用的字段校验注解

4.1 基本校验注解

  1. @NotNull:确保属性不能为null

    @NotNull(message = "字段不能为空")
    private String name;
    
  2. @NotEmpty:确保字段的值不是null且长度大于0(用于集合或字符串)。

    @NotEmpty(message = "列表不能为空")
    private List<String> items;
    
  3. @NotBlank:确保字段的值不是null且经过修剪后长度大于0(用于字符串)。

    @NotBlank(message = "字段不能为空且不能为空白字符")
    private String description;
    
  4. @Size:验证字符序列、集合、数组的大小是否在指定范围内。

    @Size(min = 2, max = 30, message = "姓名长度必须在2到30个字符之间")
    private String name;
    

4.2 数字校验注解

  1. @Min:验证字段的值大于或等于指定的最小值(用于数字)

    @Min(value = 18, message = "年龄必须大于或等于18")
    private int age;
    
  2. @Max:验证字段的值小于或等于指定的最大值(用于数字)

    @Max(value = 65, message = "年龄必须小于或等于65")
    private int age;
    
  3. @DecimalMin:验证字段的值大于或等于指定的最小值(用于小数)。

    @DecimalMin(value = "0.1", message = "金额必须大于或等于0.1")
    private BigDecimal amount;
    
  4. @DecimalMax:验证字段的值小于或等于指定的最大值(用于小数)。

    @DecimalMax(value = "100.0", message = "金额必须小于或等于100.0")
    private BigDecimal amount;
    
  5. @Digits:验证字段的值是否符合指定的整数和小数位数

    @Digits(integer = 3, fraction = 2, message = "数字必须是一个最多包含3位整数和2位小数的数值")
    private BigDecimal price;
    
  6. @Positive:验证字段的值为正数

    @Positive(message = "数量必须是正数")
    private int quantity;
    
  7. @PositiveOrZero:验证字段的值为正数或零。

    @PositiveOrZero(message = "数量必须是正数或零")
    private int quantity;
    
  8. @Negative:验证字段的值为负数

    @Negative(message = "损失必须是负数")
    private int loss;
    
  9. @NegativeOrZero:验证字段的值为负数或零。

    @NegativeOrZero(message = "损失必须是负数或零")
    private int loss;
    

4.3 时间日期校验注解

  1. @Past:验证日期是否在过去

    @Past(message = "生日必须在过去")
    private LocalDate birthDate;
    
  2. @PastOrPresent:验证日期是否在过去或现在。

    @PastOrPresent(message = "注册日期必须在过去或现在")
    private LocalDate registrationDate;
    
  3. @Future:验证日期是否在未来。

    @Future(message = "预约日期必须在未来")
    private LocalDate appointmentDate;
    
  4. @FutureOrPresent:验证日期是否在未来或现在。

    @FutureOrPresent(message = "预约日期必须在未来或现在")
    private LocalDate appointmentDate;
    

4.4 字符串校验注解

  1. @Email:验证字段是否符合电子邮件格式

    @Email(message = "电子邮件格式不正确")
    private String email;
    
  2. @Pattern:验证字段是否符合指定的正则表达式。

    @Pattern(regexp = "^[A-Za-z0-9]+$", message = "用户名只能包含字母和数字")
    private String username;
    

4.5 其他常用校验注解

  1. @AssertTrue:验证字段的值为true

    @AssertTrue(message = "必须同意条款")
    private boolean agreed;
    
  2. @AssertFalse:验证字段的值为false

    @AssertFalse(message = "必须不同意条款")
    private boolean declined;
    
    

4.6 自定义校验注解

  1. 自定义注解

    @Constraint(validatedBy = CustomValidator.class)
    @Target({ ElementType.METHOD, ElementType.FIELD })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CustomConstraint {
        String message() default "自定义校验错误信息";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    
  2. 自定义注解实现

    public class CustomValidator implements ConstraintValidator<CustomConstraint, String>{
    
        @Override
        public void initialize(CustomConstraint constraintAnnotation) {
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            // 实现自定义校验逻辑
            return value != null && value.matches("[A-Za-z]+");
        }
    }
    
  3. 使用自定义注解

    public class User {
        @CustomConstraint
        private String customField;
        // 其他字段和方法
    }
    

4.7 创建全局异常处理器拦截异常

在Spring Boot中,处理参数校验时可能会遇到两种主要的异常类型:MethodArgumentNotValidExceptionConstraintViolationException。这两种异常处理的场景和用途略有不同。

  1. MethodArgumentNotValidException
  • 异常类型MethodArgumentNotValidException 是 Spring Framework 提供的一个异常类,用于处理使用 @Valid 注解进行参数校验失败的情况。

  • 触发时机:当你在Controller层使用 @Valid 注解对请求体进行校验时,如果校验失败,Spring会抛出此异常。常见于 POST 和 PUT 请求中的请求体@RequestBody)校验。

  1. ConstraintViolationException
  • 异常类型ConstraintViolationException 是 Java Bean Validation API 提供的一个异常类,用于处理约束校验失败的情况。

  • 触发时机:当在方法参数上使用 Bean Validation 注解(如 @PathVariable@RequestParam)进行校验时,如果参数不符合约束条件,会抛出此异常。适用于请求参数、路径变量等的校验。

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidationExceptions(MethodArgumentNotValidException ex) {
        StringBuilder errors = new StringBuilder();
        for (FieldError error : ex.getBindingResult().getFieldErrors()) {
            errors.append(error.getField()).append(": ").append(error.getDefaultMessage()).append("; ");
        }
        return Result.error(errors.toString());
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        StringBuilder errors = new StringBuilder();
        ex.getConstraintViolations().forEach(violation ->
                errors.append(violation.getPropertyPath()).append(": ").append(violation.getMessage()).append("; ")
        );
        return Result.error(errors.toString());
    }
}