java 具有多个字段的 Spring 自定义注释验证

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/40353638/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 05:08:22  来源:igfitidea点击:

Spring Custom Annotation Validation with multiple field

javaspringvalidationspring-mvcannotations

提问by Hyperventilate

A little greedy question here, hope this one could also help others who want to know more about annotation validation

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

I am currently studying Spring, and for now, I am planning to try out the customize annotated validation.

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

I have searched a lot and now I know there are mainly two kinds of validations, one is used for the controller, and the other is the annotation method using @Valid

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

So here's my scenario: Suppose I have two or more fields which can be null when they are ALL NULL. But only when one of those fields contains any value except an empty string, those fields are required to have input. And I had two ideas but didn't know how to implement them correctly.

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

Here's the Class Example:

这是类示例:

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
}

Suppose I want that the birthday and confirmBirthday field need to be both null or the oppose, I may want to annotate them using one annotation for each of them and looks like this:

假设我希望birthday 和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
}

So i do need to create the validation Annotation like this:

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

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

And After that i will need to create the Validator itself:

之后我需要创建验证器本身:

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

}

So how can it be achievable?

那么如何才能实现呢?

For another idea using the same Class as an example which said that i want birthday, confirmBirthday and birthdayMessdage can only be null or the oppose at the same time. I may require to use the class annotated validation this time for cross-field validation.

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

Here's how i suppose to annotate the class:

这是我想对类进行注释的方式:

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

So when one of that field is not null, the rest of them also needs to be entered on the client size. Is it Possible?

因此,当该字段之一不为空时,其余的也需要在客户端大小上输入。是否可以?

I have read this article: How to access a field which is described in annotation property

我已阅读这篇文章:如何访问注释属性中描述的字段

But I still confusing on how the annotation validation works from those elements I listed above. Maybe I need some detail explanation on that code or even worse I may need some basic concept inspection.

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

Please Help!

请帮忙!

回答by Arne Burmeister

For this you can use a type level annotationonly because a field level annotation has no access to other fields!

为此,您只能使用类型级别的注释,因为字段级别的注释无法访问其他字段!

I did something similar to allow a choice validation (exactly one of a number of properties has to be not null). In your case the @AllOrNoneannotation (or whatever name you prefer) would need an array of field names and you will get the whole object of the annotated type to the validator:

我做了一些类似的事情来允许选择验证(许多属性中的一个必须不为空)。在您的情况下,@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;
    }
}

(As you said you use Spring I used SpEL to allow even nested fields access)

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

Now you can annotate your Subscribertype:

现在您可以注释您的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;
}

回答by Joseph Fitzgerald

Consider adding compile-time validation for the field names. For example, in @Arne answer the strings "birthday" and "confirmBirthday" are not guaranteed to match actual field names at compile time. If you want to add that functionality, here's an example from my code for a slightly different example that assumes there are exactly two fields. The purpose is to assert that two fields are ordered... For example, it could be used for "beginDate" and "endDate".

考虑为字段名称添加编译时验证。例如,在@Arne 回答中,字符串“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;
    }
}