枚举类型的验证

2023/05/12

1. 简介

在教程Java Bean Validation基础中,我们了解了如何使用JSR 380将javax验证应用于各种类型。在教程Spring MVC 自定义验证中,我们看到了如何创建自定义验证。

在下一个教程中,我们将重点介绍如何使用自定义注解为枚举构建验证

2. 验证枚举

不幸的是,大多数标准注解不能应用于枚举

例如,当将@Pattern注解应用于枚举时,我们会收到与HibernateValidator类似的错误:

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 
 'javax.validation.constraints.Pattern' validating type 'cn.tuyucheng.taketoday.javaxval.enums.demo.CustomerType'. 
 Check configuration for 'customerTypeMatchesPattern'

实际上,唯一可以应用于枚举的标准注解是@NotNull和@Null。

3. 验证枚举的模式

让我们首先定义一个注解来验证枚举的模式

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = EnumNamePatternValidator.class)
public @interface EnumNamePattern {
    String regexp();
    String message() default "must match \"{regexp}\"";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

现在我们可以简单地使用正则表达式将这个新注解添加到我们的CustomerType枚举中:

@EnumNamePattern(regexp = "NEW|DEFAULT")
private CustomerType customerType;

正如我们所见,注解实际上并不包含验证逻辑。因此,我们需要提供一个ConstraintValidator

public class EnumNamePatternValidator implements ConstraintValidator<EnumNamePattern, Enum<?>> {
    private Pattern pattern;

    @Override
    public void initialize(EnumNamePattern annotation) {
        try {
            pattern = Pattern.compile(annotation.regexp());
        } catch (PatternSyntaxException e) {
            throw new IllegalArgumentException("Given regex is invalid", e);
        }
    }

    @Override
    public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        Matcher m = pattern.matcher(value.name());
        return m.matches();
    }
}

在此示例中,实现与标准@Pattern验证器非常相似。但是,这一次,我们匹配枚举的名称

4. 验证枚举的子集

将枚举与正则表达式匹配不是类型安全的。相反,与枚举的实际值进行比较更有意义

但是,由于注解的限制,这样的注解不能通用。这是因为注解的参数只能是特定枚举的具体值,而不能是枚举父类的实例。

让我们看看如何为我们的CustomerType枚举创建特定的子集验证注解:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = CustomerTypeSubSetValidator.class)
public @interface CustomerTypeSubset {
    CustomerType[] anyOf();
    String message() default "must be any of {anyOf}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

然后可以将此注解应用于CustomerType类型的枚举:

@CustomerTypeSubset(anyOf = {CustomerType.NEW, CustomerType.OLD})
private CustomerType customerType;

接下来,我们需要定义CustomerTypeSubSetValidator来检查给定枚举值列表是否包含当前值

public class CustomerTypeSubSetValidator implements ConstraintValidator<CustomerTypeSubset, CustomerType> {
    private CustomerType[] subset;

    @Override
    public void initialize(CustomerTypeSubset constraint) {
        this.subset = constraint.anyOf();
    }

    @Override
    public boolean isValid(CustomerType value, ConstraintValidatorContext context) {
        return value == null || Arrays.asList(subset).contains(value);
    }
}

虽然注解必须特定于某个枚举,但我们当然可以在不同的验证器之间共享代码

5. 验证字符串是否与枚举的值匹配

除了验证枚举以匹配字符串之外,我们还可以做相反的事情。为此,我们可以创建一个注解来检查字符串是否对特定枚举有效。

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = ValueOfEnumValidator.class)
public @interface ValueOfEnum {
    Class<? extends Enum<?>> enumClass();
    String message() default "must be any of enum {enumClass}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

可以将此注解添加到String字段,我们可以传递任何枚举类。

@ValueOfEnum(enumClass = CustomerType.class)
private String customerTypeString;

让我们定义ValueOfEnumValidator来检查String(或任何CharSequence)是否包含在枚举中

public class ValueOfEnumValidator implements ConstraintValidator<ValueOfEnum, CharSequence> {
    private List<String> acceptedValues;

