Проверка перекрестных полей с помощью Hibernate Validator (JSR 303)

Есть ли реализация (или сторонняя реализация) проверки перекрестных полей в Hibernate Validator 4.x? Если нет, то как лучше всего реализовать валидатор перекрестных полей?

В качестве примера, как вы можете использовать API для проверки равенства двух свойств bean (например, проверка того, что поле пароля совпадает с полем проверки пароля).

В аннотациях я ожидал чего-то вроде:

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

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

person Bradley Dwyer    schedule 29.12.2009    source источник
comment
См. stackoverflow.com/questions/2781771/ для безопасного по типу и без отражения API-решения (я думаю, более элегантного) на уровне класса.   -  person Karl Richter    schedule 26.08.2015


Ответы (15)


Каждое ограничение поля должно обрабатываться отдельной аннотацией валидатора, или, другими словами, не рекомендуется проводить проверку аннотации валидации одного поля по сравнению с другими полями; перекрестная проверка должна выполняться на уровне класса. Кроме того, рекомендуется использовать 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;
    }
}
person Nicko    schedule 28.01.2010
comment
Хорошая реализация без внешних зависимостей. - person AndyT; 27.05.2010
comment
@AndyT: существует внешняя зависимость от Apache Commons BeanUtils. - person GaryF; 11.08.2010
comment
Чистая реализация, но, вероятно, для большинства целей @ScriptAssert намного удобнее. Приятно иметь возможность определять произвольные ограничения без необходимости каждый раз создавать соответствующие аннотации и валидаторы. - person ; 06.04.2011
comment
@ScriptAssert не позволяет создавать сообщение проверки с настраиваемым путем. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Дает возможность выделить правое поле (если только JSF поддерживает это). - person Peter Davis; 13.04.2011
comment
Я использовал приведенный выше образец, но он не отображает сообщение об ошибке, какая привязка должна быть в jsp? у меня привязка только для пароля и подтверждения, еще что-нибудь нужно? ‹Форма: путь к паролю = пароль /› ‹форма: путь ошибок = пароль cssClass = errorz /› ‹форма: путь к паролю = confirmPassword /› ‹форма: путь ошибок = confirmPassword cssClass = errorz /› - person Mahmoud Saleh; 12.09.2011
comment
BeanUtils.getProperty возвращает строку. Пример, вероятно, предназначался для использования PropertyUtils.getProperty, который возвращает объект. - person SingleShot; 13.12.2011
comment
Могу ли я использовать этот код в коммерческой работе? Кому бы я приписал? - person Wes; 06.12.2012
comment
Мне понравилось это решение. Единственный недостаток - когда я проверяю ошибки внутри своего контроллера. Когда я получаю result.getFieldErrors (), тех, которые подняты аннотацией, нет. Есть ли простой способ превратить его в FieldError? - person Paulo Pedroso; 02.05.2013
comment
хороший подход, но он не определяет код должным образом. В приведенном ниже ответе Брэдхауса показано, как добавить код ошибки (магия заключается в блоке if (! Match)). - person Cyril Deba; 28.07.2013
comment
Хороший ответ, но я завершил его ответом на этот вопрос: stackoverflow.com/questions/11890334/ - person maxivis; 24.09.2013
comment
Большое спасибо за это, это было очень полезно для моего проекта - person TheYann; 23.03.2014
comment
Можно использовать BeanWrapperImpl из Spring вместо BeanUtils из Apache. - person pasemes; 07.08.2014
comment
Вероятно, это должно работать для настраиваемого пути: constraintValidatorContext.disableDefaultConstraintViolation (); constraintValidatorContext .buildConstraintViolationWithTemplate (constraintValidatorContext.getDefaultConstraintMessageTemplate ()) .addPropertyNode (secondFieldName) .addConstraintViolation (); - person Krishna; 19.09.2014
comment
в последней версии BeanUtils есть метод getProperty, который возвращает Object - person gstackoverflow; 19.04.2015
comment
Есть ли способ использовать Spring beans в валидаторе? stackoverflow.com/questions/40584985/ - person dmitryvim; 14.11.2016
comment
Вы можете использовать этот метод, если не хотите использовать внешнюю библиотеку Apache BeanUtils: частный объект getValue (объект объекта, String fieldName) {попробуйте {Field field = object.getClass (). GetDeclaredField (fieldName); field.setAccessible (правда); Значение объекта = field.get (объект); возвращаемое значение; } catch (исключение e) {e.printStackTrace (); return null; }} - person Mehdi; 16.11.2017
comment
Если кто-то попадает сюда с помощью поиска, вы можете использовать это напрямую как зависимость: org.apache.struts:struts2-bean-validation-plugin - person Sutra; 10.03.2019
comment
Я сделал StringIndexOutOfBoundsException и удовлетворил это stackoverflow.com/questions/11890334/ - person user3480687; 08.12.2020

