参数校检

Lou.Chen
大约 7 分钟

参考:https://cloud.tencent.com/developer/article/1630185open in new window

1、概述

Java 早在 2009 年就提出了 Bean Validation 规范,并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0

Bean Validation和我们很久以前学习过的 JPA 一样,只提供规范,不提供具体的实现

实现 Bean Validation 规范的数据校验框架,主要有:Hibernate Validator

但是,我们在使用 Spring 的项目中,因为 Spring Validation 提供了对 Bean Validation 的内置封装支持,可以使用 @Validated 注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。而在实现原理上,也是基于 Spring AOP 拦截,实现校验相关的操作。

2、注解

2.1 Bean Validation 定义的约束注解

  • 空和非空检查

    • @NotNull :校验参数一定不能为null,但是可以为" "。

      @Null :必须为 null

      @NotEmpty :校验集合类参数(如String类、Collection、Map、数据Array)不能为null或empty。其中String的length、Collection和Map的size不能为0

      @NotBlank:验String字符串不能为null,且不能是空字符串(" "),即调用trim()之后字符串的长度不能为0。

  • 数值检查

    • @DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。

    • @DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。

    • @Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。

    • @Positive:带注释的元素必须为严格的正数(0无效)

      @PositiveOrZero:校验参数必须是正整数或0。

    • @Max(value) :该字段的值只能小于或等于该值。

    • @Min(value) :该字段的值只能大于或等于该值。

    • @Negative :判断负数。

    • @NegativeOrZero :判断负数或 0 。

  • Boolean 值检查

    • @AssertFalse :被注释的元素必须为 true
    • @AssertTrue :被注释的元素必须为 false
  • 长度检查

  • @Size(max, min) :检查该字段的 size 是否在 minmax 之间,可以是字符串、数组、集合、Map 等。

  • 日期检查

    • @Future :被注释的元素必须是一个将来的日期。
    • @FutureOrPresent :判断日期是否是将来或现在日期。
    • @Past :检查该字段的日期是在过去。
    • @PastOrPresent :判断日期是否是过去或现在日期。
  • 其它检查

    • @Email :被注释的元素必须是电子邮箱地址。
    • @Pattern(value) :被注释的元素必须符合指定的正则表达式。

2.2 Hibernate Validator 附加的约束注解

org.hibernate.validator.constraints 包下,定义了一系列的约束( constraint )注解。如下:

  • @Range(min=, max=) :被注释的元素必须在合适的范围内。
  • @Length(min=, max=) :被注释的字符串的大小必须在指定的范围内。
  • @URL(protocol=,host=,port=,regexp=,flags=) :被注释的字符串必须是一个有效的 URL 。
  • @SafeHtml :判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。

2.3 @Valid 和 @Validated

@Valid 注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。

@Validated 注解,是 Spring Validation 所定义,可以添加在方法参数普通方法上,表示它们需要进行约束校验。同时,@Validatedvalue 属性,支持分组校验。属性如下:

// Validated.java

Class<?>[] value() default {};
2.3.1 声明式校检

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

2.3.2 分组校验

Bean Validation 提供的 @Valid 注解,因为没有分组校验的属性,所以无法提供分组校验。此时,我们只能使用 @Validated 注解。

2.3.3 嵌套校验

相比来说,@Valid 注解的地方,多了【成员变量】。这就导致,如果有嵌套对象的时候,只能使用 @Valid 注解。例如说:

// User.java
public class User {
    
    private String id;

    @Valid
    private UserProfile profile;

}
// UserProfile.java
public class UserProfile {
    @NotBlank
    private String nickname;

}
  • 如果不在 User.profile 属性上,添加 @Valid 注解,就会导致 UserProfile.nickname 属性,不会进行校验。

当然,@Valid 注解的地方,也多了【构造方法】和【方法返回】,所以在有这方面的诉求的时候,也只能使用 @Valid 注解。

总的来说,绝大多数场景下,我们使用 @Validated 注解即可。

而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。

3、快速入门

3.1 引入依赖

pom.xml 文件中,引入相关依赖。

        <!-- 实现对 Spring MVC 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

		<!--一般在spring-boot-starter-web中自带该依赖,若没有则加入该依赖-->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- 保证 Spring AOP 相关的依赖包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>

3.2 UserAddDTO

@Getter
@Setter
@ToString
public class UserAddDTO {
    /**
     * 用户名
     */
    @NotEmpty(message = "账号不能为空")
    @Length(min = 5,max = 16,message = "账号长度为5-16位之间")
    @Pattern(regexp = "^[0-9a-zA-Z]+$",message = "账号只能由字母数组组成")
    private String username;

    /**
     * 密码
     */
    @NotEmpty(message = "密码不能为空")
    @Length(min = 6,max = 18,message = "密码长度只能在6-18位之间")
    private String password;
}

