Hibernate Validator를 이용한 교차 필드 검증 (JSR 303)


236

Hibernate Validator 4.x에서 필드 간 검증의 구현 (또는 타사 구현)이 있습니까? 그렇지 않은 경우 교차 필드 유효성 검사기를 구현하는 가장 깨끗한 방법은 무엇입니까?

예를 들어, API를 사용하여 두 개의 Bean 특성이 동일한 지 검증하는 방법 (예 : 비밀번호 필드 유효성 검증이 비밀번호 확인 필드와 일치 함)

주석에서 다음과 같은 것을 기대합니다.

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  @Equals(property="pass")
  private String passVerify;
}

1
클래스 수준에서 유형 안전 및 리플렉션 API가없는 (imo 더 우아한) 솔루션 은 stackoverflow.com/questions/2781771/… 을 참조하십시오 .
Karl Richter

답변:


282

각 필드 제약 조건은 고유 한 유효성 검사기 주석으로 처리해야합니다. 즉, 다른 필드에 대해 한 필드의 유효성 검사 주석을 검사하는 것이 좋습니다. 교차 필드 유효성 검사는 클래스 수준에서 수행해야합니다. 또한 JSR-303 섹션 2.2 에서 동일한 유형의 여러 유효성 검증을 선호하는 방법은 주석 목록을 사용하는 것입니다. 이를 통해 일치마다 오류 메시지를 지정할 수 있습니다.

예를 들어, 일반적인 양식의 유효성을 검사하십시오.

@FieldMatch.List({
        @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
        @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationForm  {
    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;

    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

주석 :

package constraints;

import constraints.impl.FieldMatchValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;

/**
 * Validation annotation to validate that 2 fields have the same value.
 * An array of fields and their matching confirmation fields can be supplied.
 *
 * Example, compare 1 pair of fields:
 * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
 * 
 * Example, compare more than 1 pair of fields:
 * @FieldMatch.List({
 *   @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
 *   @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
 */
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
    String message() default "{constraints.fieldmatch}";

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

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

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    /**
     * Defines several <code>@FieldMatch</code> annotations on the same element
     *
     * @see FieldMatch
     */
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
            @interface List
    {
        FieldMatch[] value();
    }
}

유효성 검사기 :

package constraints.impl;

import constraints.FieldMatch;
import org.apache.commons.beanutils.BeanUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation)
    {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context)
    {
        try
        {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);

            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }
        catch (final Exception ignore)
        {
            // ignore
        }
        return true;
    }
}

8
@AndyT : Apache Commons BeanUtils에 외부 종속성이 있습니다.
GaryF

7
@ScriptAssert를 사용하면 사용자 정의 경로로 유효성 검사 메시지를 작성할 수 없습니다. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); 올바른 필드를 강조 할 가능성을 제공합니다 (JSF 만 지원하는 경우).
피터 데이비스

8
위의 샘플을 사용했지만 오류 메시지가 표시되지 않습니다. 바인딩은 jsp에 무엇입니까? 나는 암호를 바인딩하고 확인 만, 다른 것이 필요합니까? <form : password path = "password"/> <form : errors path = "password"cssClass = "errorz"/> <form : password path = "confirmPassword"/> <form : errors path = "confirmPassword"cssClass = " errorz "/>
Mahmoud Saleh

7
BeanUtils.getProperty문자열을 반환합니다. 이 예제는 아마도 PropertyUtils.getProperty객체를 반환하는 사용 을 의미했을 것입니다 .
SingleShot

2
좋은 답변이지만이 질문에 대한 답변으로 완성했습니다 : stackoverflow.com/questions/11890334/…
maxivis

164

다른 해결책을 제안합니다. 아마도 덜 우아하지만 쉬울 것입니다!

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;

  @AssertTrue(message="passVerify field should be equal than pass field")
  private boolean isValid() {
    return this.pass.equals(this.passVerify);
  }
}

isValid방법은 자동 검사기에 의해 호출됩니다.


