【问题标题】:How to validate an attribute based on another in spring-boot in a clean way?如何以干净的方式在spring-boot中基于​​另一个属性验证一个属性?
【发布时间】:2020-11-08 20:46:10
【问题描述】:

我正在 spring-boot 2.3.1.RELEASE Web 应用程序中验证 REST 服务请求/bean。目前,我正在使用 Hibernate Validator,尽管我愿意使用任何其他方式进行验证。

假设,我有一个模型Foo,我在Rest Controller 中收到它作为请求。我想验证completionDate 是否不是null 那么status 应该是“完成”或“关闭”。

@StatusValidate
public class Foo {
    private String status;
    private LocalDate completionDate;
    // getters and setters
}

我创建了一个自定义类级别注释@StatusValidate

@Constraint(validatedBy = StatusValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface StatusValidate {

    String message() default "default status error";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

我创建了StatusValidator 类。

public class StatusValidator implements ConstraintValidator<StatusValidate, Foo> {

    @Override
    public void initialize(StatusValidateconstraintAnnotation) {
    }

    @Override
    public boolean isValid(Foovalue, ConstraintValidatorContext context) {
        if (null != value.getCompletionDate() && (!value.getStatus().equalsIgnoreCase("complete") && !value.getStatus().equalsIgnoreCase("closed"))) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).
                    .addPropertyNode("status").addConstraintViolation();
            return false;
        }
        return true;
    }
}

当我验证Foo 对象时(通过使用@Valid@Validated 或手动调用validator.validate() 方法),我在ConstraintViolation 中获得以下数据。 代码:

// Update.class is a group
Set<ConstraintViolation<Foo>> constraintViolations = validator.validate(foo, Update.class);
constraintViolations.forEach(constraintViolation -> {
    ErrorMessage errorMessage = new ErrorMessage();
    errorMessage.setKey(constraintViolation.getPropertyPath().toString());
    errorMessage.setValue(constraintViolation.getInvalidValue());
    // Do something with errorMessage here
});

constraintViolation.getPropertyPath().toString() => 状态

constraintViolation.getInvalidValue() =>(Foo 对象)

如何在自定义ConstraintValidator 或其他任何地方设置无效值(status 属性的实际值),以便constraintViolation.getInvalidValue() 返回status 属性的值? 在一个属性的验证取决于另一个属性的值的情况下,是否有更好的方法来验证请求负载/bean?

编辑: 我可以做类似的事情

if(constraintViolation.getPropertyPath().toString().equals("status")) {
    errorMessage.setValue(foo.getStatus());
}

但这将涉及在某处维护属性名称的字符串常量,例如。 "status"。不过,在StatusValidator 中,我也在设置属性名称.addPropertyNode("status"),这也是我想避免的。


总结: 我正在寻找解决方案(不一定使用自定义验证或休眠验证器),其中

  1. 我可以验证 json 请求者或 bean,以验证其验证取决于其他属性值的属性。
  2. 我不必在任何地方将 bean 属性名称维护为字符串常量(维护噩梦)。
  3. 我能够获取无效的属性名称和值。

【问题讨论】:

  • 您是否尝试检查constraintViolation.getInvalidValue()instantOf Foo,并对其进行转换并获取status 的值?
  • 感谢您的评论。我可以这样做,但我将拥有多个自定义验证注释和约束验证器。这将涉及对constraintViolation.getPropertyPath() 执行if else,然后根据它从Foo 对象中获取值。我试图找到一种方法来避免这个手动过程。
  • 你需要什么价值(你是如何使用它的)?如果您需要将其嵌入到违规消息中,您可以简单地使用context.unwrap(HibernateConstraintValidatorContext.class).addExpressionVariable("status", value.getStatus()) 并使用${status} 在验证消息中引用它
  • 至于你所说的“维护噩梦”,可以使用Lombok's @FieldNameConstants
  • 感谢@crizzis 的评论,我需要在json 错误响应中以单独的键发送属性名称和值。

标签: java spring-boot bean-validation


【解决方案1】:

您可以使用dynamic payload 在约束违规中提供额外数据。可以使用HibernateConstraintValidatorContext进行设置:

context.unwrap(HibernateConstraintValidatorContext.class)
        .withDynamicPayload(foo.getStatus().toString());

javax.validation.ConstraintViolation 可以依次解包到HibernateConstraintViolation 以检索动态有效负载:

constraintViolation.unwrap(HibernateConstraintViolation.class)
        .getDynamicPayload(String.class);

在上面的例子中,我们传递了一个简单的字符串,但是你可以传递一个包含你需要的所有属性的对象。


请注意,这仅适用于 Hibernate Validator,这是 Bean 验证规范 (JSR-303/JSR-349) 的最广泛使用的实现,并被 Spring 用作其默认验证供应商。

【讨论】:

  • 很好的答案。很高兴知道
  • 感谢@anar 的回答。这样我可以得到无效的值,我将如何获得无效的属性名称?
  • @Smile 但是你已经在你的代码中使用getPropertyPath() 做到了,不是吗?我还想指出,在动态有效负载中,您不仅可以传递字符串,还可以传递例如一个对象,在您的情况下,该对象可以包含属性名称和值。
  • 是的,正确的@Anar 目前,我设置了像context.getDefaultConstraintMessageTemplate()).addPropertyNode("status") 这样的无效属性,这再次涉及维护java bean 属性的字符串常量。我想避免这样做。
  • 没有“更清洁”的方法。您可以单独验证字段,也可以验证整个对象,然后您必须自己指定对象的一个​​或多个字段到底出了什么问题。当您在进行验证的相同方法中使用字符串名称并且您知道您实际验证的是哪个字段时,我不太明白使用字符串名称有什么问题。当然,您也可以使用反射来获取属性名称,但在我看来这很愚蠢。祝你好运!