Предлагаю вам другое возможное решение. Возможно, менее элегантно, но проще!

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 вызывается валидатором автоматически.

person Alberthoven    schedule 20.01.2010
comment
Интересно. Это, безусловно, позволяет обойти сложность правильного связывания ошибок уровня поля, когда вы используете валидатор уровня класса. Меня беспокоит только i18n. Для данного «типа» ошибки проверки у меня есть общие сообщения для этих типов. Если бы в вашем приложении был только один @AssertTrue, это было бы хорошо, но в остальном я не уверен, что это сработает (в общем смысле с i18n). - person Bradley Dwyer; 20.01.2010
comment
Разве нельзя поместить код сообщения в атрибут сообщения, чтобы он проходил через обычный валидатор MessageInterpolator? - person GaryF; 25.03.2010
comment
Я думаю, что это снова смешение опасений. Вся суть Bean Validation состоит в том, чтобы перенести валидацию в ConstraintValidators. В этом случае у вас есть часть логики проверки в самом bean-компоненте, а часть - в структуре Validator. Путь - это ограничение на уровне класса. Hibernate Validator теперь также предлагает @ScriptAssert, который упрощает реализацию внутренних зависимостей bean-компонентов. - person Hardy; 18.05.2010
comment
match () больше не является допустимым соглашением об именах для аннотаций Spring. Пожалуйста, используйте соглашения об именах java bean-компонентов, такие как isValid () - person Ganesh Krishnan; 06.11.2013
comment
Я бы сказал, что это более элегантно, не менее! - person NickJ; 21.02.2014
comment
Один - недостаток - это не декларативно - более программно. Я не могу экстернализировать это ... скажем, в jsonSchema с проверкой - person bhantol; 06.08.2014
comment
С более теоретической точки зрения я думаю, что Харди прав (так что +1). На самом деле я использую этот метод постоянно. С прагматической точки зрения это намного проще, чем создание ограничения на уровне класса для каждого конкретного варианта использования (то есть +1 к Альбертховену). - person Henno Vermeulen; 13.01.2015
comment
Я не тестировал его, но, по крайней мере, с Hibernate Validator, I18N можно сделать, используя ключ пакета ресурсов ValidationMessage в фигурных скобках. - person Henno Vermeulen; 13.01.2015
comment
На данный момент я считаю, что JSR Bean Validation представляет собой смесь проблем. - person Dmitry Minkovsky; 21.07.2015
comment
@GaneshKrishnan Что, если мы хотим иметь несколько таких @AssertTrue-ed методов? Есть какое-то соглашение об именах? - person Stephane; 20.11.2015
comment
@Alberthoven Как показать сообщение об ошибке, не относящееся к какому-либо полю. - person Zahid Khan; 24.03.2016
comment
почему это не лучший ответ - person funky-nd; 04.08.2016
comment
Когда я использую @AssertTrue, мой метод вызывается несколько раз ... Кто-нибудь может мне помочь? ссылка - person Henrique Fernandes Cipriano; 01.09.2017
comment
@Hardy Хотя некоторые могут рассматривать это как смешение проблем, можно утверждать, что JSR 303 поощряет нарушение более фундаментального принципа инкапсуляции. Почему объект домена не должен знать, как проверить свое собственное частное состояние? Я думаю, что преобладающая в настоящее время точка зрения является лишь отражением того, в какой степени JPA и Hibernate способствовали широкому распространению антипаттерна Anemic Domain. - person Dave; 08.01.2018
comment
Это лучший ответ. Честно говоря, я думаю, что декларативная проверка значительно усложняет задачу. Достаточно легко проверить отдельные свойства в установщике, а проверка перекрестных свойств может быть выполнена с помощью одного метода validate (). - person Greg Brown; 16.08.2018
comment
@bhantol - вы также не можете экстернализовать логику, хранящуюся в настраиваемом валидаторе. - person Greg Brown; 16.08.2018
comment
@GregBrown - это не логика, а декларативные свойства, аннотированные, могут быть выполнены с помощью настраиваемого валидатора. Вот старый способ github.com/yogeshgadge/crossfield-bean-validator/blob/master/, где описано поле, от которого OtherField оно зависит, но оно может быть более наглядным, и вся логика может быть описанным. - person bhantol; 16.08.2018
comment
github.com/FasterXML/jackson-module-jsonSchema может экспортировать JSON, который некоторые другие платформа (например, Javascript) может интерпретировать и применять логику. Но вы правы, сама логика не может быть экстернализована. - person bhantol; 16.08.2018
comment
@Stephane Я кодирую все подобные проверки между полями разными методами, чтобы получать соответствующие сообщения об ошибках. Все они должны начинаться с is, иначе платформа их проигнорирует. Фактическое имя имеет значение только для вас, поскольку методы лучше оставить как private. Например: @AssertTrue(message="The two email fields should be the same") private boolean isEmailVerifyValid() { return this.email.equals(this.emailVerify); } и @AssertTrue(message="The two password fields should be the same") private boolean isPassVerifyValid() { return this.pass.equals(this.passVerify); }. - person Tobia; 07.04.2019
comment
@ funky-nd отсутствуют реальные поля и соответствующие значения - person yongfa365; 12.11.2019
comment
Имя метода должно быть isFieldName, где FieldName соответствует проверяемому полю. Например. если у вас есть поле firstName, вы должны проверить его с помощью @AssertTrue private boolean isFirstName() { ... } - это позволяет Spring сопоставить результат проверки с полем, к которому он относится. - person Josh M.; 11.06.2020

