JSR 303 验证,如果一个字段等于“某物”,则这些其他字段不应为空

Posted

技术标签:

【中文标题】JSR 303 验证,如果一个字段等于“某物”,则这些其他字段不应为空【英文标题】:JSR 303 Validation, If one field equals "something", then these other fields should not be null 【发布时间】:2012-03-06 06:25:03 【问题描述】:

我希望使用 JSR-303 javax.validation 进行一些自定义验证。

我有一个领域。如果在这个字段中输入了某个值,我想要求其他几个字段不是null

我正在努力解决这个问题。不确定我会称之为什么来帮助找到解释。

任何帮助将不胜感激。我对此很陌生。

目前我正在考虑自定义约束。但我不确定如何从注释中测试依赖字段的值。基本上我不确定如何从注释中访问面板对象。

public class StatusValidator implements ConstraintValidator<NotNull, String> 

    @Override
    public void initialize(NotNull constraintAnnotation) 

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) 
        if ("Canceled".equals(panel.status.getValue())) 
            if (value != null) 
                return true;
            
         else 
            return false;
        
    

这是panel.status.getValue(); 给我带来了麻烦.. 不知道如何做到这一点。

【问题讨论】:

【参考方案1】:

示例如下:

package io.quee.sample.javax;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.Pattern;
import java.util.Set;

/**
 * Created By [**Ibrahim Al-Tamimi **](https://www.linkedin.com/in/iloom/)
 * Created At **Wednesday **23**, September 2020**
 */
@SpringBootApplication
public class SampleJavaXValidation implements CommandLineRunner 
    private final Validator validator;

    public SampleJavaXValidation(Validator validator) 
        this.validator = validator;
    

    public static void main(String[] args) 
        SpringApplication.run(SampleJavaXValidation.class, args);
    

    @Override
    public void run(String... args) throws Exception 
        Set<ConstraintViolation<SampleDataCls>> validate = validator.validate(new SampleDataCls(SampleTypes.TYPE_A, null, null));
        System.out.println(validate);
    

    public enum SampleTypes 
        TYPE_A,
        TYPE_B;
    

    @Valid
    public static class SampleDataCls 
        private final SampleTypes type;
        private final String valueA;
        private final String valueB;

        public SampleDataCls(SampleTypes type, String valueA, String valueB) 
            this.type = type;
            this.valueA = valueA;
            this.valueB = valueB;
        

        public SampleTypes getType() 
            return type;
        

        public String getValueA() 
            return valueA;
        

        public String getValueB() 
            return valueB;
        

        @Pattern(regexp = "TRUE")
        public String getConditionalValueA() 
            if (type.equals(SampleTypes.TYPE_A)) 
                return valueA != null ? "TRUE" : "";
            
            return "TRUE";
        

        @Pattern(regexp = "TRUE")
        public String getConditionalValueB() 
            if (type.equals(SampleTypes.TYPE_B)) 
                return valueB != null ? "TRUE" : "";
            
            return "TRUE";
        
    

【讨论】:

【参考方案2】:

在这种情况下,我建议编写一个自定义验证器,它将在类级别验证(以允许我们访问对象的字段)只有当另一个字段具有特定值时才需要一个字段。请注意,您应该编写获取 2 个字段名称并仅使用这 2 个字段的通用验证器。要要求多个字段,您应该为每个字段添加此验证器。

使用下面的代码作为一个想法(我没有测试过)。

验证器接口

/**
 * Validates that field @code dependFieldName is not null if
 * field @code fieldName has value @code fieldValue.
 **/
@Target(TYPE, ANNOTATION_TYPE)
@Retention(RUNTIME)
@Repeatable(NotNullIfAnotherFieldHasValue.List.class) // only with hibernate-validator >= 6.x
@Constraint(validatedBy = NotNullIfAnotherFieldHasValueValidator.class)
@Documented
public @interface NotNullIfAnotherFieldHasValue 

    String fieldName();
    String fieldValue();
    String dependFieldName();

    String message() default "NotNullIfAnotherFieldHasValue.message";
    Class<?>[] groups() default ;
    Class<? extends Payload>[] payload() default ;

    @Target(TYPE, ANNOTATION_TYPE)
    @Retention(RUNTIME)
    @Documented
    @interface List 
        NotNullIfAnotherFieldHasValue[] value();
    


