使用Jakarta Bean Validation 3.0验证容器元素

2023/05/12

1. 概述

之前的2.0版本的Java Bean Validation规范已经添加了几个新特性,其中包括验证容器元素的可能性。

这个新功能利用了Java 8中引入的类型注解。因此它需要Java 8或更高版本才能工作。

但是,在本文中,使用了Jakarta Bean Validation 3.0,它需要Java 17,因为我们使用最新的Spring Boot 3来获取参考实现Hibernate Validator 8.x。

可以将验证注解添加到容器中,例如集合、Optional对象和其他内置以及自定义容器

有关Java Bean Validation的介绍以及如何设置我们需要的Maven依赖项,请在此处查看我们之前的文章

在以下部分中,我们将重点介绍如何验证每种类型的容器的元素。

2. 集合元素

我们可以将验证注解添加到类型为java.util.Iterable、java.util.List和java.util.Map的集合元素。

让我们看一个验证List元素的示例:

public class Customer {
    List<@NotBlank(message="Address must not be blank") String> addresses;

    // standard getters, setters 
}

在上面的示例中,我们为Customer类定义了一个addresses属性,其中不能包含为空字符串的元素。

请注意,@NotBlank验证应用于String元素,而不是整个集合。如果集合为空,则不应用任何验证。

让我们验证一下,如果我们尝试向addresses列表添加一个空字符串,验证框架将返回一个ConstraintViolation:

@Test
public void whenEmptyAddress_thenValidationFails() {
    Customer customer = new Customer();
    customer.setName("John");

    customer.setAddresses(Collections.singletonList(" "));
    Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
    
    assertEquals(1, violations.size());
    assertEquals("Address must not be blank", violations.iterator().next().getMessage());
}

接下来,让我们看看如何验证Map类型集合的元素:

public class CustomerMap {

    private Map<@Email String, @NotNull Customer> customers;

    // standard getters, setters
}

请注意,我们可以为Map元素的键和值添加验证注解

让我们验证添加带有无效电子邮件的条目是否会导致验证错误:

@Test
public void whenInvalidEmail_thenValidationFails() {
    CustomerMap map = new CustomerMap();
    map.setCustomers(Collections.singletonMap("john", new Customer()));
    Set<ConstraintViolation<CustomerMap>> violations = validator.validate(map);
 
    assertEquals(1, violations.size());
    assertEquals("Must be a valid email", violations.iterator().next().getMessage());
}

3. Optional值

验证约束也可以应用于Optional值:

private Integer age;

public Optional<@Min(18) Integer> getAge() {
    return Optional.ofNullable(age);
}

让我们创建一个年龄过低的Customer-并验证这是否会导致验证错误:

@Test
public void whenAgeTooLow_thenValidationFails() {
    Customer customer = new Customer();
    customer.setName("John");
    customer.setAge(15);
    Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
 
    assertEquals(1, violations.size());
}

另一方面,如果age为空,则不会验证Optional值:

@Test
public void whenAgeNull_thenValidationSucceeds() {
    Customer customer = new Customer();
    customer.setName("John");
    Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
 
    assertEquals(0, violations.size());
}

4. 非泛型容器元素

除了为类型参数添加注解外,我们还可以对非泛型容器应用验证,只要有一个带有@UnwrapByDefault注解的类型的值提取器。

值提取器是从容器中提取值以进行验证的类。

参考实现包含OptionalInt、OptionalLong和OptionalDouble的值提取器

@Min(1)
private OptionalInt numberOfOrders;

在这种情况下,@Min注解适用于包装的Integer值,而不是容器。

5. 自定义容器元素

除了内置的值提取器之外,我们还可以定义自己的值提取器并将它们注册到容器类型中。

通过这种方式,我们可以向自定义容器的元素添加验证注解。

让我们添加一个包含companyName属性的新Profile类:

public class Profile {
    private String companyName;

    // standard getters, setters 
}

接下来,我们要在Customer类中添加一个带有@NotBlank注解的Profile属性-验证companyName不是空字符串:

@NotBlank
private Profile profile;

为此,我们需要一个值提取器来确定要应用于companyName属性的验证,而不是直接应用于Profile对象。

让我们添加一个实现ValueExtractor接口并覆盖extractValue()方法的ProfileValueExtractor类:

@UnwrapByDefault
public class ProfileValueExtractor implements ValueExtractor<@ExtractedValue(type = String.class) Profile> {

    @Override
    public void extractValues(Profile originalValue, ValueExtractor.ValueReceiver receiver) {
        receiver.value(null, originalValue.getCompanyName());
    }
}

这个类还需要指定使用@ExtractedValue注解提取的值的类型。

此外,我们还添加了@UnwrapByDefault注解,指定验证应应用于解包值而不是容器

最后,我们需要通过将名为javax.validation.valueextraction.ValueExtractor的文件添加到META-INF/services目录来注册该类,其中包含我们的ProfileValueExtractor类的全名:

org.baeldung.javaxval.container.validation.valueextractors.ProfileValueExtractor

现在,当我们使用空的companyName验证具有profile属性的Customer对象时,我们将看到验证错误:

@Test
public void whenProfileCompanyNameBlank_thenValidationFails() {
    Customer customer = new Customer();
    customer.setName("John");
    Profile profile = new Profile();
    profile.setCompanyName(" ");
    customer.setProfile(profile);
    Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
 
    assertEquals(1, violations.size());
}

请注意,如果你使用的是hibernate-validator-annotation-processor,将验证注解添加到自定义容器类,当它被标记为@UnwrapByDefault时,将导致版本6.0.2中的编译错误。

这是一个已知问题,可能会在未来的版本中得到解决。

6. 总结

在本文中,我们展示了如何使用Jakarta Bean Validation 3.0验证多种类型的容器元素。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章