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

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

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

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

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

当前回答

非常好的解决方案,布拉德豪斯。是否有办法将@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();
    }
}

其他回答

与问题相关的解决方案: 如何访问在注释属性中描述的字段

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

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

你们太棒了。真是惊人的想法。我最喜欢阿尔伯托芬和麦金的,所以我决定把他们的想法结合起来。并开发一些通用的解决方案,以满足所有情况。以下是我提出的解决方案。

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

}

我在Nicko的解决方案中做了一个小的调整,这样就没有必要使用Apache Commons BeanUtils库,并将其替换为春季已经可用的解决方案,对于那些使用它的人来说,我可以更简单:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object object, final ConstraintValidatorContext context) {

        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
        final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
        final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);

        boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);

        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                .addPropertyNode(firstFieldName)
                .addConstraintViolation();
        }

        return isValid;

    }
}

为什么不试试Oval: http://oval.sourceforge.net/呢

我看起来像它支持OGNL,所以也许你可以用更自然的方式来做

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