    @Override
    public void initialize(ValueOfEnum annotation) {
        acceptedValues = Stream.of(annotation.enumClass().getEnumConstants())
                .map(Enum::name)
                .collect(Collectors.toList());
    }

    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        return acceptedValues.contains(value.toString());
    }
}

在使用JSON对象时,此验证尤其有用。因为在将不正确的值从JSON对象映射到枚举时会出现以下异常:

Cannot deserialize value of type CustomerType from String value 'UNDEFINED': value not one
 of declared Enum instance names: [...]

我们当然可以处理这个异常。但是,这不允许我们一次报告所有违规行为。

我们可以将它映射到String而不是将值映射到枚举。然后我们将使用我们的验证器来检查它是否匹配任何枚举值。

6. 整合一切

现在,我们可以使用我们的任何新验证注解来验证bean。最重要的是,我们所有的验证都接受空值。因此,我们也可以将它与注解@NotNull结合起来:

public class Customer {
    @ValueOfEnum(enumClass = CustomerType.class)
    private String customerTypeString;

    @NotNull
    @CustomerTypeSubset(anyOf = {CustomerType.NEW, CustomerType.OLD})
    private CustomerType customerTypeOfSubset;

    @EnumNamePattern(regexp = "NEW|DEFAULT")
    private CustomerType customerTypeMatchesPattern;

    // constructor, getters etc.
}

在下一节中,我们将了解如何测试新注解。

7. 测试枚举的Javax验证

为了测试我们的验证器,我们将设置一个验证器,它支持我们新定义的注解。我们将为所有测试使用Customer bean。

首先,我们要确保有效的Customer实例不会导致任何违规行为:

@Test 
public void whenAllAcceptable_thenShouldNotGiveConstraintViolations() { 
    Customer customer = new Customer(); 
    customer.setCustomerTypeOfSubset(CustomerType.NEW); 
    Set violations = validator.validate(customer); 
    assertThat(violations).isEmpty(); 
}

其次,我们希望我们的新注解支持和接受空值。我们只希望有一次违规。这应该通过@NotNull注解在customerTypeOfSubset上报告:

@Test
public void whenAllNull_thenOnlyNotNullShouldGiveConstraintViolations() {
    Customer customer = new Customer();
    Set<ConstraintViolation> violations = validator.validate(customer);
    assertThat(violations.size()).isEqualTo(1);

    assertThat(violations)
        .anyMatch(havingPropertyPath("customerTypeOfSubset")
        .and(havingMessage("must not be null")));
}

最后,当输入无效时,我们验证验证器以报告违规行为:

@Test
public void whenAllInvalid_thenViolationsShouldBeReported() {
    Customer customer = new Customer();
    customer.setCustomerTypeString("invalid");
    customer.setCustomerTypeOfSubset(CustomerType.DEFAULT);
    customer.setCustomerTypeMatchesPattern(CustomerType.OLD);

    Set<ConstraintViolation> violations = validator.validate(customer);
    assertThat(violations.size()).isEqualTo(3);

    assertThat(violations)
        .anyMatch(havingPropertyPath("customerTypeString")
        .and(havingMessage("must be any of enum class com.baeldung.javaxval.enums.demo.CustomerType")));
    assertThat(violations)
        .anyMatch(havingPropertyPath("customerTypeOfSubset")
        .and(havingMessage("must be any of [NEW, OLD]")));
    assertThat(violations)
        .anyMatch(havingPropertyPath("customerTypeMatchesPattern")
        .and(havingMessage("must match \"NEW|DEFAULT\"")));
}

8. 总结

在本教程中,我们介绍了使用自定义注解和验证器验证枚举的三个选项

首先,我们学习了如何使用正则表达式验证枚举的名称。

其次,我们讨论了对特定枚举值的子集的验证。我们还解释了为什么我们不能构建通用注解来执行此操作。

最后,我们还研究了如何为字符串构建验证器。为了检查String是否符合给定枚举的特定值。

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

Show Disqus Comments

Post Directory

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