1. 概述

Java Bean Validation 2.0版本新增了多项特性,其中最实用的就是容器元素验证功能。这个特性充分利用了Java 8引入的类型注解(Type Annotations),因此需要Java 8或更高版本支持。

⚠️ 本文实际使用的是Jakarta Bean Validation 3.0(配合Spring Boot 3和Hibernate Validator 8.x),但核心功能与2.0版本一致。

验证注解现在可以直接应用于集合、Optional对象以及其他内置或自定义容器。关于基础配置和Maven依赖,可以参考我们的前置文章

接下来我们将分场景演示如何验证各类容器元素:

2. 集合元素验证

可以对java.util.Iterablejava.util.Listjava.util.Map等集合的元素添加验证注解。

2.1 List元素验证

public class Customer {    
     List<@NotBlank(message="地址不能为空") String> addresses;
    
    // 标准getter/setter 
}

关键点:

  • @NotBlank注解作用于String元素而非整个集合
  • 空集合不会触发验证

验证测试:

@Test
public void 当地址为空时验证失败() {
    Customer customer = new Customer();
    customer.setName("John");
    customer.setAddresses(Collections.singletonList(" "));
    
    Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
    
    assertEquals(1, violations.size());
    assertEquals("地址不能为空", violations.iterator().next().getMessage());
}

2.2 Map元素验证

public class CustomerMap {
    
    private Map<@Email String, @NotNull Customer> customers;
    
    // 标准getter/setter
}

Map的键和值都可以独立添加验证注解

验证测试:

@Test
public void 当邮箱无效时验证失败() {
    CustomerMap map = new CustomerMap();
    map.setCustomers(Collections.singletonMap("john", new Customer()));
    
    Set<ConstraintViolation<CustomerMap>> violations = validator.validate(map);
 
    assertEquals(1, violations.size());
    assertEquals("必须是有效的邮箱", violations.iterator().next().getMessage());
}

3. Optional值验证

验证注解同样适用于Optional包装的值:

private Integer age;

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

验证场景:

  • 当年龄小于18时触发验证失败
  • 当年龄为null时(Optional为空)不触发验证
@Test
public void 当年龄过小时验证失败() {
    Customer customer = new Customer();
    customer.setName("John");
    customer.setAge(15);
    
    Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
    assertEquals(1, violations.size());
}

@Test
public void 当年龄为null时验证通过() {
    Customer customer = new Customer();
    customer.setName("John");
    
    Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
    assertEquals(0, violations.size());
}

4. 非泛型容器验证

对于非泛型容器,只要存在带@UnwrapByDefault注解的值提取器(Value Extractor),同样可以应用验证。

Hibernate Validator内置了这些值提取器

  • OptionalInt
  • OptionalLong
  • OptionalDouble

示例:

@Min(1)
private OptionalInt numberOfOrders;

这里@Min注解实际作用于包装的int值,而非容器本身。

5. 自定义容器验证

除了内置容器,我们还可以为自定义容器创建值提取器。

5.1 创建自定义容器

public class Profile {
    private String companyName;
    
    // 标准getter/setter 
}

5.2 添加验证注解

@NotBlank
private Profile profile;

5.3 实现值提取器

@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指定验证应用于解包后的值

5.4 注册值提取器

META-INF/services目录创建文件: javax.validation.valueextraction.ValueExtractor

内容:

com.example.validation.ProfileValueExtractor

5.5 验证测试

@Test
public void 当公司名为空时验证失败() {
    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验证各类容器元素的完整方案:

核心能力

  • 集合元素验证(List/Map)
  • Optional值验证
  • 非泛型容器验证
  • 自定义容器验证

完整示例代码可在GitHub仓库获取。


原始标题:Validating Container Elements with Bean Validation 2.0