验证器实现

/**
 * Implementation of @link NotNullIfAnotherFieldHasValue validator.
 **/
public class NotNullIfAnotherFieldHasValueValidator
    implements ConstraintValidator<NotNullIfAnotherFieldHasValue, Object> 

    private String fieldName;
    private String expectedFieldValue;
    private String dependFieldName;

    @Override
    public void initialize(NotNullIfAnotherFieldHasValue annotation) 
        fieldName          = annotation.fieldName();
        expectedFieldValue = annotation.fieldValue();
        dependFieldName    = annotation.dependFieldName();
    

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext ctx) 

        if (value == null) 
            return true;
        

        try 
            String fieldValue       = BeanUtils.getProperty(value, fieldName);
            String dependFieldValue = BeanUtils.getProperty(value, dependFieldName);

            if (expectedFieldValue.equals(fieldValue) && dependFieldValue == null) 
                ctx.disableDefaultConstraintViolation();
                ctx.buildConstraintViolationWithTemplate(ctx.getDefaultConstraintMessageTemplate())
                    .addNode(dependFieldName)
                    .addConstraintViolation();
                    return false;
            

         catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) 
            throw new RuntimeException(ex);
        

        return true;
    


验证器使用示例(hibernate-validator >= 6 with Java 8+)

@NotNullIfAnotherFieldHasValue(
    fieldName = "status",
    fieldValue = "Canceled",
    dependFieldName = "fieldOne")
@NotNullIfAnotherFieldHasValue(
    fieldName = "status",
    fieldValue = "Canceled",
    dependFieldName = "fieldTwo")
public class SampleBean 
    private String status;
    private String fieldOne;
    private String fieldTwo;

    // getters and setters omitted

验证器使用示例(hibernate-validator

@NotNullIfAnotherFieldHasValue.List(
    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldOne"),
    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldTwo")
)
public class SampleBean 
    private String status;
    private String fieldOne;
    private String fieldTwo;

    // getters and setters omitted

请注意,验证器实现使用 commons-beanutils 库中的 BeanUtils 类,但您也可以使用 BeanWrapperImpl from Spring Framework。

另请参阅这个很棒的答案:Cross field validation with Hibernate Validator (JSR 303)

【讨论】:

@Benedictus 这个示例只适用于字符串,但您可以修改它以适用于任何对象。有 2 种方法:1) 使用要验证的类参数化验证器(而不是 Object)。在这种情况下,您甚至不需要使用反射来获取值,但在这种情况下验证器变得不那么通用 2)使用来自 Spring Framework(或其他库)的BeanWrapperImp 及其getPropertyValue() 方法。在这种情况下,您将能够获得 Object 的值并转换为您需要的任何类型。 是的,但是你不能将 Object 作为注解参数,所以你需要为你想要验证的每种类型设置一堆不同的注解。 是的,这就是我所说的“在这种情况下验证器变得不那么通用”的意思。 我想将这个技巧用于 protoBuffer 类。这很有帮助(: 不错的解决方案。对构建自定义注释非常有帮助!【参考方案3】:

这是我的看法,尽量保持简单。

界面:

@Target(TYPE, ANNOTATION_TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = OneOfValidator.class)
@Documented
public @interface OneOf 

    String message() default "one.of.message";

    Class<?>[] groups() default ;

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

    String[] value();

验证实现:

public class OneOfValidator implements ConstraintValidator<OneOf, Object> 

    private String[] fields;

    @Override
    public void initialize(OneOf annotation) 
        this.fields = annotation.value();
    

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) 

        BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);

        int matches = countNumberOfMatches(wrapper);

        if (matches > 1) 
            setValidationErrorMessage(context, "one.of.too.many.matches.message");
            return false;
         else if (matches == 0) 
            setValidationErrorMessage(context, "one.of.no.matches.message");
            return false;
        

        return true;
    

    private int countNumberOfMatches(BeanWrapper wrapper) 
        int matches = 0;
        for (String field : fields) 
            Object value = wrapper.getPropertyValue(field);
            boolean isPresent = detectOptionalValue(value);

            if (value != null && isPresent) 
                matches++;
            
        
        return matches;
    

    private boolean detectOptionalValue(Object value) 
        if (value instanceof Optional) 
            return ((Optional) value).isPresent();
        
        return true;
    

    private void setValidationErrorMessage(ConstraintValidatorContext context, String template) 
        context.disableDefaultConstraintViolation();
        context
            .buildConstraintViolationWithTemplate("" + template + "")
            .addConstraintViolation();
    