12
나는 이것이 다시 우려의 혼합이라고 생각합니다. Bean Validation의 핵심은 ConstraintValidators로 검증을 외부화하는 것입니다. 이 경우 Bean 자체에 유효성 검증 로직의 일부가 있고 유효성 검증기 프레임 워크에 일부가 있습니다. 갈 길은 클래스 수준의 제약입니다. Hibernate Validator는 또한 빈 내부 의존성을 쉽게 구현할 수있는 @ScriptAssert를 제공합니다.
Hardy

10
나는 이것이 우아하지 않다고 말할 것입니다 !
NickJ

8
지금까지 내 의견은 Bean Validation JSR이 우려의 혼합이라는 것입니다.
Dmitry

3
@GaneshKrishnan 만약 우리가 그러한 여러 가지 @AssertTrue방법을 원한다면 ? 일부 명명 규칙이 있습니까?
Stephane

3
왜 이것이 최선의 대답이
아닌가

32

나는 이것이 즉시 사용할 수 없다는 것에 놀랐다. 어쨌든, 여기에 가능한 해결책이 있습니다.

원래 질문에서 설명한 필드 수준이 아닌 클래스 수준 유효성 검사기를 만들었습니다.

주석 코드는 다음과 같습니다.

package com.moa.podium.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{com.moa.podium.util.constraints.matches}";

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

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

  String field();

  String verifyField();
}

그리고 유효성 검사기 자체 :

package com.moa.podium.util.constraints;

import org.mvel2.MVEL;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

  private String field;
  private String verifyField;


  public void initialize(Matches constraintAnnotation) {
    this.field = constraintAnnotation.field();
    this.verifyField = constraintAnnotation.verifyField();
  }

  public boolean isValid(Object value, ConstraintValidatorContext context) {
    Object fieldObj = MVEL.getProperty(field, value);
    Object verifyFieldObj = MVEL.getProperty(verifyField, value);

    boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);

    if (neitherSet) {
      return true;
    }

    boolean matches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

    if (!matches) {
      context.disableDefaultConstraintViolation();
      context.buildConstraintViolationWithTemplate("message")
          .addNode(verifyField)
          .addConstraintViolation();
    }

    return matches;
  }
}

MVEL을 사용하여 유효성을 검사하는 객체의 속성을 검사했습니다. 이것은 표준 리플렉션 API로 대체되거나 유효성 검사중인 특정 클래스 인 경우 액세서 메소드 자체입니다.

그런 다음 @Matches 주석을 다음과 같이 Bean에서 사용할 수 있습니다.

@Matches(field="pass", verifyField="passRepeat")
public class AccountCreateForm {

  @Size(min=6, max=50)
  private String pass;
  private String passRepeat;

  ...
}

면책 조항으로, 나는 지난 5 분 동안 이것을 작성 했으므로 아직 모든 버그를 해결하지는 못했을 것입니다. 문제가 발생하면 답변을 업데이트하겠습니다.


1
이것은 addNote가 더 이상 사용되지 않고 addPropertyNode를 대신 사용하면 AbstractMethodError를 얻는 것을 제외하고는 훌륭하고 나를 위해 노력하고 있습니다. Google은 나를 도와주지 않습니다. 어떤 해결책입니까? 어딘가에 의존성이 없는가?
Paul Grenyer

29

최대 절전 모드 검사기 4.1.0.Final I와 함께 사용하는 것이 좋습니다 @ScriptAssert을 . JavaDoc에서 발췌 :

스크립트 표현식은 클래스 경로 에서 JSR 223 ( "JavaTM 플랫폼 용 스크립팅") 호환 엔진을 찾을 수있는 모든 스크립팅 또는 표현식 언어로 작성할 수 있습니다.

참고 : 평가는 일부 의견에 명시된대로 "클라이언트 측"이 아닌 Java "서버 측"에서 실행 되는 스크립팅 " engine "에 의해 수행됩니다 .

예:

