Xác thực JSR 303, Nếu một trường bằng “cái gì đó”, thì các trường khác này không được rỗng


89

Tôi đang tìm cách xác thực tùy chỉnh nhỏ với JSR-303 javax.validation.

Tôi có một cánh đồng. Và Nếu một giá trị nhất định được nhập vào trường này, tôi muốn yêu cầu một vài trường khác không được nhập null.

Tôi đang cố gắng tìm ra điều này. Không chắc chính xác tôi sẽ gọi điều này là gì để giúp tìm ra lời giải thích.

Bất kỳ trợ giúp sẽ được đánh giá cao. Tôi khá mới mẻ với điều này.

Hiện tại, tôi đang nghĩ đến một Ràng buộc Tùy chỉnh. Nhưng tôi không chắc chắn về cách kiểm tra giá trị của trường phụ thuộc từ bên trong chú thích. Về cơ bản, tôi không chắc cách truy cập đối tượng bảng điều khiển từ chú thích.

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

Đó là panel.status.getValue();rắc rối cho tôi .. không biết làm thế nào để hoàn thành việc này.

Câu trả lời:


106

Trong trường hợp này, tôi khuyên bạn nên viết trình xác thực tùy chỉnh, trình xác thực này sẽ xác thực ở cấp lớp (để cho phép chúng tôi truy cập vào các trường của đối tượng) rằng một trường chỉ được yêu cầu nếu trường khác có giá trị cụ thể. Lưu ý rằng bạn nên viết trình xác thực chung có 2 tên trường và chỉ hoạt động với 2 trường này. Để yêu cầu nhiều trường, bạn nên thêm trình xác thực này cho từng trường.

Sử dụng đoạn mã sau làm ý tưởng (Tôi chưa thử nghiệm nó).

  • Giao diện trình xác thực

    /**
     * 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();
        }
    
    }
    
  • Triển khai trình xác thực

    /**
     * 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;
        }
    
    }
    
  • Ví dụ sử dụng trình xác thực (hibernate-validator> = 6 với 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
    }
    
  • Ví dụ sử dụng trình xác thực (hibernate-validator <6; ví dụ cũ)

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

Lưu ý rằng việc triển khai trình xác thực sử dụng BeanUtilslớp từ commons-beanutilsthư viện nhưng bạn cũng có thể sử dụng BeanWrapperImpltừ Spring Framework .

Xem thêm câu trả lời tuyệt vời này: Xác thực trường chéo với Trình xác thực Hibernate (JSR 303)


1
@Benedictus Ví dụ này sẽ chỉ hoạt động với chuỗi nhưng bạn có thể sửa đổi nó để hoạt động với bất kỳ đối tượng nào. Có 2 cách: 1) tham số xác nhận hợp lệ với lớp mà bạn muốn xác thực (thay vì Object). Trong trường hợp này, bạn thậm chí không cần sử dụng phản chiếu để nhận các giá trị nhưng trong trường hợp này trình xác thực trở nên ít chung chung hơn 2) sử dụng BeanWrapperImptừ Spring Framework (hoặc các thư viện khác) và getPropertyValue()phương thức của nó . Trong trường hợp này, bạn sẽ có thể nhận được một giá trị Objectvà chuyển thành bất kỳ kiểu nào bạn cần.
Slava Semushin

Vâng, nhưng bạn không thể có Đối tượng dưới dạng tham số chú thích, Vì vậy, bạn sẽ cần một loạt các chú thích khác nhau cho mỗi loại bạn muốn xác thực.
Ben

1
Vâng, đó là ý tôi khi tôi nói "trong trường hợp này trình xác nhận trở nên ít chung chung hơn".
Slava Semushin

Tôi muốn sử dụng thủ thuật này cho các lớp bộ đệm protoBuffer. điều này rất hữu ích (:
Saeed

Giải pháp tốt. Rất hữu ích để tạo chú thích tùy chỉnh!
Vishwa

126

Xác định phương thức phải xác thực thành true và đặt @AssertTruechú thích lên đầu nó:

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

Phương thức phải bắt đầu bằng 'is'.


Tôi đã sử dụng phương pháp của bạn và nó hoạt động, nhưng tôi không thể tìm ra cách nhận được tin nhắn. Bạn có tình cờ biết không?
anaBad

12
Đây là lựa chọn hiệu quả nhất cho đến nay. Cảm ơn! @anaBad: Chú thích AssertTrue có thể nhận một thông báo tùy chỉnh, giống như các chú thích ràng buộc khác.
ernest_k

@ErnestKiwele Cảm ơn bạn đã trả lời, nhưng vấn đề của tôi không phải là đặt tin nhắn mà là đưa nó vào jsp của mình. Tôi có chức năng sau trong mô hình: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } Và điều này trong jsp của tôi: <th><form:label path="reference"><s:message code="reference"/></form:label></th><td><form:input path="reference" cssErrorClass="errorField"/><br/><form:errors path="isReferenceOk" cssClass="error"/></td> Nhưng nó gây ra lỗi.
anaBad

@ErnestKiwele Đừng bận tâm, tôi đã tìm ra, tôi đã tạo một thuộc tính boolean được đặt khi setReference () được gọi.
anaBad

2
tôi đã phải thực hiện phương pháp công cộng
Tibi

20

Bạn nên sử dụng tùy chỉnh DefaultGroupSequenceProvider<T>:

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
}

Xem thêm câu hỏi liên quan về chủ đề này .


Cách làm thú vị. Câu trả lời có thể làm với giải thích thêm về cách hoạt động, tuy nhiên, vì tôi đã phải đọc nó hai lần trước khi tôi thấy những gì đang xảy ra ...
Jules

Xin chào, tôi đã triển khai giải pháp của bạn nhưng gặp sự cố. Không có đối tượng nào được chuyển cho getValidationGroups(MyCustomForm myCustomForm)phương thức. Bạn có thể giúp đỡ ở đây? : stackoverflow.com/questions/44520306/…
user238607

2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) sẽ gọi nhiều thời gian cho mỗi cá thể bean và nó sẽ trôi qua một thời gian rỗng. Bạn chỉ bỏ qua nếu nó vượt qua null.
pramoth

7

Đây là công việc của tôi, cố gắng giữ cho nó càng đơn giản càng tốt.

Giao diện:

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

Triển khai xác thực:

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

}

Sử dụng:

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

    private StateType stateType;

    private ModeType modeType;

}

Tin nhắn:

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}

3

Một cách tiếp cận khác sẽ là tạo một getter (được bảo vệ) trả về một đối tượng chứa tất cả các trường phụ thuộc. Thí dụ:

public class MyBean {
  protected String status;
  protected String name;

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

StatusAndSomethingValidator hiện có thể truy cập StatusAndSomething.status và StatusAndSomething.something và thực hiện kiểm tra phụ thuộc.


0

Mẫu bên dưới:

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";
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.