使用Bean Validation 2.0定义方法约束

本文翻译自Method Constraints with Bean Validation 2.0

概述

在本文中,我们会讨论如何使用Bean Validation 2.0(JSR-380)来定义和校验方法约束。

这里我们主要聚焦在如下几种类型的方法约束:

  • 单参数约束
  • 跨多参数约束
  • 返回值约束

本文中的例子需要引入JSR-380的相关依赖:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.2.Final</version>
</dependency>
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>javax.el</artifactId>
    <version>2.2.6</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>6.0.2.Final</version>
    <scop>provided</scop>
</dependency>

对依赖的具体说明可以参考 ValidationApi.md

声明方法约束

首先我们讨论如何声明方法参数约束和返回值约束。

我们可以使用javax.validation.contraints中的注解,也可以使用创建自定义的约束(例如,创建跨多个参数的约束)

单参数约束

在单个参数上定义约束是非常直接的,我们可以按需在每个参数上添加约束注解:

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

同样的,我们也可以使用在构造函数上使用同样的方法定义约束:

public class Customer {
 
    public Customer(
      @Size(min = 5, max = 200) @NotNull String firstName, 
      @Size(min = 5, max = 200) @NotNull String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
 
    // properties, getters, and setters
}

使用多参数约束

在某些情况下,我们可能需要同时校验多个值,例如,校验两个数字其中一个大于另外一个。

对于这些场景,我们可以定义多参数约束,同时校验两个或两个以上的参数。

方法的多参数校验,类似于基于多个属性的类级别校验。让我们考虑一个简单的例子:有一个方法createReservation具有开始日期(begin)和结束日期(end)两个LocateDate类型的参数。

我们想确保begin是未来的某个时间,而endbegin之后。与之前的例子不同的是,这次我们无法通过单个注解来定义这个约束,而需要一个跨多参数的约束。

跟单参数约束不同,跨多参数的约束申明在方法上:

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

创建多参数约束

为了实现@ConsistentDateParameters约束,我们需要两个步骤。

首先,我们需要定义约束注解

@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 - 默认错误消息的key,让我们可以使用消息解析
  • groups - 允许为约束指定校验组
  • payload - Bean Validation API的客户端端可以使用这个属性为约束附加自定义的负载对象

如何定义自定义约束的细节说明可以查阅官方文档

接下来,我们就可以定义校验器类了:

@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]);
    }
}

上述代码中,isValid方法包含实际的校验逻辑。首先我们确定拿到两个LocalDate类型的参数。然后,检查两个参数都是未来的时间,且endbegin之后。

同时,注意ConsistentDateParameterValidator这个类上必须有这个注解@SupportedValidationTarget(ValidationTarget.PARAMETERS)。因为@ConsistentDateParameter设置在方法级别,但是约束需要施加在方法参数上(且不是施加在方法的返回值)。

注意:Bean校验规范建议认为null值是有效的。如果null是个无效值,应当使用@NotNull注解进行约束。

返回值约束

有时我们需要校验一个方法的返回值对象。为此,我们可以使用返回值约束。

接下来的例子使用内置约束:

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

对于方法getAllCustomers(),施加了如下约束:

  • 返回值列表必须不能为null且必须有至少一个元素
  • 列表中不能包含null元素

返回值自定义约束

又是我们需要校验复杂的对象:

public class ReservationManagement {
 
    @ValidReservation
    public Reservation getReservationsById(int id) {
        return null;
    }
}

在这个例子中,返回的Reservation对象必须符合@ValidReservation注解定义的约束。

我们需要在定义一个约束注解@ValidReservation:

@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 {};
}

接下来,定义校验器类:

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);
    }
}

构造方法的返回值校验

在上面我们定义的@ValidaReservation注解的目标为METHODCONTRUCTOR,所以也这个约束注解也可以施加在构造方法上来校验方法构造出的实例。

public class Reservation {
 
