【问题标题】:Spring Custom Annotation Validation with multiple field具有多个字段的 Spring 自定义注释验证
【发布时间】:2017-03-14 05:06:02
【问题描述】:

这里有点贪心的问题,希望这个问题也可以帮助其他想了解更多注释验证的人

我目前正在学习 Spring,目前我正计划尝试自定义带注释的验证。

查了很多,现在知道验证主要有两种,一种是用于控制器的,另一种是使用@Valid的注解方法

所以这是我的场景: 假设我有两个或多个字段,当它们都为 NULL 时它们可以为空。 但只有当这些字段之一包含除空字符串之外的任何值时,这些字段才需要输入。我有两个想法,但不知道如何正确实施。

这是类示例:

public class Subscriber {
    private String name;
    private String email;
    private Integer age;
    private String phone;
    private Gender gender;
    private Date birthday;
    private Date confirmBirthday;
    private String birthdayMessage;
    private Boolean receiveNewsletter;

    //Getter and Setter
}

假设我希望生日和 ConfirmBirthday 字段都需要为 null 或相反,我可能想为它们每个使用一个注释来注释它们,如下所示:

public class Subscriber {
    private String name;
    private String email;
    private Integer age;
    private String phone;
    private Gender gender;

    @NotNullIf(fieldName="confirmBirthday")
    private Date birthday;

    @NotNullIf(fieldName="birthday")
    private Date confirmBirthday;

    private String birthdayMessage;
    private Boolean receiveNewsletter;

    //Getter and Setter
}

所以我确实需要像这样创建验证注释:

@Documented
@Constraint(validatedBy = NotNullIfConstraintValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD })
public @interface NotNullIf {

    String fieldName();

    String message() default "{NotNullIf.message}";
    Class<?>[] group() default {};
    Class<? extends Payload>[] payload() default {};
}

然后我需要自己创建验证器:

public class NotNullIfConstraintValidator implements ConstraintValidator<NotNullIf, String>{

    private String fieldName;

    public void initialize(NotNullIf constraintAnnotation) {
        fieldName = constraintAnnotation.fieldName();
    }

    public boolean isValid(String value, ConstraintValidatorContext context) {
        if(value == null) {
            return true;
        };
        //TODO Validation
        return false;
    }

}

那么如何实现呢?

另一个想法使用相同的类作为一个例子,说我想要生日,confirmBirthday 和birthdayMessdage 只能为空或同时反对。 这次我可能需要使用类注释验证来进行跨字段验证。

这是我想注释类的方式:

@NotNullIf(fieldName={"birthday", "confirmBirthday", "birthdayMessage"})
public class Subscriber {
    //Those field same as the above one
}

所以当其中一个字段不为空时,其余字段也需要在客户端大小上输入。 有可能吗?

我看过这篇文章:How to access a field which is described in annotation property

但我仍然对上面列出的这些元素的注释验证如何工作感到困惑。 也许我需要对该代码进行一些详细的解释,或者更糟的是我可能需要一些基本的概念检查。

请帮忙!

