【问题标题】:Cross field validation with Hibernate Validator (JSR 303)使用 Hibernate Validator (JSR 303) 进行跨字段验证
【发布时间】:2010-12-30 16:26:06
【问题描述】:

Hibernate Validator 4.x 中是否有跨字段验证的实现(或第三方实现)?如果没有,实现跨字段验证器的最简洁方法是什么?

例如,如何使用 API 来验证两个 bean 属性是否相等(例如验证密码字段与密码验证字段匹配)。

在注释中,我希望是这样的:

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

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

【问题讨论】:

标签: validation hibernate-validator bean-validation


【解决方案1】:

每个字段约束都应由不同的验证器注释处理,或者换句话说,不建议将一个字段的验证注释与其他字段进行检查;跨领域验证应在类级别完成。此外,JSR-303 Section 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;
    }
}

【讨论】:

  • @AndyT:对 Apache Commons BeanUtils 存在外部依赖。
  • @ScriptAssert 不允许您使用自定义路径构建验证消息。 context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); 可以突出显示正确的字段(如果只有 JSF 支持的话)。
  • 我使用了上面的示例,但它没有显示错误消息,jsp中的绑定应该是什么?我只绑定了密码和确认,还有什么需要的吗?
  • BeanUtils.getProperty 返回一个字符串。该示例可能意味着使用返回对象的PropertyUtils.getProperty
  • 不错的答案,但我已经完成了这个问题的答案:stackoverflow.com/questions/11890334/…
【解决方案2】:

我建议您另一种可能的解决方案。也许不那么优雅,但更容易!

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

  private String passVerify;

  @NotNull
  private LocalDate passExpiry;
  @NotNull
  private LocalDate dateOfJoining;

  @AssertTrue(message = "Fields `pass` and `passVerify` should be equal")
  // Any method name is ok als long it begins with `is`
  private boolean isValidPass() {
    //return pass == null && passVerify == null || pass.equals(passVerify);
    // Since Java 7:
    return Objects.equals(pass, passVerify);
  }

  @AssertTrue(message = "Field `passExpiry` should be later than `dateOfJoining`")
  // Other rules can also be validated in other methods
  private boolean isPassExpiryAfterDateOfJoining() {
    return dateOfJoining.isBefore(passExpiry);
  }
}

验证器会自动调用isValid()isPassExpiryAfterDateOfJoining() 方法。 ConstraintViolations 中报告的属性路径将从方法名称中提取:validpassExpiryAfterDateOfJoining

【讨论】:

  • 我认为这又是一个混合问题。 Bean Validation 的全部意义在于将验证外部化为 ConstraintValidators。在这种情况下,您在 bean 本身中拥有一部分验证逻辑,并在 Validator 框架中拥有一部分。要走的路是类级别的约束。 Hibernate Validator 现在还提供了一个@ScriptAssert,它使 bean 内部依赖的实现更容易。
  • 我会说这是优雅,而不是更少!
  • 到目前为止,我的观点是 Bean Validation JSR 是一个混合关注点。
  • @Hardy 虽然有些人可能认为这是一种混合关注点,但可以说 JSR 303 鼓励违反更基本的封装原则。为什么域对象不知道如何验证自己的私有状态?我认为目前流行的观点只是反映了 JPA 和 Hibernate 在多大程度上鼓励了贫血域反模式的广泛传播。
  • @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); }
【解决方案3】:

我很惊讶这不是开箱即用的。无论如何,这是一个可能的解决方案。

我创建了一个类级别验证器,而不是原始问题中描述的字段级别。

这里是注释代码:

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 分钟内写了这篇文章,所以我可能还没有解决所有的错误。如果有任何问题,我会更新答案。

【讨论】:

  • 这很好,它对我有用,除了 addNote 已被弃用,如果我使用 addPropertyNode 会得到 AbstractMethodError 。谷歌在这里没有帮助我。有什么解决办法?某处是否缺少依赖项?
【解决方案4】:

使用 Hibernate Validator 4.1.0.Final 我建议使用 @ScriptAssert。摘自其 JavaDoc:

脚本表达式可以用任何脚本或表达式编写 语言,JSR 223(“JavaTM 平台脚本”) 可以在类路径中找到兼容的引擎。

注意:评估是由运行在 Java VM 中的脚本“引擎”执行的,因此在 Java“服务器端”,不是在“客户端”如某些 cmets 所述。

例子:

@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+ null-safe 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 解决方案没有任何问题。

