使用 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;
}
185561 次浏览

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

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

下面是注释代码:

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

您需要显式地调用它。在上面的例子中,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()方法由验证器自动调用。__abc2中报告的属性路径将从方法名validpassExpiryAfterDateOfJoining中提取。

非常好的解决方案,布拉德豪斯。是否有办法将@Matches注释应用到多个字段?

< p >编辑: 下面是我想出的解决方案来回答这个问题,我修改了约束以接受数组而不是单个值:

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

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

我尝试了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;
}


}

使用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解决方案并没有错。

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

举例:—比较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 {};
}

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验证约束,我们还可以比较两个以上的属性,并且可以混合使用四种字符串比较方法中的任何一种。

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

测试

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等跨字段验证约束。

注:我没有在生产环境下测试此代码(尽管我在开发环境下测试了它),所以将此代码视为里程碑发布。如果你发现一个bug,请写一个好的评论。:)

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

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

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

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

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

  • 如果第一个或字段名出错,就会出现验证错误,就好像值不匹配一样。不要被拼写错误绊倒。

@FieldMatch(第一次=“无效的FieldName1",第二=“validFieldName2")

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

private String stringField = "1";

private Integer integerField = new Integer(1)

private int intField = 1;

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

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


}
与问题相关的解决方案: 如何访问在注释属性中描述的字段 < / p >
@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;
}

我喜欢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;
}

我在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;


}
}