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

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

在注释中,我期望如下内容:

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

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

当前回答

如果您正在使用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;
}

其他回答

使用Hibernate Validator 4.1.0。最后,我建议使用@ScriptAssert。除了它的JavaDoc:

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

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

例子:

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

  private String passVerify;
}

或者使用更短的别名和null安全:

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

  private String passVerify;
}

或者使用Java 7+ null-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解决方案并没有什么问题。

我很惊讶这个没有开箱即用。不管怎样,这里有一个可能的解决方案。

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

下面是注释代码:

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

如果您正在使用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;
}

您需要显式地调用它。在上面的例子中,bradhouse给出了编写自定义约束的所有步骤。

在调用者类中添加此代码。

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

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

在上述情况下,它将是

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

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

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中报告的属性路径将从方法名中提取:valid和passExpiryAfterDateOfJoining。