Я удивлен, что это недоступно из коробки. В любом случае, вот возможное решение.

Я создал валидатор на уровне класса, а не на уровне поля, как описано в исходном вопросе.

Вот код аннотации:

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 можно использовать в компоненте следующим образом:

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

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

  ...
}

В качестве отказа от ответственности я написал это за последние 5 минут, поэтому я, вероятно, еще не устранил все ошибки. Я обновлю ответ, если что-то пойдет не так.

person Bradley Dwyer    schedule 10.01.2010
comment
Это здорово, и это работает для меня, за исключением того, что addNote устарел, и я получаю AbstractMethodError, если вместо этого использую addPropertyNode. Гугл мне здесь не помогает. Какое решение? Где-то отсутствует зависимость? - person Paul Grenyer; 01.12.2013

В Hibernate Validator 4.1.0.Final я рекомендую использовать @ ScriptAssert. Исключение из своего JavaDoc:

Выражения сценария могут быть написаны на любом языке сценариев или выражений, для которого существует JSR 223 («Сценарии для платформы JavaTM») совместимый механизм можно найти в пути к классам.

Примечание: оценка выполняется «движком» сценариев, работающим в виртуальной машине Java, поэтому на «стороне сервера» Java, не на «стороне клиента», как указано в некоторые комментарии.

Пример:

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

  private String passVerify;
}

или с более коротким псевдонимом и нулевым значением:

@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+ нулевой безопасностью 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 пользовательского валидатора уровня класса нет ничего плохого.

person Hardy    schedule 08.11.2010
comment
Интересное решение, действительно ли мы используем здесь javascript для выполнения этой проверки? Это кажется излишним для того, что должна выполнять аннотация на основе java. На мой взгляд, решение Нико, предложенное выше, все еще кажется более чистым как с точки зрения удобства использования (его аннотация легко читается и довольно функциональна по сравнению с неэлегантными ссылками на javascript- ›java), так и с точки зрения масштабируемости (я предполагаю, что есть разумные накладные расходы на обрабатывать javascript, но, может быть, Hibernate, по крайней мере, кеширует скомпилированный код?). Мне любопытно понять, почему это предпочтительнее. - person David Parks; 08.11.2010
comment
Я согласен с тем, что реализация Nicko хороша, но я не вижу ничего предосудительного в использовании JS в качестве языка выражений. Java 6 включает Rhino именно для таких приложений. Мне нравится @ScriptAssert, потому что он просто работает без необходимости создавать аннотацию и валидатор каждый раз, когда мне нужно выполнить новый тип теста. - person ; 06.04.2011
comment
Как уже было сказано, с валидатором уровня класса все в порядке. ScriptAssert - это просто альтернатива, которая не требует написания специального кода. Я не говорил, что это предпочтительное решение ;-) - person Hardy; 06.04.2011
comment
Отличный ответ, потому что подтверждение пароля не является критической проверкой, поэтому его можно сделать на стороне клиента. - person peterchaula; 30.12.2018

