Spring Validation 参数校验

1,214 阅读6分钟

JSR-303

什么是JSR? JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

JSR-303定义的是什么标准? JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

也可以看哈这篇文章:mp.weixin.qq.com/s/g04HMhrjb…

使用场景

  1. 校验方法参数,构造器
  2. 校验返回值

注解

  • 空和非空检查
    • @NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 。
    • @NotEmpty :集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。
    • @NotNull :不能为 null 。
    • @Null :必须为 null 。
  • 数值检查
    • @DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。
    • @DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。
    • @Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。
    • @Positive :判断正数。
    • @PositiveOrZero :判断正数或 0 。
    • @Max(value) :该字段的值只能小于或等于该值。
    • @Min(value) :该字段的值只能大于或等于该值。
    • @Negative :判断负数。
    • @NegativeOrZero :判断负数或 0 。
  • Boolean 值检查
    • @AssertFalse :被注释的元素必须为 true 。
    • @AssertTrue :被注释的元素必须为 false 。
  • 长度检查
    • @Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。
  • 日期检查
    • @Future :被注释的元素必须是一个将来的日期。
    • @FutureOrPresent :判断日期是否是将来或现在日期。
    • @Past :检查该字段的日期是在过去。
    • @PastOrPresent :判断日期是否是过去或现在日期。
  • 其它检查
    • @Email :被注释的元素必须是电子邮箱地址。
    • @Pattern(value) :被注释的元素必须符合指定的正则表达式。

@Valid 和 @Validated:

  • @Valid 注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。因为能用在成员变量上,所以支持嵌套校验。如图
  • @Validated 注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated 有 value 属性,支持分组校验

Spring Validation 仅对 @Validated 注解,实现声明式校验。

什么意思?
只有在类上或方法上使用@Validated注解,上述的@NotBlank等注解,用于方法参数上才能生效。

所以,绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上即可。

基本使用

spring boot 版本:2.7.5

需要注意的是,spring的校验是通过AOP实现的,所以只有在不同对象之间的调用才会生效。
如果是调用的同一个对象的内部方法,即可以通过this直接调用方法,则注解不能生效。

校验参数

@Data
public class UserVo {

    /**
     * 账号
     */
    @NotEmpty(message = "登录账号不能为空")
    @Size(min = 5, max = 16, message = "账号长度为 5-16 位")
    @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
    private String username;
    /**
     * 密码
     */
    @NotEmpty(message = "密码不能为空")
    @Size(min = 4, max = 16, message = "密码长度为 4-16 位")
    private String password;

}

在Controller中使用:

@RestController
@RequestMapping("/users")
@Validated
@Slf4j
public class UserController {

    @GetMapping("/get")
    public int get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
        log.info("[get][id: {}]", id);
        return 1;
    }

    @PostMapping("/add")
    public String add(@Valid @RequestBody UserVo userVo) {
        log.info("[add][addDTO: {}]", userVo);
        return JSON.toJSONString(userVo);
    }
}

但最好用于service:

@Validated
public interface UserService {

    public Integer get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id);

    public Integer add(@Valid UserVo userVo);
}

校验返回值

@Validated
public interface UserService {
    public @NotNull Integer getOne(Integer one);

    public @Min(value = 10) Integer getOne2(Integer one);
}

异常处理

对于抛出的异常,可以使用全局异常捕获的方式进行处理。


@Slf4j
@ControllerAdvice(basePackages = "pers.h.controller")
public class GlobalExceptionHandler {    
    @ResponseBody
    @ExceptionHandler(value = ConstraintViolationException.class)
    public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
        log.debug("[constraintViolationExceptionHandler]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(constraintViolation.getMessage());
        }
        // 包装 CommonResult 结果
        return CommonResult.error(400,
                                  "参数校验异常:"+ detailMessage.toString());
    }

    @ResponseBody
    @ExceptionHandler(value = BindException.class)
    public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
        log.debug("[bindExceptionHandler]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ObjectError objectError : ex.getAllErrors()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(objectError.getDefaultMessage());
        }
        // 包装 CommonResult 结果
        return CommonResult.error(400,
                                  "参数校验异常:"+ detailMessage.toString());
    }
}

自定义校验

参考文章:www.iocoder.cn/Spring-Boot…

  1. 创建接口,用于定义注解的数据类型
public interface IntArrayValuable {

    /**
     * @return int 数组
     */
    int[] array();

}
  1. 创建枚举类
package pers.h.constraints;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Arrays;

@AllArgsConstructor
@Getter
public enum SexEnum implements IntArrayValuable {

    UNKNOWN(0, "未知"),
    MAN(1, "男"),
    WOMAN(2, "女");

    private final Integer code;
    public final String desc;


    @Override
    public int[] array() {
        return Arrays.stream(values()).mapToInt(SexEnum::getCode).toArray();
    }
}

  1. 创建自定义约束注解