【问题讨论】:

    标签: java spring validation spring-mvc annotations


    【解决方案1】:

    为此,您只能使用type level annotation,因为字段级注释无权访问其他字段!

    我做了类似的事情来允许选择验证(许多属性中的一个必须不为空)。在您的情况下,@AllOrNone 注释(或您喜欢的任何名称)需要一个字段名称数组,您将获得带注释类型的整个对象到验证器:

    @Target(ElementType.TYPE)
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = AllOrNoneValidator.class)
    public @interface AllOrNone {
        String[] value();
    
        String message() default "{AllOrNone.message}";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
    
    public class AllOrNoneValidator implements ConstraintValidator<AllOrNone, Object> {
        private static final SpelExpressionParser PARSER = new SpelExpressionParser();
        private String[] fields;
    
        @Override
        public void initialize(AllOrNone constraintAnnotation) {
            fields = constraintAnnotation.value();
        }
    
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            long notNull = Stream.of(fields)
                    .map(field -> PARSER.parseExpression(field).getValue(value))
                    .filter(Objects::nonNull)
                    .count();
            return notNull == 0 || notNull == fields.length;
        }
    }
    

    (正如您所说,您使用 Spring 我使用 SpEL 甚至允许访问嵌套字段)

    现在您可以注释您的Subscriber 类型:

    @AllOrNone({"birthday", "confirmBirthday"})
    public class Subscriber {
        private String name;
        private String email;
        private Integer age;
        private String phone;
        private Gender gender;
        private Date birthday;
        private Date confirmBirthday;
        private String birthdayMessage;
        private Boolean receiveNewsletter;
    }
    

    【讨论】:

    • 除此之外我们还需要做什么才能开始使用自定义注释吗?
    • @TusharBanne 如果你有一个运行中的验证框架,比如 hibernate 验证器,没有
    • 有没有办法验证传递给@AllOrNone 的参数是编译时存在的字段名称? @AllOrNone({"birthdayX", "confirmBirthdayZ"}) 将编译,但会在运行时爆炸。
    • 其实@ArneBurmeister,你可以!如果你让你的 AllOrNoneValidator 扩展 AbstractProcessor。您将覆盖 init 方法来获取消息传递者(我没有拼错)。然后你将覆盖 process 方法,它有点复杂,但你可以测试字段名称是否与对象中的字段匹配。
    【解决方案2】:

    考虑为字段名称添加编译时验证。例如,在@Arne answer 中,字符串“birthday”和“confirmBirthday”不能保证在编译时匹配实际的字段名称。如果您想添加该功能,下面是我的代码中的一个示例,该示例假设正好有两个字段,但略有不同。目的是断言两个字段是有序的......例如,它可以用于“beginDate”和“endDate”。

    public class OrderedValidator extends AbstractProcessor implements ConstraintValidator<Ordered, Object>
    {
        private String field1;
        private String field2;
    
        private Messager messager;
    
        public void initialize(Ordered constraintAnnotation)
        {
            this.field1 = constraintAnnotation.field1();
            this.field2 = constraintAnnotation.field2();
        }
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv)
        {
            super.init(processingEnv);
            messager = processingEnv.getMessager();
        }
    
        @SuppressWarnings("unchecked")
        public boolean isValid(Object value, ConstraintValidatorContext context)
        {
            Object field1Value = new BeanWrapperImpl(value).getPropertyValue(field1);
            Object field2Value = new BeanWrapperImpl(value).getPropertyValue(field2);
    
            boolean valid = true;
    
            if (field1Value != null && field2Value != null)
            {
                if (field1Value.getClass().equals(field2Value.getClass()))
                {
                    valid = ((Comparable) field1Value).compareTo((Comparable) field2Value) <= 0;
                }
            }
    
            return valid;
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
        {
            for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Ordered.class))
            {
                if (annotatedElement.getKind() != ElementKind.CLASS)
                {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated with " + Ordered.class.getSimpleName());
                    return true;
                }
    
                TypeElement typeElement = (TypeElement) annotatedElement;
    
                List<? extends Element> elements = typeElement.getEnclosedElements();
                boolean field1Found = false;
                boolean field2Found = false;
                for (Element e : elements)
                {
                    if (e.getKind() == ElementKind.FIELD && field1 != null && field1.equals(e.getSimpleName()))
                    {
                        field1Found = true;
                    }
                    else if (e.getKind() == ElementKind.FIELD && field2 != null && field2.equals(e.getSimpleName()))
                    {
                        field2Found = true;
                    }
                }
    
                if (field1 != null && !field1Found)
                {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Could not find field named " + field1);
                    return true;
                }
    
                if (field2 != null && !field2Found)
                {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Could not find field named " + field2);
                    return true;
                }
            }
    
            return false;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2018-06-08
      • 2016-09-26
      • 2012-11-30
      • 1970-01-01
      • 1970-01-01
      • 2020-07-19
      • 2017-12-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多