用法:

@OneOf("stateType", "modeType")
public class OneOfValidatorTestClass 

    private StateType stateType;

    private ModeType modeType;


消息:

one.of.too.many.matches.message=Only one of the following fields can be specified: value
one.of.no.matches.message=Exactly one of the following fields must be specified: value

【讨论】:

【参考方案4】:

定义必须验证为真的方法并将@AssertTrue注解放在其顶部:

  @AssertTrue
  private boolean isOk() 
    return someField != something || otherField != null;
  

方法必须以'is'开头。

【讨论】:

我使用了你的方法并且它有效,但我不知道如何获取消息。你碰巧知道吗? 这是迄今为止最有效的选择。谢谢! @anaBad:AssertTrue 注释可以接受自定义消息,就像其他约束注释一样。 @ErnestKiwele 感谢您的回答,但我的问题不在于设置消息,而是在我的 jsp 中获取它。我的模型具有以下功能:@AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); 而这在我的 jsp 中:&lt;th&gt;&lt;form:label path="reference"&gt;&lt;s:message code="reference"/&gt;&lt;/form:label&gt;&lt;/th&gt;&lt;td&gt;&lt;form:input path="reference" cssErrorClass="errorField"/&gt;&lt;br/&gt;&lt;form:errors path="isReferenceOk" cssClass="error"/&gt;&lt;/td&gt; 但它会引发错误。 @ErnestKiwele 没关系,我想通了,我创建了一个布尔属性,在调用 setReference() 时设置。 我必须公开该方法【参考方案5】:

你应该使用自定义DefaultGroupSequenceProvider&lt;T&gt;:

ConditionalValidation.java

// Marker interface
public interface ConditionalValidation 

MyCustomFormSequenceProvider.java

public class MyCustomFormSequenceProvider
    implements DefaultGroupSequenceProvider<MyCustomForm> 

    @Override
    public List<Class<?>> getValidationGroups(MyCustomForm myCustomForm) 

        List<Class<?>> sequence = new ArrayList<>();

        // Apply all validation rules from ConditionalValidation group
        // only if someField has given value
        if ("some value".equals(myCustomForm.getSomeField())) 
            sequence.add(ConditionalValidation.class);
        

        // Apply all validation rules from default group
        sequence.add(MyCustomForm.class);

        return sequence;
    

MyCustomForm.java

@GroupSequenceProvider(MyCustomFormSequenceProvider.class)
public class MyCustomForm 

    private String someField;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldTwo;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldThree;

    @NotEmpty
    private String fieldAlwaysValidated;


    // getters, setters omitted

另见related question on this topic。

【讨论】:

有趣的做法。不过,答案可能需要更多地解释它是如何工作的,因为在我看到发生了什么之前我必须阅读它两次...... 您好,我实施了您的解决方案,但遇到了问题。没有对象被传递给getValidationGroups(MyCustomForm myCustomForm) 方法。你能在这里帮忙吗? :***.com/questions/44520306/… @user238607 getValidationGroups(MyCustomForm myCustomForm) 每个 bean 实例都会调用很多次,并且有时会传递 null。如果它通过 null,你就忽略它。【参考方案6】:

另一种方法是创建一个(受保护的)getter,它返回一个包含所有相关字段的对象。示例:

public class MyBean 
  protected String status;
  protected String name;

  @StatusAndSomethingValidator
  protected StatusAndSomething getStatusAndName() 
    return new StatusAndSomething(status,name);
  

StatusAndSomethingValidator 现在可以访问 StatusAndSomething.status 和 StatusAndSomething.something 并进行相关检查。

【讨论】:

以上是关于JSR 303 验证,如果一个字段等于“某物”,则这些其他字段不应为空的主要内容,如果未能解决你的问题,请参考以下文章

使用 JSR 303 在 JSF 中使用内联消息进行跨字段验证

Java Hibernate Validator JSR-303验证

Java Hibernate Validator JSR-303验证

JSR-303

JSR 303 Bean 验证 - 为啥使用 getter 而不是 setter?

JSR 303 Bean 验证 - 为啥使用 getter 而不是 setter?