使用 @Valid 注解校验嵌套对象

1、简介

本文将带你了解如何使用 @Valid 注解来验证对象及其嵌套的子对象。

当传入数据是基本数据类型,比如 IntegerString 时,验证数据可以很简单。然而,当传入信息是一个对象,特别是一个对象图时,验证可能会更加困难。幸运的是,@Valid 注解简化了对嵌套子对象的验证。

2、@Valid 注解是啥

@Valid 注解来自 Jakarta Bean Validation 规范,用于标记需要验证的特定参数。

使用该注解可确保传递给方法或存储在字段中的数据符合指定的验证规则。这有助于提高数据的完整性和一致性。

当在 JavaBean 的字段或方法上使用时,它会触发所有已定义的约束检查。Bean Validation API 中最常用的一些约束包括 @NotNull@NotBlank@NotEmpty@Size@Email@Pattern 等。

3、在子对象上使用 @Valid 注解

首先,必须确定验证规则,并对字段应用前面提到的验证约束。

接下来,定义一个 Project 类,该类包含一个嵌套的 User 对象,并用 @Valid 注解来装饰该对象:

public class Project {

    @NotBlank(message = "Project title must be present")
    @Size(min = 3, max = 20, message = "Project title size not valid")
    private String title;

    @Valid          // 校验嵌套的对象
    private User owner;

    // 构造函数、Getter、Setter 方法省略

}
public class User {

    // 校验规则
    @NotBlank(message = "User name must be present")
    @Size(min = 3, max = 50, message = "User name size not valid")
    private String name;

    // 校验规则
    @NotBlank(message = "User email must be present")
    @Email(message = "User email format is incorrect")
    private String email;

    // 构造函数、Getter、Setter 方法省略

}

之后,通过 Validator 实例的 validate() 方法来验证对象:

@Test
public void whenInvalidProjectAndUser_thenAssertConstraintViolations() {
    Project project = new Project(null);
    project.setOwner(new User(null, "invalid-email"));

    List<String> messages = validate(project);

    assertEquals(3, messages.size());
    assertTrue(messages.contains("Project title must be present"));
    assertTrue(messages.contains("User name must be present"));
    assertTrue(messages.contains("User email format is incorrect"));
}

private List<String> validate(Project project) {
    return validator.validate(project)
      .stream()
      .map(ConstraintViolation::getMessage)
      .collect(Collectors.toList());
}

此外,@Valid 注解可与 Spring 和 Jakarta EE 等框架完美配合。通过在 Controller 类的方法参数上使用该注解,可以在进入 Controller 方法之前就执行验证,这对于保持数据的一致性是再好不过了。

4、对象图验证

在对象有其他嵌套对象的情况下,需要使用一种称为对象图验证(Object Graph Validation)的机制。

该机制可验证对象图中相关对象的整个结构。所有使用 @Valid 注解的子对象(以及它们的子对象)在其父对象被验证时也会被验证。换句话说,验证在整个图中递归应用。

通过这种图遍历,可以得到一个 ConstraintViolations 集合,其中包含嵌套对象的所有验证失败的行为。

由于递归验证图中的每个对象,因此可能会遇到循环引用的问题,即对象在循环中相互引用。这可能会导致死循环,不断重复验证相同的对象。

幸运的是,Jakarta Bean Validation 包含定义验证路径的概念,即从根(ROOT)对象开始的 @Valid 关联序列。该实现跟踪已在当前路径中验证的每个实例,从根对象开始。如果同一实例在给定导航路径中出现多次,验证会忽略它,从而防止死循环。

5、在子对象上使用注解

接下来看看,如何在嵌套实例、集合和容器对象中的泛型参数上使用 @Valid

5.1、使用 @Valid 验证嵌套的实例

验证嵌套实例的一种方法是使用字段访问策略,就像上一个示例中验证 Project 内部嵌套的用户 User 一样。只需用 @Valid 注解装饰字段,该实例就会添加到导航路径中:

@Valid
private User owner;

同样,验证嵌套实例的另一种方法是使用属性访问策略,这意味着可以在访问属性的 getter 方法上添加 @Valid

@Valid
public User getOwner() {
    return owner;
}

5.2、使用 @Valid 验证可迭代数据

集合、数组或 java.lang.Iterebale 接口的任何其他实现都可以使用 @Valid 注解。此时,会按照相同的规则对 Iterable 的每个元素进行验证。

注意,如果验证的是 java.util.Map 接口的实现,那么默认情况下只有 Value 会被验证。如果你确实需要对 Key 也进行验证,那么可以给 Key 也添加 @Valid 注解。

如下,对 Map 中的 KeyValue 都进行校验:

private Map<@Valid User, @Valid Task> assignedTasks;

5.3、在容器对象和泛型参数上使用注解

在容器对象和泛型参数上应用注解非常相似:


// @Valid 定义在容器对象上
@Valid 
private List<Task> tasks;

// @Valid 定义在泛型参数上
private List<@Valid Task> tasks;

第一个示例是在容器上使用注解,而第二个示例是直接在泛型参数上使用注解。在这种情况下,两者并无区别,都能按照预期运行。原则上,应该避免在两个地方都使用注解,因为这可能导致容器元素被验证两次。

如你所见,注解的使用是灵活的,但也有翻车的情况。例如,在嵌套泛型容器的情况下,要验证容器中的对象,就必须在内部容器的泛型引用上添加注解。

如下,一个嵌套在 Map 中的 List,需要在其泛型添加注解:

private Map<String, List<@Valid Task>> taskByType;

6、总结

本文介绍了什么是 @Valid 注解、如何使用它对子对象执行验证以及对象图验证的工作原理。

@Valid 注解是一个功能强大的工具,可以在不同的地方使用它来确保事物按照预期进行验证。它能自动检查对象图中的每个需要验证对象,使我们的工作更加轻松。


Ref:https://www.baeldung.com/java-valid-annotation-child-objects