package pers.h.constraints;


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = InEnumValidator.class)
public @interface InEnum {

    /**
     * @return 实现 IntArrayValuable 接口的
     */
    Class<? extends IntArrayValuable> value();

    /**
     * @return 提示内容
     */
    String message() default "必须在指定范围 {value}";

    /**
     * @return 分组
     */
    Class<?>[] groups() default {};

    /**
     * @return Payload 数组
     */
    Class<? extends Payload>[] payload() default {};

    /**
     * Defines several {@code @InEnum} constraints on the same element.
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        InEnum[] value();
    }

}

  1. 创建约束注解的校验器
package pers.h.constraints;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;

public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {

    /**
     * 值数组
     */
    private Set<Integer> values;

    @Override
    public void initialize(InEnum annotation) {
        IntArrayValuable[] values = annotation.value().getEnumConstants();
        if (values.length == 0) {
            this.values = Collections.emptySet();
        } else {
            this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toSet());
        }
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        // 校验通过
        if (values.contains(value)) {
            return true;
        }
        // 校验不通过
        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
//        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
//                .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
//        return false;
        throw new IllegalArgumentException(context.getDefaultConstraintMessageTemplate()
                .replaceAll("\\{value}", values.toString()));
    }

}
  1. 使用
package pers.h.model;

import lombok.Data;
import pers.h.constraints.InEnum;
import pers.h.constraints.SexEnum;

import javax.validation.constraints.NotNull;

@Data
public class UserUpdateGenderDTO {

    /**
     * 用户编号
     */
    @NotNull(message = "用户编号不能为空")
    private Integer id;

    /**
     * 性别
     */
    @NotNull(message = "性别不能为空")
    @InEnum(value = SexEnum.class, message = "性别必须是 {value}")
    private Integer gender;
}

分组校验

分组校验,即相同的 Bean 对象,根据校验分组,使用不同的校验规则。
典型的场景是:在新增一条数据时,我们往往不需要主键,但是,在更新一条数据时,主键必不可少。

  1. 创建分组接口

/**
 * 校验数据分组 ,数据新增场景
 */
public interface ValidationGroupAdd {
}

/**
 * 校验数据分组,数据更新场景
 */
public interface ValidationGroupUpdate {
}

  1. bean

@Data
public class UserVo2 {

    /**
     * 账号
     */
    @NotBlank(message = "登录账号不能为空", groups = ValidationGroupUpdate.class)
    private String username;
    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空", groups = ValidationGroupAdd.class)
    private String password;

    @Valid
    @NotNull(groups = ValidationGroupAdd.class, message = "部门不能为空!")
    private Dept dept;

    @Data
    public static class Dept {

        @NotBlank(message = "部门id不能为空", groups = ValidationGroupAdd.class)
        private String deptId;
        @NotBlank(message = "部门名称不能为空", groups = ValidationGroupAdd.class)
        private String deptName;

    }

}
  1. 接口
public interface UserService {
	@Validated(value = ValidationGroupAdd.class)
    String addOne(@Valid UserVo2 userVo);
    
    @Validated(value = ValidationGroupUpdate.class)
    String updateOne(@Valid UserVo2 userVo);
}

手动校验

@Autowired 
private Validator validator;

@Test
public void testValidator() {
    // 打印,查看 validator 的类型
    System.out.println(validator);

    // 创建 UserAddDTO 对象
    UserVo addDTO = new UserVo();
    // 校验
    Set<ConstraintViolation<UserVo>> result = validator.validate(addDTO);
    // 打印校验结果 // <4>
    for (ConstraintViolation<UserVo> constraintViolation : result) {
        // 属性:消息
        System.out.println(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());
    }
}

输出:

org.springframework.validation.beanvalidation.LocalValidatorFactoryBean@6b2e46af
username:登录账号不能为空
password:密码不能为空
  • 如果校验通过,则返回的 Set<ConstraintViolation<?>> 集合为空。

国际化

  1. 创建资源文件。
    直接在resources目录下创建ValidationMessages.properties系列文件。
    image.png
    文件内容如下:

    # ValidationMessages.properties
    id.NotNull = id\u4e0d\u80fd\u4e3a\u7a7a\uff01
    
    # ValidationMessages_en_US.properties
    id.NotNull = userId cannot be empty !
    
    # ValidationMessages_zh_CN.properties
    id.NotNull = id\u4e0d\u80fd\u4e3a\u7a7a\uff01
    
  2. 在Bean中使用配置

    
    @Data
    public class UserVoI18 {
        @NotNull(message = "{id.NotNull}")
        private Integer id;
    }
    
    
  3. 使用时,可以通过Accept-Language来指定语言环境

    image.png
    image.png

参考文章

www.iocoder.cn/Spring-Boot…
blog.51cto.com/u_3631118/2…
blog.csdn.net/luo15242208…