Проверка перекрестных полей может выполняться путем создания настраиваемых ограничений.

Пример: - Сравните поля password и confirmPassword экземпляра User.

CompareStrings

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

StringComparisonMode

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

CompareStringsValidator

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

ConstraintValidatorHelper

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, мы также можем сравнивать более двух свойств и можем смешивать любой из четырех методов сравнения строк.

Выбор цвета

@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 и т. Д.

P.S. Я не тестировал этот код в производственной среде (хотя я тестировал его в среде разработки), поэтому рассматривайте этот код как Milestone Release. Если вы нашли ошибку, напишите, пожалуйста, хороший комментарий. :)

person dira    schedule 25.11.2010
comment
Мне нравится этот подход, так как он более гибкий, чем другие. Это позволяет мне проверять на равенство более двух полей. Хорошая работа! - person Tauren; 15.02.2011

Если вы используете Spring Framework, вы можете использовать для этого Spring Expression Language (SpEL). Я написал небольшую библиотеку, которая предоставляет валидатор JSR-303 на основе SpEL - это упрощает кросс-полевую валидацию! Взгляните на 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;
}
person Jakub Jirutka    schedule 04.01.2014

Я попробовал пример Альбертховена (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;
    }

}
person Ralph    schedule 09.03.2010
comment
У меня он работал правильно, но не отображал сообщение об ошибке. Это сработало и отобразило для вас сообщение об ошибке. Как? - person Tiny; 10.08.2012
comment
@Tiny: сообщение должно быть в нарушениях, возвращаемых валидатором. (Напишите модульный тест: stackoverflow .com / questions / 5704743 /). НО сообщение проверки принадлежит свойству isValid. Поэтому сообщение будет отображаться в графическом интерфейсе только в том случае, если графический интерфейс показывает проблемы для retypedPassword И isValid (рядом с повторно введенным паролем). - person Ralph; 10.08.2012

Мне нравится идея Якуба Джирутки использовать 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;
}
person holmis83    schedule 24.11.2016

У меня нет репутации комментировать первый ответ, но я хотел добавить, что добавил модульные тесты для победившего ответа и имею следующие наблюдения:

  • Если вы введете неправильное имя или имя поля, вы получите ошибку проверки, как если бы значения не совпадали. Не запутайтесь из-за орфографических ошибок, например

@FieldMatch (первое = недопустимое имя поля1, второе = действительное имя поля2)

  • Валидатор будет принимать эквивалентные типы данных, т.е. все они пройдут с FieldMatch:

частный String stringField = 1;

частное целое число integerField = новое целое число (1)

частный int intField = 1;

  • Если поля относятся к объектному типу, который не реализует равенства, проверка не удастся.
person Chanoch    schedule 01.03.2014

Очень красивое решение 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();
    }
}
person McGin    schedule 20.01.2010
comment
Хм. Не уверен. Вы можете попробовать создать определенные валидаторы для каждого поля подтверждения (чтобы они имели разные аннотации) или обновить аннотацию @Matches, чтобы она принимала несколько пар полей. - person Bradley Dwyer; 20.01.2010
comment
Спасибо, bradhouse, нашел решение и опубликовал его выше. Требуется небольшая работа, чтобы учесть разное количество аргументов, поэтому вы не получите IndexOutOfBoundsExceptions, но основы есть. - person McGin; 22.01.2010

Вам нужно называть это явно. В приведенном выше примере bradhouse предоставил вам все шаги для написания настраиваемого ограничения.

Добавьте этот код в свой класс вызывающего абонента.

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

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

в приведенном выше случае это было бы

Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);
person Vishal    schedule 20.01.2010

Почему бы не попробовать Oval: http://oval.sourceforge.net/

Похоже, он поддерживает OGNL, так что, возможно, вы могли бы сделать это более естественным

@Assert(expr = "_value ==_this.pass").
person Mircea D.    schedule 28.11.2011

Вы, ребята, классные. Действительно потрясающие идеи. Мне больше всего нравятся Альбертовен и Макгинс, поэтому я решил объединить обе идеи. И разработайте общее решение для всех случаев. Вот предлагаемое мной решение.

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

}
person Sheikh Abdul Wahid    schedule 27.05.2016

Я сделал небольшую адаптацию в решении Нико, так что нет необходимости использовать библиотеку 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;

    }
}
person Pedro Bacchini    schedule 23.03.2020

Решение реализовано с вопросом: Как для доступа к полю, описанному в свойстве аннотации

@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;
}
person zach    schedule 14.06.2016