@ScriptAssert(lang = "javascript", script = "_this.passVerify.equals(_this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

또는 더 짧은 별칭과 null 안전

@ScriptAssert(lang = "javascript", alias = "_",
    script = "_.passVerify != null && _.passVerify.equals(_.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

또는 Java 7 + null 안전 Objects.equals():

@ScriptAssert(lang = "javascript", script = "Objects.equals(_this.passVerify, _this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

그럼에도 불구하고 사용자 정의 클래스 레벨 유효성 검사기 @Matches 솔루션 에는 아무런 문제가 없습니다 .


1
흥미로운 해결책, 우리는이 유효성 검사를 수행하기 위해 실제로 자바 스크립트를 사용하고 있습니까? 그것은 자바 기반 주석이 달성 할 수 있어야하는 것에 대한 과도한 것처럼 보입니다. 내 처녀 눈으로 위에서 제안한 Nicko의 솔루션은 유용성 관점 (그의 주석은 읽기 쉽고 기능이 뛰어나고 자바 스크립트-> Java 참조에 비해 기능이 뛰어남)과 확장 성 관점 (여기에 합리적인 오버 헤드가 있다고 가정합니다) 자바 스크립트를 처리하지만 Hibernate는 적어도 컴파일 된 코드를 캐싱하고 있습니까?). 왜 이것이 바람직한 지 이해하고 싶습니다.
David Parks

2
Nicko의 구현은 훌륭하지만 JS를 표현 언어로 사용하는 것에 대해 불쾌감을 느끼지 않습니다. Java 6에는 이러한 응용 프로그램을위한 Rhino가 포함되어 있습니다. @ScriptAssert는 새로운 유형의 테스트를 수행 할 때마다 주석과 유효성 검사기를 만들 필요없이 작동하기 때문에 @ScriptAssert를 좋아합니다.

4
말했듯이 클래스 레벨 유효성 검사기에는 아무런 문제가 없습니다. ScriptAssert는 사용자 지정 코드를 작성할 필요가없는 대안입니다. 나는 그것이 선호되는 해결책이라고 말하지 않았다 ;-)
Hardy

비밀번호 확인은 중요한 유효성 검사가 아니기 때문에 큰 대답이므로 클라이언트 측에서 수행 할 수 있습니다
peterchaula

19

사용자 정의 제한 조건을 작성하여 교차 필드 유효성 검증을 수행 할 수 있습니다.

예 :-사용자 인스턴스의 비밀번호 및 confirmPassword 필드를 비교하십시오.

비교 문자열

@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy=CompareStringsValidator.class)
@Documented
public @interface CompareStrings {
    String[] propertyNames();
    StringComparisonMode matchMode() default EQUAL;
    boolean allowNull() default false;
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

문자열 비교 모드

public enum StringComparisonMode {
    EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE
}

문자열 비교기

public class CompareStringsValidator implements ConstraintValidator<CompareStrings, Object> {

    private String[] propertyNames;
    private StringComparisonMode comparisonMode;
    private boolean allowNull;

    @Override
    public void initialize(CompareStrings constraintAnnotation) {
        this.propertyNames = constraintAnnotation.propertyNames();
        this.comparisonMode = constraintAnnotation.matchMode();
        this.allowNull = constraintAnnotation.allowNull();
    }

