JSR 380 Java Bean Validation

  • 该技术规范规定了用于对 Java Bean 中的属性进行验证的 API,从而避免了传参时的大量 if-else 校验。

  • SpringBoot 中提供了相应的 API,可以通过注解的方式来对属性进行约束,在传参时自动校验,并能在参数校验不通过时返回错误信息

引入依赖

要在 SpringBoot 中引入该 API 包,添加如下 Maven 依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

该依赖中提供了 Hibernate-Validator 的传递依赖,因此不用再引入 Hibernate-Validator

常用注解

注解 说明
@NotNull 该属性不能为空
@AssertTrue 该属性需要为 true
@Size 用于字符串、集合或数组,限定该属性的大小(字符串长度、集合数组的元素个数),通过 minmax 两个属性来指定区间
@Min 该属性值不能小于该注解的 value 属性的值
@Max 该属性值不能大于该注解的 value 属性的值
@Email 该属性值需要为合法的邮箱地址
@NotEmpty 用于字符串、集合和数组,限定该属性不能为 null 或空
@NotBlank 用于字符串,限定该字符串不能为 null,空或全是空格
@Positive 用于数字量,限定该属性严格大于 0
@PositiveOrZero 用于数字量,限定该属性严格大于等于 0
@Negative 用于数字量,限定该属性严格小于 0
@NegativeOrZero 用于数字量,限定该属性严格小于等于 0
@Past 用于日期类型的属性,限定该属性必须为过去时间
@PastOrPresent 用于日期类型的属性,限定该属性必须为过去或当前时间
@Future 用于日期类型的属性,限定该属性必须为将来时间
@FutureOrPresent 用于日期类型的属性,限定该属性必须为将来或当前时间

这些注解还可以用于集合的元素,如下:

List<@NotBlank String> preferences;

在传递参数时进行参数校验

给需要校验的属性设置好约束后,我们就可以在 Controller 层的方法参数列表中校验前端的传参,具体做法如下:

@RestController
public class UserController {

@PostMapping("/users")
ResponseEntity<String> addUser(@Valid @RequestBody User user) {
// persisting the user
return ResponseEntity.ok("User is valid");
}

// standard constructors / other methods

}

在 SpringBoot 的 RestController 中,设定哪些参数校验十分简单,只需要在要校验的参数前加上 @Valid 注解即可,此注解会自动启动引导 JSR 380 的实现类—— Hibernate Validator,并验证该参数。

如果参数未通过校验,SpringBoot 会抛出 MethodArgumentNotValidException 异常

异常处理

在验证到参数非法后,SpringBoot 会抛出 MethodArgumentNotValidException 异常,我们需要捕获这个异常并返回错误信息,此时,我们可以新建一个方法专门用于处理参数非法异常,并为这个方法加上 @ExceptionHandler 注解,如下所示:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}

这样,在程序抛出 MethodArgumentNotValidException 异常时,被 @ExceptionHandler(MethodArgumentNotValidException.class) 标注的方法就会自动捕获该异常,并执行方法体中的语句,并将该方法的返回值返回给前端。

参数交叉验证

所谓参数交叉验证,就是指传递的多个参数之间有约束联系,而我们需要验证这些约束联系是否被满足,例如:

  • 传递两个数值参数 ab,其中 a 必须比 b
  • 传递两个日期参数 beginend,其中 begin 的时间必须比 end 的要早

之前介绍的注解,都只适用于单个参数的值的校验,无法完成参数之间交叉验证的需求,于是,我们引入了自定义注解,来实现这一需求。

假如我们需要验证传递来的两个日期参数,一个比另一个早,且两个都晚于当前时间,则我们按如下步骤来实现:

  1. 在需要交叉验证参数的方法上加上我们的自定义注解,这里为 @ConsistentDateParameters

    @ConsistentDateParameters
    public void createReservation(LocalDate begin,
    LocalDate end, Customer customer) {

    // ...
    }
  2. 定义我们定义的自定义注解,如下:

    @Constraint(validatedBy = ConsistentDateParameterValidator.class)
    @Target({ METHOD, CONSTRUCTOR })
    @Retention(RUNTIME)
    @Documented
    public @interface ConsistentDateParameters {

    String message() default
    "End date must be after begin date and both must be in the future";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
    }

    此注解中,有三个必填属性:

    属性 说明
    message 返回的错误信息
    groups 允许指定约束的分组
    payload Bean Validation API 的客户端可以使用它来将自定义有效负载对象分配给约束
  3. 定义自定义注解参数验证类

    @SupportedValidationTarget(ValidationTarget.PARAMETERS)
    public class ConsistentDateParameterValidator
    implements ConstraintValidator<ConsistentDateParameters, Object[]> {

    @Override
    public boolean isValid(
    Object[] value,
    ConstraintValidatorContext context) {

    if (value[0] == null || value[1] == null) {
    return true;
    }

    if (!(value[0] instanceof LocalDate)
    || !(value[1] instanceof LocalDate)) {
    throw new IllegalArgumentException(
    "Illegal method signature, expected two parameters of type LocalDate.");
    }

    return ((LocalDate) value[0]).isAfter(LocalDate.now())
    && ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
    }
    }

    注意:

    1. 类名需要与自定义注解元注解 @ConstraintvalidatedBy 的属性值一致

    2. ConstraintValidator 接口有两个泛型需要指定:

      1. 自定义的注解

      2. 指定该验证类可以验证的数据类型(不确定可以设为 Object[])

    3. 方法 isvalid 有两个参数:

      1. 传递的需要验证的参数
      2. 默认参数 ConstraintValidatorContext context

    isValid 方法中就包含了实际的验证逻辑,返回值为 ture 表示验证通过,返回值为 false 表示验证未通过。

    注意!@SupportedValidationTarget(ValidationTarget.PARAMETERS)ConsistentDateParameterValidator 类的批注是必需的。这样做的原因是 @ConsistentDateParameter 是在方法级别设置的,但约束应应用于方法参数(而不是方法的返回值)

    提示:Bean 验证规范建议将 null 值视为有效值。如果 null 不是有效值,则应添加 @NotNull 注释。