【讨论】:

  • 有趣的解决方案,我们真的在这里使用 javascript 来完成此验证吗?对于基于 java 的注解应该能够完成的工作来说,这似乎有点过头了。在我的处女眼看来,Nicko 提出的上述解决方案从可用性的角度来看(他的注释很容易阅读并且相当实用,而不是不优雅的 javascript->java 引用)和可扩展性的角度来看(我假设有合理的开销处理javascript,但也许Hibernate至少缓存了编译代码?)。我很想知道为什么这是首选。
  • 我同意 Nicko 的实现很不错,但我认为使用 JS 作为表达式语言没有什么不妥之处。 Java 6 包含了 Rhino,用于此类应用程序。我喜欢@ScriptAssert,因为每次我要执行一种新颖的测试类型时,它都无需创建注释和验证器即可工作。
  • 如前所述,类级别验证器没有任何问题。 ScriptAssert 只是一种替代方案,不需要您编写自定义代码。我没有说这是首选的解决方案;-)
  • 很好的答案,因为密码确认不是关键验证,因此可以在客户端完成
  • 我在高处和低处搜索了如何在项目中设置@ScriptAssert 以便找到语言的示例。无论我从什么例子(spring,javascript)中放什么,总是有一个错误说找不到语言。到目前为止,我一直找不到任何关于如何设置其中一种语言的文档。我会假设“spring”应该与 Spring Boot 一起开箱即用,但可惜它没有。
【解决方案5】:

可以通过创建自定义约束来完成跨字段验证。

示例:- 比较用户实例的密码和确认密码字段。

比较字符串

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

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.我没有在生产环境下测试过这段代码(虽然我是在开发环境下测试过的),所以把这段代码当作里程碑发布。如果您发现错误,请写一个很好的评论。 :)

【讨论】:

  • 我喜欢这种方法,因为它比其他方法更灵活。它让我可以验证两个以上的字段是否相等。干得好!
【解决方案6】:

如果您使用的是 Spring 框架,那么您可以使用 Spring 表达式语言 (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;
}

【讨论】:

    【解决方案7】:

    我尝试了 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:消息应该在验证器返回的违规中。 (编写单元测试:stackoverflow.com/questions/5704743/…)。但是验证消息属于“isValid”属性。因此,只有当 GUI 显示 retypedPassword AND isValid 问题(在 retyped Password 旁边)时,该消息才会显示在 GUI 中。
    【解决方案8】:

    我喜欢 Jakub Jirutka 提出的使用 Spring 表达式语言的想法。如果您不想添加另一个库/依赖项(假设您已经使用 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;
    }
    

    【讨论】:

      【解决方案9】:

      我没有评论第一个答案的声誉,但想补充一点,我已经为获胜答案添加了单元测试,并且有以下观察结果:

      • 如果您的名字或字段名称错误,那么您会收到一个验证错误,就好像值不匹配一样。不要被拼写错误绊倒,例如

      @FieldMatch(first="invalidFieldName1", second="validFieldName2")

      • 验证器接受等效的数据类型,即这些数据类型都将通过 FieldMatch:

      私有字符串 stringField = "1";

      private Integer integerField = new Integer(1)

      private int intField = 1;

      • 如果字段属于未实现 equals 的对象类型,则验证将失败。

      【讨论】:

        【解决方案10】:

        非常好的解决方案 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,想出了一个解决方案并将其发布在上面。当通过不同数量的争论时需要做一些工作来满足您的需求,这样您就不会得到 IndexOutOfBoundsExceptions,但基础知识就在那里。
        【解决方案11】:

        我在 Nicko 的解决方案中做了一个小改动,这样就不需要使用 Apache Commons BeanUtils 库并将其替换为 spring 中已经可用的解决方案,对于那些使用它的人来说,我可以更简单:

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

        【讨论】:

          【解决方案12】:

          您需要显式调用它。在上面的示例中,bradhouse 为您提供了编写自定义约束的所有步骤。

          将此代码添加到您的调用者类中。

          ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
          validator = factory.getValidator();
          
          Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);
          

          在上述情况下,它会是

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

          【讨论】:

            【解决方案13】:

            为什么不试试椭圆形:http://oval.sourceforge.net/

            我看起来它支持 OGNL 所以也许你可以更自然地做到这一点

            @Assert(expr = "_value ==_this.pass").
            

            【讨论】:

              【解决方案14】:

              你们太棒了。真是了不起的想法。我最喜欢 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;
                  }
              
              }
              

              【讨论】:

                【解决方案15】:

                与问题相关的解决方案: How to access a field which is described in annotation property

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

                【讨论】:

                  猜你喜欢
                  • 2011-10-23
                  • 2011-01-05
                  • 2011-08-04
                  • 2012-05-12
                  • 1970-01-01
                  • 2011-10-29
                  • 2014-03-24
                  • 2012-11-03
                  相关资源
                  最近更新 更多