3.3 UserController

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

    @GetMapping("/get/{id}")
    public ResponseModel<?> get(@PathVariable("id") @Positive(message = "编号必须大于0") Integer id) {
        return ResponseModel.ok(id);
    }

    @PostMapping("/add")
    public ResponseModel<?> add(@Valid UserAddDTO userAddDTO) {
        return ResponseModel.ok(userAddDTO);
    }

}
  • 在类上,添加 @Validated 注解,表示 UserController 是所有接口都需要进行参数校验。

第一点#get(id) 方法上,我们并没有给 id 添加 @Valid 注解,而 #add(addDTO) 方法上,我们给 addDTO 添加 @Valid 注解。这个差异,是为什么呢?

因为 UserController 使用了 @Validated 注解,那么 Spring Validation 就会使用 AOP 进行切面,进行参数校验。而该切面的拦截器,使用的是MethodValidationInterceptor

  • 对于 #get(id) 方法,需要校验的参数 id ,是平铺开的,所以无需添加 @Valid 注解。
  • 对于 #add(addDTO) 方法,需要校验的参数 addDTO ,实际相当于嵌套校验,要校验的参数的都在 addDTO 里面,所以需要添加 @Valid 注解。

第二点#get(id) 方法的返回的结果是 status = 500 ,而 #add(addDTO) 方法的返回的结果是 status = 400

  • 对于 #get(id) 方法,在 MethodValidationInterceptor 拦截器中,校验到参数不正确,会抛出 ConstraintViolationException 异常。
  • 对于 #add(addDTO) 方法,因为 addDTO 是个 POJO 对象,所以会走 SpringMVC 的 DataBinder 机制,它会调用 DataBinder#validate(Object... validationHints) 方法,进行校验。在校验不通过时,会抛出 BindException

在 SpringMVC 中,默认使用 DefaultHandlerExceptionResolver 处理异常。

  • 对于 BindException 异常,处理成 400 的状态码。
  • 对于 ConstraintViolationException 异常,没有特殊处理,所以处理成 500 的状态码。

5、全局异常处理

5.1 ResponseModel

public class ResponseModel<T> implements Serializable {

    private static final long serialVersionUID = -1263032784821448286L;

    /**
     * 返回码
     */
    private int code;

    /**
     * 返回信息
     */
    private String msg;

    /**
     * 数据
     */
    private T data;

    public ResponseModel() {
        this(ResponseStatusEnum.SUCCESS.getCode(), ResponseStatusEnum.SUCCESS.getMessage(), null);
    }

    public ResponseModel(T data) {
        this(ResponseStatusEnum.SUCCESS.getCode(), ResponseStatusEnum.SUCCESS.getMessage(), data);
    }

    public ResponseModel(int code, String msg) {
        this(code, msg, null);
    }

    public ResponseModel(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static <T> ResponseModel<T> ok() {
        return new ResponseModel<>();
    }

    public static <T> ResponseModel<T> ok(T data) {
        return new ResponseModel<>(data);
    }

    public static <T> ResponseModel<T> of(int code, String msg, T data) {
        return new ResponseModel<>(code, msg, data);
    }

    public static <T> ResponseModel<T> fail(int code, String msg) {
        return new ResponseModel<>(code, msg);
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

5.2 ResponseStatusEnum

public enum ResponseStatusEnum {

    /**
     * 成功
     */
    SUCCESS(0, "成功"),

    /**
     * 系统异常
     */
    SYSTEM_EXCEPTION(10, "系统异常"),

    /**
     * 参数错误
     */
    PARAMETER_EXCEPTION(20, "参数错误"),

    /**
     * 数据库异常
     */
    DATABASE_EXCEPTION(30, "数据库异常");

    /**
     * 错误码
     */
    private final int code;

    /**
     * 错误信息
     */
    private final String message;

    ResponseStatusEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

5.3 GlobalExceptionHandler

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 参数验证异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseModel<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        if (logger.isWarnEnabled()) {
            logger.warn("参数校验异常", e);
        }
        String message = "未知错误";
        if (e.getBindingResult().getFieldError() != null) {
            message = e.getBindingResult().getFieldError().getDefaultMessage();
        }
        return ResponseModel.fail(ResponseStatusEnum.PARAMETER_EXCEPTION.getCode(), message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseModel<?> constraintViolationExceptionHandler(ConstraintViolationException cve) {
        StringBuilder message=new StringBuilder();
        for (ConstraintViolation<?> constraintViolation : cve.getConstraintViolations()) {
            if (message.length() > 0) {
                message.append(";");
            }
            message.append(constraintViolation.getMessage());
        }
        return ResponseModel.fail(ResponseStatusEnum.PARAMETER_EXCEPTION.getCode(), message.toString());
    }

    @ExceptionHandler(BindException.class)
    public ResponseModel<?> constraintViolationExceptionHandler(BindException be) {
        StringBuilder message=new StringBuilder();
        for (ObjectError objectError : be.getAllErrors()) {
            if (message.length() > 0) {
                message.append(";");
            }
            message.append(objectError.getDefaultMessage());
        }
        return ResponseModel.fail(ResponseStatusEnum.PARAMETER_EXCEPTION.getCode(), message.toString());
    }
}