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

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

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