Java使用Bean Validation验证枚举类型

1.简介

第一篇介绍了Bean Validation基本使用。本文主要介绍通过自定义Validation注解来验证枚举。系统提供的默认注解
大多数注解无法用来验证枚举。例如,使用@Pattern注解验证enum将会得到错误提示。

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

因此为了完成验证,需要自定义注解。如何自定义在Bean Validation这里已经介绍,本文主要介绍验证枚举的方法。

2 自定义注解验证枚举

这节,介绍几个验证枚举的例子。包括验证枚举名称、验证枚举成员类型及验证字符串值是否与枚举名匹配。

2.1 验证枚举名称

验证名称,即通过枚举的name方法获取枚举名,然后通过正则表达式看是否与名称的模式匹配。这里关键是在实现Validator时,值的类型要指定为Enum,随后获取枚举的公用方法对其验证。

ConstraintValidator>

enum CustomerType {
    T,S,NEW;
}


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


public class EnumNamePatternValidator implements ConstraintValidator<EnumNamePattern, Enum<?>> {

    private Pattern pattern;


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

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

@Test
public void test() {
    T t = new T();
    t.setM(CustomerType.S);

    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    Set<ConstraintViolation<T>> violations = validator.validate(t);

    for (ConstraintViolation<T> violation : violations) {
        System.out.println(violation.getMessage());
    }
}

2.2 验证变量是否为枚举成员

上一个例子中看到如何验证枚举名称。如下CustomerType枚举有三种:

enum CustomerType {
    T,S,NEW;
}

当定义一个枚举时我们希望验证是否为 T或S这两种成员。此时我们要通过自定义枚举传入CustomerType枚举数组

    CustomerType[] anyOf();
@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 {};
}

在验证变量时指定期望的类型

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

随后在验证时判断是否属于指定的集合即可。

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

2.3 验证字符串值是否与枚举名匹配

如下这个枚举:

enum CustomerType {
    T,S,NEW;
}

对于字符串String s = “T”,即验证CustomerType中是否有一个成员的名称为T。显然本例中有。为了验证,我们就需要拿到这个枚举的各个成员。而Class对象的getEnumConstants()方法即提供了这样的功能。对于CustomerType,该方法会返回一个数组包含T,S,NEW。因此最终实现如下:

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

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


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

2.4 扩展

例2.3可以看到,既然能拿到Class对象,那么还能拿到一些其它额外的信息。例如获取方法名。调用枚举的方法,验证某个数字是否是为一个枚举中定义的code。最终实功能更强大的自定义注解。同理,对其非枚举类型,思路也是一样。

3. 总结

通过2的3个例子我们可以看到,为了验证枚举,需要完成三件事。

  1. 首先通过注解提供需要检测的信息。
  2. 其次通过Enum或Class拿到足够多的关于验证对象的信息。其它类型也是同理。
  3. 最后通过比较获取到的值的信息和注解中提供的信息来验证是否满足我们的期望来完成验证。

2是一个关键,只有通过不同渠道获取足够的信息,才能完成丰富的验证。

4. 参考

[1].https://www.baeldung.com/javax-validations-enums
[2].https://stackoverflow.com/questions/6294587/java-string-validation-using-enum-values-and-annotation

你可能感兴趣的:(Java)