使用Hibernate Validator进行交叉字段validation(JSR 303)
在Hibernate Validator 4.x中是否有实现(或第三方实现)的交叉字段validation? 如果不是,那么实现跨域validation器的最简洁的方法是什么?
例如,如何使用API来validation两个bean属性是否相等(如validation密码字段与密码validation字段匹配)。
在注释中,我希望如下所示:
public class MyBean { @Size(min=6, max=50) private String pass; @Equals(property="pass") private String passVerify; }
每个字段约束应该由一个独特的validation器注释来处理,或者换句话说,不build议实践将一个字段的validation注释检查与其他字段进行比较; 应该在课堂上进行跨领域validation。 此外, JSR-303第2.2节首选通过注释列表来表示同一types的多个validation。 这允许每个匹配指定错误消息。
例如,validation一个常见的forms:
@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(); } }
validation者:
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; } }
我build议你另一个可能的解决scheme 也许不那么优雅,但更容易!
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); } }
isValid()方法由validation器自动调用。
我很惊讶,这是不可用的。 无论如何,这是一个可能的解决scheme。
我创build了一个类级别的validation器,而不是原始问题中描述的字段级别。
这是注释代码:
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(); }
validation者本身:
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来检查被validation对象的属性。 这可以用标准的reflectionAPI来代替,或者如果它是你正在validation的特定的类,访问器方法本身。
@Matches注解然后可以在bean上使用,如下所示:
@Matches(field="pass", verifyField="passRepeat") public class AccountCreateForm { @Size(min=6, max=50) private String pass; private String passRepeat; ... }
作为一个免责声明,我在最后5分钟内写了这个,所以我可能还没有把所有的错误都解决掉了。 如果出现任何问题,我会更新答案。
用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; }
自定义类级别validation器@Matches解决scheme没有错。
交叉字段validation可以通过创build自定义约束来完成。
例如: – 比较用户实例的密码和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; } }
testing
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.
通过使用CompareStringsvalidation约束,我们还可以比较两个以上的属性,我们可以混合四种string比较方法中的任意一种。
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; ...... }
testing
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等交叉字段validation约束。
PS我没有在生产环境下testing这个代码(虽然我在开发环境下testing过),所以把这个代码当作Milestone Release。 如果你发现一个错误,请写一个很好的评论。 🙂
我已经尝试了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; } }
如果您使用的是Spring框架,那么您可以使用Springexpression式语言(SpEL)。 我写了一个小型库,提供基于SpEL的JSR-303validation器 – 它使得跨场validation变得轻而易举! 看看https://github.com/jirutka/validator-spring 。
这将validation密码字段的长度和相等性。
@SpELAssert(value = "pass.equals(passVerify)", message = "{validator.passwords_not_same}") public class MyBean { @Size(min = 6, max = 50) private String pass; private String passVerify; }
您也可以很容易地修改这个来validation密码字段,只有当两者都不为空时。
@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; }
从这个专用的JSR-303约束库中使用@EqualProperties
:
@EqualProperties({"pass", "passVerify"}) public class MyBean { @Size(min=6, max=50) @NotNull private String pass; @NotNull private String passVerify; }
我没有评论第一个答案的声望,但想补充一点,我已经为获胜的答案添加了unit testing,并且有以下观察:
- 如果你得到第一个或字段名称错误,那么你会得到一个validation错误,就像这些值不匹配一样。 不要因为拼写错误而绊倒
@FieldMatch(first =“ invalid FieldName1”,second =“validFieldName2”)
- validation器将接受等效的数据types,即这些将全部通过FieldMatch:
private String stringField =“1”;
私人整数整数字段=新的整数(1)
private int intField = 1;
- 如果这些字段是不实现equals的对象types,则validation将失败。
非常好的解决schemebradhouse。 有没有办法将@Matches注解应用到多个字段?
编辑:这是我想出来回答这个问题的解决scheme,我修改约束接受数组而不是一个单一的值:
@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(); } }
你需要明确地调用它。 在上面的例子中,bradhouse为您提供了编写自定义约束的所有步骤。
在你的调用者类中添加这个代码。
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);
在上面的情况下是这样的
Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);
为什么不试试椭圆形: http : //oval.sourceforge.net/
我看起来像它支持OGNL,所以也许你可以做一个更自然的
@Assert(expr = "_value ==_this.pass").
我喜欢Jakub Jirutka的想法,使用Springexpression式语言。 如果你不想添加另一个库/依赖(假设你已经使用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(); }
validation者:
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; }
你们真棒 真的很棒的想法。 我最喜欢Alberthoven和McGin ,所以我决定把这两个想法结合起来。 并制定一些通用的解决scheme来迎合所有的情况。 这是我提出的解决scheme。
@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; } }
与问题相关的解决scheme: 如何访问注释属性中描述的字段
@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; }