方法返回值校验

有时我们需要检验方法的返回值是否合乎要求,这时我们需要进行方法返回值的校验,方法返回值校验有两种途径:

  1. 使用自带注解进行校验
  2. 使用自定义注解进行校验

使用自带注解进行校验

这种方式很简单,只需要在方法之上加上对返回值的约束注解即可,如:

public class ReservationManagement {

@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
}

加上注解后,该方法的返回值会:

  • 返回的列表不能为 null,且列表中至少包含一个元素
  • 返回的列表中的元素不能为 null

使用自定义注解进行校验

与参数交叉验证相似,我们可以按如下步骤来完成对方法返回值的校验:

  1. 在需要校验返回值的方法上加上我们的自定义注解,如 @ValidReservation

    public class ReservationManagement {

    @ValidReservation
    public Reservation getReservationsById(int id) {
    return null;
    }
    }
  2. 定义自定义注解:

    @Constraint(validatedBy = ValidReservationValidator.class)
    @Target({ METHOD, CONSTRUCTOR })
    @Retention(RUNTIME)
    @Documented
    public @interface ValidReservation {
    String message() default "End date must be after begin date "
    + "and both must be in the future, room number must be bigger than 0";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
    }
  3. 定义验证类:

    public class ValidReservationValidator
    implements ConstraintValidator<ValidReservation, Reservation> {

    @Override
    public boolean isValid(
    Reservation reservation, ConstraintValidatorContext context) {

    if (reservation == null) {
    return true;
    }

    if (!(reservation instanceof Reservation)) {
    throw new IllegalArgumentException("Illegal method signature, "
    + "expected parameter of type Reservation.");
    }

    if (reservation.getBegin() == null
    || reservation.getEnd() == null
    || reservation.getCustomer() == null) {
    return false;
    }

    return (reservation.getBegin().isAfter(LocalDate.now())
    && reservation.getBegin().isBefore(reservation.getEnd())
    && reservation.getRoom() > 0);
    }
    }

此注解还可以应用到类的构造器上,来校验实例化的对象是否合乎要求

级联验证

前面我们介绍的验证,都是验证一个简单对象的属性,如:

public class Customer {

@Size(min = 5, max = 200)
private String firstName;

@Size(min = 5, max = 200)
private String lastName;

// constructor, getters and setters
}

如果我们有一个复杂对象,这个复杂对象的某些属性是其他的自定义对象,当我们要需要验证它时,就需要用到级联验证,如:

public class Reservation {

@Valid
private Customer customer;

@Positive
private int room;

// further properties, constructor, getters and setters
}

我们在需要级联验证的属性对象上也加上了 @Valid 注解,这样在验证 Reservation 对象属性时,不仅会验证 room 属性,还会验证该对象的属性 customer 对象下的全部属性是否合法

public void createNewCustomer(@Valid Reservation reservation) {
// ...
}

该方法同样适合验证方法的返回值是复杂对象时,返回的值是否合法

使用 Spring 进行自动验证

前面我们已经自定义了注解并实现了自定义的校验类,现在,我们可以进行校验了,其中一条实现校验的途径就是使用 Spring 进行自动验证

Spring Validation 提供了与 Hibernate Validator 的集成。

注意:Spring Validation 基于 AOP,并使用 Spring AOP 作为默认实现。因此,验证仅适用于方法,而不适用于构造函数

如果我们想要使用 Spring 进行自动验证,我们需要完成以下两步:

  1. 在需要进行参数验证的方法所属的类上加上 @Validated 注解

    @Validated
    public class ReservationManagement {

    public void createReservation(@NotNull @Future LocalDate begin,
    @Min(1) int duration, @NotNull Customer customer){

    // ...
    }

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getAllCustomers(){
    return null;
    }
    }
  2. 配置提供一个 MethodValidationPostProcessor bean

    @Configuration
    @ComponentScan({ "org.baeldung.javaxval.methodvalidation.model" })
    public class MethodValidationConfig {

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
    return new MethodValidationPostProcessor();
    }
    }

    SpringBoot 应用不需要这一步,IOC 容器会自动为我们注册一个 MethodValidationPostProcessor bean.

帮助文档:Hibernate Validator 8.0.1.Final - Jakarta Bean 验证参考实现:参考指南 (jboss.org)