使用Hibernate Validator进行交叉字段验证(JSR 303)

bra*_*use 228 validation hibernate-validator bean-validation

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

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

在注释中,我希望有类似的东西:

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

  @Equals(property="pass")
  private String passVerify;
}
Run Code Online (Sandbox Code Playgroud)

小智 274

每个字段约束应该由不同的验证器注释处理,或者换句话说,不建议练习使一个字段的验证注释检查其他字段; 跨领域验证应在班级进行.此外,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;
}
Run Code Online (Sandbox Code Playgroud)

注释:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

验证者:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @AndyT:Apache Commons BeanUtils存在外部依赖. (8认同)
  • 我使用上面的示例,但它没有显示错误信息,jsp中的绑定应该是什么?我有密码绑定和确认,还有什么需要吗?<form:password path ="password"/> <form:errors path ="password"cssClass ="errorz"/> <form:password path ="confirmPassword"/> <form:errors path ="confirmPassword"cssClass =" errorz"/> (8认同)
  • @ScriptAssert不允许您使用自定义路径构建验证消息.`context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation();`提供突出显示正确字段的可能性(如果只有JSF支持它). (7认同)
  • `BeanUtils.getProperty`返回一个字符串.该示例可能意味着使用返回对象的`PropertyUtils.getProperty`. (7认同)
  • 很好的答案,但我已经完成了这个问题的答案:http://stackoverflow.com/questions/11890334/cross-field-validation-with-hibernatevalidator-works-fine-but-displays-no-error (2认同)
  • 可以使用Spring中的BeanWrapperImpl而不是Apache的BeanUtils. (2认同)

Alb*_*ven 152

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

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

isValid验证器自动调用该方法.

  • 我认为这再次引起了人们的关注.Bean Validation的重点是将验证外部化为ConstraintValidators.在这种情况下,您在bean本身中拥有部分验证逻辑,并且部分在Validator框架中.要走的路是一个类级约束.Hibernate Validator现在还提供了一个@ScriptAssert,它使bean内部依赖的实现更容易. (12认同)
  • 我会说这是*更优雅,而不是更少! (10认同)
  • 到目前为止,我的观点是**Bean Validation**JSR是一个混合的问题. (8认同)
  • @GaneshKrishnan如果我们想要几个这样的`@AssertTrue`-ed方法怎么办?一些命名约定是否成立? (3认同)
  • 为什么这不是最好的答案 (3认同)
  • @Stephane 我正在用不同的方法对所有此类跨领域验证进行编码,以便获得适当的错误消息。它们都必须以 `is` 开头,否则框架将忽略它们。实际名称只对您重要,因为方法最好保留为“私有”。例如:`@AssertTrue(message="两个电子邮件字段应该相同") private boolean isEmailVerifyValid() { return this.email.equals(this.emailVerify); }` 和 `@AssertTrue(message="两个密码字段应该是一样的") private boolean isPassVerifyValid() { return this.pass.equals(this.passVerify); }` (3认同)
  • @Hardy虽然有些人可能认为这是一个混合的问题,但可以认为JSR 303鼓励违反更基本的封装原则.为什么域对象不应该知道如何验证自己的私有状态?我认为目前流行的观点只反映了JPA和Hibernate在多大程度上鼓励了贫血领域反模式的广泛传播. (2认同)

bra*_*use 29

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

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

这是注释代码:

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();
}
Run Code Online (Sandbox Code Playgroud)

验证器本身:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,我已使用MVEL检查要验证的对象的属性.这可以用标准反射API替换,或者如果它是您正在验证的特定类,则访问器方法本身.

然后可以在bean上使用@Matches注释,如下所示:

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

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

  ...
}
Run Code Online (Sandbox Code Playgroud)

作为免责声明,我在最后5分钟写了这个,所以我可能还没有解决所有的错误.如果出现任何问题,我会更新答案.


Har*_*rdy 27

使用Hibernate Validator 4.1.0.Final我建议使用@ScriptAssert:

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

  private String passVerify;
}
Run Code Online (Sandbox Code Playgroud)

自定义类级验证器@Matches解决方案没有任何问题.

  • 如上所述,类级别验证器没有任何问题.ScriptAssert只是一种替代方案,不需要您编写自定义代码.我没有说这是首选的解决方案;-) (4认同)
  • 有趣的解决方案,我们真的在这里使用 javascript 来完成此验证吗?对于基于 java 的注释应该能够完成的任务来说,这似乎有点矫枉过正。在我的处女眼中,Nicko 提出的解决方案从可用性的角度来看仍然看起来更干净(他的注释很容易阅读,并且相对于不优雅的 javascript-&gt;java 引用来说相当实用),并且从可扩展性的角度来看(我认为有合理的开销)处理 javascript,但也许 Hibernate 至少缓存了编译后的代码?)。我很好奇为什么这是首选。 (2认同)
  • 我同意Nicko的实现很好,但是我没有看到任何关于使用JS作为表达式语言的反感.Java 6包含Rhino用于此类应用程序.我喜欢@ScriptAssert,因为它只是工作而不必每次我都有一个新颖的测试类型来创建一个注释和验证器. (2认同)

dir*_*ira 18

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

示例: - 比较User实例的password和confirmPassword字段.

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 {};
}
Run Code Online (Sandbox Code Playgroud)

StringComparisonMode

public enum StringComparisonMode {
    EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

用户

@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; }
}
Run Code Online (Sandbox Code Playgroud)

测试

    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);
    }
Run Code Online (Sandbox Code Playgroud)

产量 Message:- [Password, ConfirmPassword] must be equal.

通过使用CompareStrings验证约束,我们还可以比较两个以上的属性,并且我们可以混合四种字符串比较方法中的任何一种.

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;
        ......
}
Run Code Online (Sandbox Code Playgroud)

测试

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());
        }
Run Code Online (Sandbox Code Playgroud)

产量 Message:- Please choose three different colors.

同样,我们可以使用CompareNumbers,CompareDates等跨字段验证约束.

PS我没有在生产环境下测试过这段代码(虽然我在开发环境下测试过),所以请将此代码视为Milestone Release.如果您发现了错误,请写一个好评.:)


Ral*_*lph 9

我试过Alberthoven的例子(hibernate-validator 4.0.2.GA),我得到一个ValidationException:"带注释的方法必须遵循JavaBeans命名约定.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;
    }

}
Run Code Online (Sandbox Code Playgroud)


Jak*_*tka 6

如果您正在使用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;
}
Run Code Online (Sandbox Code Playgroud)

您也可以轻松修改此选项,以便仅在不为空时才验证密码字段.

@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;
}
Run Code Online (Sandbox Code Playgroud)


hol*_*s83 6

我喜欢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();
}
Run Code Online (Sandbox Code Playgroud)

验证器:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

像这样申请:

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


归档时间:

查看次数:

153256 次

最近记录:

6 年,2 月 前