【解决方案2】:

您可以使用表达式语言来评估属性路径。例如

Set<ConstraintViolation<Foo>> constraintViolations = validator.validate(foo);
    constraintViolations.forEach(constraintViolation -> {
        Path propertyPath = constraintViolation.getPropertyPath();
        Foo rootBean = constraintViolation.getRootBean();

        Object invalidPropertyValue = getPropertyValue(rootBean, propertyPath);
        System.out.println(MessageFormat.format("{0} = {1}", propertyPath, invalidPropertyValue));
    });

private static Object getPropertyValue(Object bean, Path propertyPath) {
    ELProcessor el = new ELProcessor();
    el.defineBean("bean", bean);
    String propertyExpression = MessageFormat.format("bean.{0}", propertyPath);
    Object propertyValue = el.eval(propertyExpression);
    return propertyValue;
}

表达式语言也适用于嵌套 bean。这是一个完整的例子

您将需要 Java >1.8 和以下依赖项:

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.0.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.2.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>6.0.2.Final</version>
</dependency>

还有我的java代码

public class Main {

    public static void main(String[] args) {
        ValidatorFactory buildDefaultValidatorFactory = Validation.buildDefaultValidatorFactory();

        Validator validator = buildDefaultValidatorFactory.getValidator();

        // I added Bar to show how nested bean property validation works
        Bar bar = new Bar();

        // Must be 2 - 4 characters
        bar.setName("A");

        Foo foo = new Foo();
        foo.setBar(bar);
        foo.setCompletionDate(LocalDate.now());

        // must be complete or closed
        foo.setStatus("test");

        Set<ConstraintViolation<Foo>> constraintViolations = validator.validate(foo);

        System.out.println("Invalid Properties:");

        constraintViolations.forEach(constraintViolation -> {
            Path propertyPath = constraintViolation.getPropertyPath();
            Foo rootBean = constraintViolation.getRootBean();

            Object invalidPropertyValue = getPropertyValue(rootBean, propertyPath);
            System.out.println(MessageFormat.format("{0} = {1}", propertyPath, invalidPropertyValue));
        });
    }

    private static Object getPropertyValue(Object bean, Path propertyPath) {
        ELProcessor el = new ELProcessor();
        el.defineBean("bean", bean);
        String propertyExpression = MessageFormat.format("bean.{0}", propertyPath);
        Object propertyValue = el.eval(propertyExpression);
        return propertyValue;
    }

    @StatusValidate
    public static class Foo {
        private String status;
        private LocalDate completionDate;

        @Valid
        private Bar bar;

        public void setBar(Bar bar) {
            this.bar = bar;
        }

        public Bar getBar() {
            return bar;
        }

        public String getStatus() {
            return status;
        }

        public void setStatus(String status) {
            this.status = status;
        }

        public LocalDate getCompletionDate() {
            return completionDate;
        }

        public void setCompletionDate(LocalDate completionDate) {
            this.completionDate = completionDate;
        }

    }

    public static class Bar {

        @Size(min = 2, max = 4)
        private String status;

        public String getStatus() {
            return status;
        }

        public void setName(String status) {
            this.status = status;
        }
    }

    @Constraint(validatedBy = StatusValidator.class)
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface StatusValidate {

        String message()

        default "default status error";

        Class<?>[] groups() default {};

        Class<? extends Payload>[] payload() default {};
    }

    public static class StatusValidator implements ConstraintValidator<StatusValidate, Foo> {

        @Override
        public boolean isValid(Foo value, ConstraintValidatorContext context) {
            if (null != value.getCompletionDate() && (!value.getStatus().equalsIgnoreCase("complete")
                    && !value.getStatus().equalsIgnoreCase("closed"))) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                        .addPropertyNode("status").addConstraintViolation();
                return false;
            }
            return true;
        }
    }
}

输出是:

Invalid Properties:
status = test
bar.status = A

【讨论】:

  • 感谢您的回答。这部分解决了问题,但仍然涉及在 context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addPropertyNode("status").addConstraintViolation(); 中维护 bean 属性名称 "status"
【解决方案3】:

使用@NotNull 作为完成日期并使用自定义枚举验证器作为这样的状态:

  /*enum class*/
public enum Status{
    COMPLETE,
    CLOSED
}

/*custom validator*/
@ValueValidator(EnumValidatorClass = Status.class)
@NotNull
private String status;

@NotNull
private LocalDate completionDate;

 /*anotation interface*/
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
@Documented
public @interface ValueValidator {
    
       public abstract String message() default "Invalid Status!";
     
        public abstract Class<?>[] groups() default {};
      
        public abstract Class<? extends Payload>[] payload() default {};
         
        public abstract Class<? extends java.lang.Enum<?>> EnumValidatorClass();
         

}

/*anotation implementation*/
public class EnumValueValidator implements ConstraintValidator<ValueValidator, String>{

 private List<String> values;
 
@Override
public void initialize(ValueValidator annotation)
{
    values = Stream.of(annotation.EnumValidatorClass().getEnumConstants()).map(Enum::name).collect(Collectors.toList());
}

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

    return values.contains(value);
}}

【讨论】:

  • 问题是用于验证的组合 - 只有在 completionDate 不为空时,状态才应该是完整的或关闭的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-28
  • 1970-01-01
  • 1970-01-01
  • 2020-03-02
  • 2012-07-15
  • 1970-01-01
  • 2016-07-21
相关资源
最近更新 更多