    @Override
    public boolean isValid(Object target, ConstraintValidatorContext context) {
        boolean isValid = true;
        List<String> propertyValues = new ArrayList<String> (propertyNames.length);
        for(int i=0; i<propertyNames.length; i++) {
            String propertyValue = ConstraintValidatorHelper.getPropertyValue(String.class, propertyNames[i], target);
            if(propertyValue == null) {
                if(!allowNull) {
                    isValid = false;
                    break;
                }
            } else {
                propertyValues.add(propertyValue);
            }
        }

        if(isValid) {
            isValid = ConstraintValidatorHelper.isValid(propertyValues, comparisonMode);
        }

        if (!isValid) {
          /*
           * if custom message was provided, don't touch it, otherwise build the
           * default message
           */
          String message = context.getDefaultConstraintMessageTemplate();
          message = (message.isEmpty()) ?  ConstraintValidatorHelper.resolveMessage(propertyNames, comparisonMode) : message;

          context.disableDefaultConstraintViolation();
          ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(message);
          for (String propertyName : propertyNames) {
            NodeBuilderDefinedContext nbdc = violationBuilder.addNode(propertyName);
            nbdc.addConstraintViolation();
          }
        }    

        return isValid;
    }
}

구속 조건

public abstract class ConstraintValidatorHelper {

public static <T> T getPropertyValue(Class<T> requiredType, String propertyName, Object instance) {
        if(requiredType == null) {
            throw new IllegalArgumentException("Invalid argument. requiredType must NOT be null!");
        }
        if(propertyName == null) {
            throw new IllegalArgumentException("Invalid argument. PropertyName must NOT be null!");
        }
        if(instance == null) {
            throw new IllegalArgumentException("Invalid argument. Object instance must NOT be null!");
        }
        T returnValue = null;
        try {
            PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, instance.getClass());
            Method readMethod = descriptor.getReadMethod();
            if(readMethod == null) {
                throw new IllegalStateException("Property '" + propertyName + "' of " + instance.getClass().getName() + " is NOT readable!");
            }
            if(requiredType.isAssignableFrom(readMethod.getReturnType())) {
                try {
                    Object propertyValue = readMethod.invoke(instance);
                    returnValue = requiredType.cast(propertyValue);
                } catch (Exception e) {
                    e.printStackTrace(); // unable to invoke readMethod
                }
            }
        } catch (IntrospectionException e) {
            throw new IllegalArgumentException("Property '" + propertyName + "' is NOT defined in " + instance.getClass().getName() + "!", e);
        }
        return returnValue; 
    }

    public static boolean isValid(Collection<String> propertyValues, StringComparisonMode comparisonMode) {
        boolean ignoreCase = false;
        switch (comparisonMode) {
        case EQUAL_IGNORE_CASE:
        case NOT_EQUAL_IGNORE_CASE:
            ignoreCase = true;
        }

        List<String> values = new ArrayList<String> (propertyValues.size());
        for(String propertyValue : propertyValues) {
            if(ignoreCase) {
                values.add(propertyValue.toLowerCase());
            } else {
                values.add(propertyValue);
            }
        }

        switch (comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            Set<String> uniqueValues = new HashSet<String> (values);
            return uniqueValues.size() == 1 ? true : false;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            Set<String> allValues = new HashSet<String> (values);
            return allValues.size() == values.size() ? true : false;
        }

        return true;
    }

    public static String resolveMessage(String[] propertyNames, StringComparisonMode comparisonMode) {
        StringBuffer buffer = concatPropertyNames(propertyNames);
        buffer.append(" must");
        switch(comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            buffer.append(" be equal");
            break;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            buffer.append(" not be equal");
            break;
        }
        buffer.append('.');
        return buffer.toString();
    }

    private static StringBuffer concatPropertyNames(String[] propertyNames) {
        //TODO improve concating algorithm
        StringBuffer buffer = new StringBuffer();
        buffer.append('[');
        for(String propertyName : propertyNames) {
            char firstChar = Character.toUpperCase(propertyName.charAt(0));
            buffer.append(firstChar);
            buffer.append(propertyName.substring(1));
            buffer.append(", ");
        }
        buffer.delete(buffer.length()-2, buffer.length());
        buffer.append("]");
        return buffer;
    }
}

사용자

@CompareStrings(propertyNames={"password", "confirmPassword"})
public class User {
    private String password;
    private String confirmPassword;

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getConfirmPassword() { return confirmPassword; }
    public void setConfirmPassword(String confirmPassword) { this.confirmPassword =  confirmPassword; }
}

테스트

    public void test() {
        User user = new User();
        user.setPassword("password");
        user.setConfirmPassword("paSSword");
        Set<ConstraintViolation<User>> violations = beanValidator.validate(user);
        for(ConstraintViolation<User> violation : violations) {
            logger.debug("Message:- " + violation.getMessage());
        }
        Assert.assertEquals(violations.size(), 1);
    }

산출 Message:- [Password, ConfirmPassword] must be equal.

CompareStrings 유효성 검사 제약 조건을 사용하여 둘 이상의 속성을 비교할 수 있으며 4 가지 문자열 비교 방법 중 하나를 혼합 할 수 있습니다.

