1. 概述

本文将深入探讨如何使用 Jakarta Bean Validation 3.0 定义和验证方法约束。在前一篇文章中,我们讨论了 JSR-380 的内置注解及属性验证实现。本文将聚焦以下方法约束类型:

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

同时,我们将演示如何通过 Spring Validator 手动和自动验证这些约束。本文示例依赖与 Java Bean Validation 基础 完全一致。

2. 方法约束的声明

2.1 单参数约束

在单个参数上定义约束非常直接,只需为每个参数添加所需注解

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

    // 属性、getter 和 setter
}

2.2 使用跨参数约束

某些场景需要同时验证多个参数(如两个数值的大小关系)。此时可定义跨参数约束,依赖两个或以上参数。

⚠️ 跨参数约束可视为方法验证中的类级约束等价物,两者都基于多个属性实现验证。

createReservation() 方法为例:它接收两个 LocalDate 参数(开始日期和结束日期)。需确保:

  1. 开始日期在未来
  2. 结束日期晚于开始日期

单参数约束无法满足,必须使用跨参数约束。与单参数不同,跨参数约束声明在方法或构造函数上

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

    // ...
}

2.3 创建跨参数约束

实现 @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:错误消息默认键,支持消息插值
  • groups:指定验证组
  • payload:供客户端附加自定义载荷对象

第二步:定义验证器类

@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() 包含核心验证逻辑:检查参数类型、日期有效性
  • @SupportedValidationTarget(ValidationTarget.PARAMETERS) 注解必须添加,表明约束应用于参数而非返回值
  • 规范建议将 null 视为有效值,若需禁止应使用 @NotNull

2.4 返回值约束

有时需验证方法返回的对象,此时使用返回值约束

public class ReservationManagement {

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

getAllCustomers() 的约束要求:

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

2.5 返回值自定义约束

复杂对象验证需自定义约束:

public class ReservationManagement {

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

返回的 Reservation 对象必须满足 @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);
    }
}

2.6 构造函数中的返回值

ValidReservation 注解的 @Target 包含 CONSTRUCTOR可直接注解构造函数验证实例

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

    // 属性、getter 和 setter
}

2.7 级联验证

Bean Validation API 支持通过级联验证验证对象图。使用 @Valid 注解触发递归验证,适用于方法参数和返回值。

假设 Customer 类有属性约束:

public class Customer {

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

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

    // 构造函数、getter 和 setter
}

Reservation 类包含 Customer 属性及其他约束:

public class Reservation {

    @Valid
    private Customer customer;
    
    @Positive
    private int room;
    
    // 其他属性、构造函数、getter 和 setter
}

Reservation 作为方法参数时,可强制递归验证所有属性

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

@Valid 的双重作用:

  1. reservation 参数:调用 createNewCustomer() 时触发 Reservation 对象验证
  2. customer 属性:触发嵌套对象验证

返回值同样适用:

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

3. 验证方法约束

3.1 使用 Spring 自动验证

Spring Validation 集成了 Hibernate Validator。注意:其基于 AOP 实现,仅支持方法验证,不支持构造函数

启用自动验证需两步:

第一步:在需验证的 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({ "com.example.validation.model" })
public class MethodValidationConfig {

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

约束违反时,容器将抛出 jakarta.validation.ConstraintViolationException

✅ Spring Boot 项目:只要类路径包含 hibernate-validator,容器会自动注册 MethodValidationPostProcessor

3.2 使用 CDI 自动验证

Bean Validation 1.1+ 支持 CDI(Jakarta EE 的上下文与依赖注入)。在 Jakarta EE 容器中运行时,方法调用时自动验证约束

3.3 编程式验证

独立 Java 应用中手动验证方法,使用 jakarta.validation.executable.ExecutableValidator 接口:

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

ExecutableValidator 提供四类方法:

  • 方法验证:validateParameters()validateReturnValue()
  • 构造函数验证:validateConstructorParameters()validateConstructorReturnValue()

验证 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 或代理等拦截技术使用。

4. 结论

本文系统介绍了 Hibernate Validator 的方法约束使用及 JSR-380 新特性,涵盖:

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

并演示了通过 Spring Validator 手动和自动验证约束的实践。完整示例代码见 GitHub 仓库


原始标题:Method Constraints with Bean Validation 2.0