    @ValidReservation
    public Reservation(
      LocalDate begin, 
      LocalDate end, 
      Customer customer, 
      int room) {
        this.begin = begin;
        this.end = end;
        this.customer = customer;
        this.room = room;
    }
 
    // properties, getters, and setters
}

级联校验

最后,Bean Validation API不仅允许我们校验单个对象,同事也可以使用级联校验来校验整个对象图。

如果我们想校验复杂的对象,可以通过@Valid注解使用级联校验,这个注解对方法参数和返回值都有效。

我们假设有一个Customer类,这个类有一些被约束的属性:

public class Customer {
 
    @Size(min = 5, max = 200)
    private String firstName;
 
    @Size(min = 5, max = 200)
    private String lastName;
 
    // constructor, getters and setters
}

另外有个Reservation类,它有一个Customer类型的属性,同时还有一些其他被约束的属性:

public class Reservation {
 
    @Valid
    private Customer customer;
     
    @Positive
    private int room;
     
    // further properties, constructor, getters and setters
}

我们现在引用Reservation作为一个方法参数,我们可以强制递归校验所有的属性:

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

上面代码中,我们在两个地址使用了@Valid注解:

  • 在Reservation类型参数上:当createNewCustomer方法被调用时,它触发了对Reservation对象的校验。
  • 在Reservation内嵌的Customer类型属性上:因此会触发对内嵌属性的校验。

对返回值类型为Reservation的方法同样有效:

@Valid
public Reservation getReservationById(int id) {
    return null;
}

校验方法约束

在前面的章节中,我们定义了很多约束,现在我们开始真正来执行这些约束的校验。有多种执行校验的途径,下面来意义说明。

使用Spring提供的自动校验机制

Spring集成了Hibernate Validator来提供校验机制。

注意:Spring校验机制基于AOP,且使用Spring AOP作为默认实现。因此校验只能在普通方法上工作,对构造方法无效。

我们现在想要Spring来自动校验我们的约束,我们需要做两件事:

首先,我们要为需要校验的bean加上@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;
    }
}

然后,我们要提供一个MethodValidationPostProcessor Bean:

@Configuration
@ComponentScan({ "org.baeldung.javaxval.methodvalidation.model" })
public class MethodValidationConfig {
 
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

现在Spring容器会在违反约束时抛出javax.validation.ConstraintViolationException异常。

如果我们使用Spring-Boot,只要hibernate-validator出现在类路径中,容器会自动注册MethodValidationPostProcessor。

人工编程校验

在单独的Java应用中,我们可以使用javax.validation.executable.ExecutableValidator接口来进行校验。

可以通过如下代码获得该接口的实例:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();

ExecutableValidator提供了4个方法:

  • 用于普通方法校验的 validateParameters() 和 validateReturnValue()
  • 用于构造方法校验的 validateConstructorParameters() 和 validateConstrctorReturnValue()

校验我们定义的createReservation()方法的参数的代码如下:

ReservationManagement object = new ReservationManagement();
Method method = ReservationManagement.class
  .getMethod("createReservation", LocalDate.class, int.class, Customer.class);
Object[] parameterValues = { LocalDate.now(), 0, null };
Set<ConstraintViolation<ReservationManagement>> violations 
  = executableValidator.validateParameters(object, method, parameterValues);

注意:官方文档不鼓励在应用代码中直接使用这个接口,而应该通过方法连接技术例如AOP或代理模式。

如果你对ExecutableValidator接口有兴趣,你可以看一下官方文档

总结

在这个教程中,我们快速浏览了如何通过Hibernate Validator使用方法约束,还讨论了JSR-380的一些新特性。

首先,我们讨论了如何声明不同类型的约束:

  • 单参数约束
  • 多参数约束
  • 返回值约束

我们还看到了如何进行人工编程校验,以及使用Spring Validation进行自动校验。

本文的完整示例代码可以从GitHub获得

上一篇:基于java SSM springboot+redis网上水果超市商城设计和实现以及文档


下一篇:类型安全