ColorChoice

@CompareStrings(propertyNames={"color1", "color2", "color3"}, matchMode=StringComparisonMode.NOT_EQUAL, message="Please choose three different colors.")
public class ColorChoice {

    private String color1;
    private String color2;
    private String color3;
        ......
}

테스트

ColorChoice colorChoice = new ColorChoice();
        colorChoice.setColor1("black");
        colorChoice.setColor2("white");
        colorChoice.setColor3("white");
        Set<ConstraintViolation<ColorChoice>> colorChoiceviolations = beanValidator.validate(colorChoice);
        for(ConstraintViolation<ColorChoice> violation : colorChoiceviolations) {
            logger.debug("Message:- " + violation.getMessage());
        }

산출 Message:- Please choose three different colors.

마찬가지로 CompareNumbers, CompareDates 등의 교차 필드 유효성 검사 제약 조건을 가질 수 있습니다.

PS 프로덕션 환경에서는이 코드를 테스트하지 않았지만 (개발 환경에서는 테스트 했음)이 코드를 마일스톤 릴리스로 고려하십시오. 버그를 발견하면 좋은 의견을 적어주십시오. :)


다른 방법보다 융통성이 있기 때문에이 방법이 마음에 듭니다. 2 개 이상의 필드가 동일한 지 유효성을 검사 할 수 있습니다. 좋은 작업!
Tauren

9

Alberthoven의 예제 (hibernate-validator 4.0.2.GA)를 시도했는데 ValidationException이 발생합니다. „주석이있는 메소드는 JavaBeans 명명 규칙을 따라야합니다. match ()는 그렇지 않습니다.” 메소드 이름을 "match"에서 "isValid"로 바꾼 후에 작동합니다.

public class Password {

    private String password;

    private String retypedPassword;

    public Password(String password, String retypedPassword) {
        super();
        this.password = password;
        this.retypedPassword = retypedPassword;
    }

    @AssertTrue(message="password should match retyped password")
    private boolean isValid(){
        if (password == null) {
            return retypedPassword == null;
        } else {
            return password.equals(retypedPassword);
        }
    }

    public String getPassword() {
        return password;
    }

    public String getRetypedPassword() {
        return retypedPassword;
    }

}

그것은 나를 위해 올바르게 작동했지만 오류 메시지를 표시하지 않았습니다. 작동하고 오류 메시지가 표시 되었습니까? 어떻게?
Tiny

1
@Tiny : 메시지가 유효성 검증기에서 리턴 한 위반에 있어야합니다. (단위 테스트 작성 : stackoverflow.com/questions/5704743/… ). 그러나 유효성 검사 메시지는 "isValid"속성에 속합니다. 따라서 GUI에 retypedPassword AND isValid (다시 입력 한 비밀번호 옆)에 문제가있는 경우 GUI에만 메시지가 표시됩니다.
Ralph

8

Spring Framework를 사용하고 있다면 Spring Expression Language (SpEL)를 사용할 수 있습니다. SpEL을 기반으로 JSR-303 유효성 검사기를 제공하는 작은 라이브러리를 작성했습니다. 교차 필드 유효성 검사가 쉬워집니다! https://github.com/jirutka/validator-spring을 살펴보십시오 .

암호 필드의 길이와 동등성을 검증합니다.

@SpELAssert(value = "pass.equals(passVerify)",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}

비밀번호 필드가 비어 있지 않은 경우에만 비밀번호 필드의 유효성을 검사하도록이를 쉽게 수정할 수 있습니다.

@SpELAssert(value = "pass.equals(passVerify)",
            applyIf = "pass || passVerify",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}

4

나는 아이디어를 좋아한다 Jakub Jirutka Spring Expression Language를 사용 . 이미 Spring을 사용한다고 가정 할 때 다른 라이브러리 / 종속성을 추가하지 않으려는 경우 그의 아이디어를 간단히 구현 한 것입니다.

구속 조건 :

@Constraint(validatedBy=ExpressionAssertValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExpressionAssert {
    String message() default "expression must evaluate to true";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String value();
}

유효성 검사기 :

public class ExpressionAssertValidator implements ConstraintValidator<ExpressionAssert, Object> {
    private Expression exp;

    public void initialize(ExpressionAssert annotation) {
        ExpressionParser parser = new SpelExpressionParser();
        exp = parser.parseExpression(annotation.value());
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return exp.getValue(value, Boolean.class);
    }
}

다음과 같이 적용하십시오 :

@ExpressionAssert(value="pass == passVerify", message="passwords must be same")
public class MyBean {
    @Size(min=6, max=50)
    private String pass;
    private String passVerify;
}

3

첫 번째 답변에 대해 언급 한 평판은 없지만 우승 답변에 대한 단위 테스트를 추가하고 다음과 같은 관찰을하고 싶다고 덧붙였습니다.

  • 첫 번째 또는 필드 이름이 잘못되면 값이 일치하지 않는 것처럼 유효성 검사 오류가 발생합니다. 철자 실수로 넘어지지 마십시오. 예 :

@FieldMatch (first = " 잘못된 FieldName1", second = "validFieldName2")

  • 검사기 동등한 데이터 유형을 허용합니다.

private String stringField = "1";

개인 정수 integerField = 새로운 정수 (1)

private int intField = 1;

  • 필드가 equals를 구현하지 않는 객체 유형 인 경우 유효성 검사가 실패합니다.

2

아주 좋은 해결책 bradhouse. @Matches 주석을 둘 이상의 필드에 적용하는 방법이 있습니까?

편집 : 여기이 질문에 대답하기 위해 생각해 낸 해결책이 있습니다. 단일 값 대신 배열을 허용하도록 제약 조건을 수정했습니다.

@Matches(fields={"password", "email"}, verifyFields={"confirmPassword", "confirmEmail"})
public class UserRegistrationForm  {

    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;


    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

주석 코드 :

package springapp.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{springapp.util.constraints.matches}";

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

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

  String[] fields();

  String[] verifyFields();
}

그리고 구현 :

package springapp.util.constraints;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.beanutils.BeanUtils;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

    private String[] fields;
    private String[] verifyFields;

    public void initialize(Matches constraintAnnotation) {
        fields = constraintAnnotation.fields();
        verifyFields = constraintAnnotation.verifyFields();
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {

        boolean matches = true;

        for (int i=0; i<fields.length; i++) {
            Object fieldObj, verifyFieldObj;
            try {
                fieldObj = BeanUtils.getProperty(value, fields[i]);
                verifyFieldObj = BeanUtils.getProperty(value, verifyFields[i]);
            } catch (Exception e) {
                //ignore
                continue;
            }
            boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
            if (neitherSet) {
                continue;
            }

            boolean tempMatches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

            if (!tempMatches) {
                addConstraintViolation(context, fields[i]+ " fields do not match", verifyFields[i]);
            }

            matches = matches?tempMatches:matches;
        }
        return matches;
    }

    private void addConstraintViolation(ConstraintValidatorContext context, String message, String field) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addNode(field).addConstraintViolation();
    }
}

흠. 확실하지 않다. 각 확인 필드에 대해 특정 유효성 검사기를 만들거나 (따라서 서로 다른 주석이 있음) 여러 쌍의 필드를 허용하도록 @Matches 주석을 업데이트 할 수 있습니다.
bradhouse

감사합니다 bradhouse는 솔루션을 생각해 내고 위에 게시했습니다. 다른 수의 인수가 전달 될 때 약간의 노력이 필요하므로 IndexOutOfBoundsExceptions를 얻지 못하지만 기본 사항이 있습니다.
McGin

1

명시 적으로 호출해야합니다. 위의 예에서 bradhouse는 사용자 정의 제한 조건을 작성하는 모든 단계를 제공했습니다.

이 클래스를 호출자 클래스에 추가하십시오.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();

Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);

위의 경우에

Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);


1

당신들은 멋진데. 정말 놀라운 아이디어. 나는 AlberthovenMcGin을 가장 좋아하기 때문에 두 아이디어를 결합하기로 결정했습니다. 모든 경우를 충족시키는 일반적인 솔루션을 개발하십시오. 여기 내가 제안한 해결책이 있습니다.

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


    String message() default "NotFalse";
    String[] messages();
    String[] properties();
    String[] verifiers();

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

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

}

public class NotFalseValidator implements ConstraintValidator<NotFalse, Object> {
    private String[] properties;
    private String[] messages;
    private String[] verifiers;
    @Override
    public void initialize(NotFalse flag) {
        properties = flag.properties();
        messages = flag.messages();
        verifiers = flag.verifiers();
    }

    @Override
    public boolean isValid(Object bean, ConstraintValidatorContext cxt) {
        if(bean == null) {
            return true;
        }

        boolean valid = true;
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);

        for(int i = 0; i< properties.length; i++) {
           Boolean verified = (Boolean) beanWrapper.getPropertyValue(verifiers[i]);
           valid &= isValidProperty(verified,messages[i],properties[i],cxt);
        }

        return valid;
    }

    boolean isValidProperty(Boolean flag,String message, String property, ConstraintValidatorContext cxt) {
        if(flag == null || flag) {
            return true;
        } else {
            cxt.disableDefaultConstraintViolation();
            cxt.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(property)
                    .addConstraintViolation();
            return false;
        }

    }



}

@NotFalse(
        messages = {"End Date Before Start Date" , "Start Date Before End Date" } ,
        properties={"endDateTime" , "startDateTime"},
        verifiers = {"validDateRange" , "validDateRange"})
public class SyncSessionDTO implements ControllableNode {
    @NotEmpty @NotPastDate
    private Date startDateTime;

    @NotEmpty
    private Date endDateTime;



    public Date getStartDateTime() {
        return startDateTime;
    }

    public void setStartDateTime(Date startDateTime) {
        this.startDateTime = startDateTime;
    }

    public Date getEndDateTime() {
        return endDateTime;
    }

    public void setEndDateTime(Date endDateTime) {
        this.endDateTime = endDateTime;
    }


    public Boolean getValidDateRange(){
        if(startDateTime != null && endDateTime != null) {
            return startDateTime.getTime() <= endDateTime.getTime();
        }

        return null;
    }

}

0

나는 Nicko의 솔루션을 약간 조정하여 Apache Commons BeanUtils 라이브러리를 사용하고 봄에 이미 사용 가능한 솔루션으로 대체 할 필요가 없도록 간단하게 만들었습니다.

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object object, final ConstraintValidatorContext context) {

        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
        final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
        final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);

        boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);

        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                .addPropertyNode(firstFieldName)
                .addConstraintViolation();
        }

        return isValid;

    }
}

-1

질문으로 실현 된 해결책 : 주석 속성에 설명 된 필드에 액세스하는 방법

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Match {

    String field();

    String message() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MatchValidator.class)
@Documented
public @interface EnableMatchConstraint {

    String message() default "Fields must match!";

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

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

public class MatchValidator implements  ConstraintValidator<EnableMatchConstraint, Object> {

    @Override
    public void initialize(final EnableMatchConstraint constraint) {}

    @Override
    public boolean isValid(final Object o, final ConstraintValidatorContext context) {
        boolean result = true;
        try {
            String mainField, secondField, message;
            Object firstObj, secondObj;

            final Class<?> clazz = o.getClass();
            final Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                if (field.isAnnotationPresent(Match.class)) {
                    mainField = field.getName();
                    secondField = field.getAnnotation(Match.class).field();
                    message = field.getAnnotation(Match.class).message();

                    if (message == null || "".equals(message))
                        message = "Fields " + mainField + " and " + secondField + " must match!";

                    firstObj = BeanUtils.getProperty(o, mainField);
                    secondObj = BeanUtils.getProperty(o, secondField);

                    result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
                    if (!result) {
                        context.disableDefaultConstraintViolation();
                        context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
                        break;
                    }
                }
            }
        } catch (final Exception e) {
            // ignore
            //e.printStackTrace();
        }
        return result;
    }
}

그리고 그것을 사용하는 방법 ...? 이처럼 :

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.