【问题标题】:Hibernate Validation of Collections of Primitives原语集合的休眠验证
【发布时间】:2011-05-17 12:45:50
【问题描述】:

我希望能够做类似的事情:

@Email
public List<String> getEmailAddresses()
{
   return this.emailAddresses;
}

换句话说,我希望将列表中的每个项目都验证为电子邮件地址。当然,这样注释集合是不能接受的。

有没有办法做到这一点?

【问题讨论】:

    标签: java collections bean-validation hibernate-validator


    【解决方案1】:

    JSR-303 和 Hibernate Validator 都没有任何现成的约束可以验证 Collection 的每个元素。

    解决此问题的一种可能解决方案是创建自定义 @ValidCollection 约束和相应的验证器实现 ValidCollectionValidator

    为了验证集合中的每个元素,我们需要在ValidCollectionValidator 中创建一个Validator 实例;为了获得这样的实例,我们需要自定义实现ConstraintValidatorFactory

    看看你是否喜欢以下解决方案...

    简单地说,

    • 复制粘贴所有这些 java 类(并导入相关类);
    • 在类路径中添加validation-api、hibenate-validator、slf4j-log4j12和testng jar;
    • 运行测试用例。

    有效集合

        public @interface ValidCollection {
    
        Class<?> elementType();
    
        /* Specify constraints when collection element type is NOT constrained 
         * validator.getConstraintsForClass(elementType).isBeanConstrained(); */
        Class<?>[] constraints() default {};
    
        boolean allViolationMessages() default true;
    
        String message() default "{ValidCollection.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
    }
    

    ValidCollectionValidator

        public class ValidCollectionValidator implements ConstraintValidator<ValidCollection, Collection>, ValidatorContextAwareConstraintValidator {
    
        private static final Logger logger = LoggerFactory.getLogger(ValidCollectionValidator.class);
    
        private ValidatorContext validatorContext;
    
        private Class<?> elementType;
        private Class<?>[] constraints;
        private boolean allViolationMessages;
    
        @Override
        public void setValidatorContext(ValidatorContext validatorContext) {
            this.validatorContext = validatorContext;
        }
    
        @Override
        public void initialize(ValidCollection constraintAnnotation) {
            elementType = constraintAnnotation.elementType();
            constraints = constraintAnnotation.constraints();
            allViolationMessages = constraintAnnotation.allViolationMessages();
        }
    
        @Override
        public boolean isValid(Collection collection, ConstraintValidatorContext context) {
            boolean valid = true;
    
            if(collection == null) {
                //null collection cannot be validated
                return false;
            }
    
            Validator validator = validatorContext.getValidator();
    
            boolean beanConstrained = validator.getConstraintsForClass(elementType).isBeanConstrained();
    
            for(Object element : collection) {
                Set<ConstraintViolation<?>> violations = new HashSet<ConstraintViolation<?>> ();
    
                if(beanConstrained) {
                    boolean hasValidCollectionConstraint = hasValidCollectionConstraint(elementType);
                    if(hasValidCollectionConstraint) {
                        // elementType has @ValidCollection constraint
                        violations.addAll(validator.validate(element));
                    } else {
                        violations.addAll(validator.validate(element));
                    }
                } else {
                    for(Class<?> constraint : constraints) {
                        String propertyName = constraint.getSimpleName();
                        propertyName = Introspector.decapitalize(propertyName);
                        violations.addAll(validator.validateValue(CollectionElementBean.class, propertyName, element));
                    }
                }
    
                if(!violations.isEmpty()) {
                    valid = false;
                }
    
                if(allViolationMessages) { //TODO improve
                    for(ConstraintViolation<?> violation : violations) {
                        logger.debug(violation.getMessage());
                        ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(violation.getMessage());
                        violationBuilder.addConstraintViolation();
                    }
                }
    
            }
    
            return valid;
        }
    
        private boolean hasValidCollectionConstraint(Class<?> beanType) {
            BeanDescriptor beanDescriptor = validatorContext.getValidator().getConstraintsForClass(beanType);
            boolean isBeanConstrained = beanDescriptor.isBeanConstrained();
            if(!isBeanConstrained) {
                return false;
            }
            Set<ConstraintDescriptor<?>> constraintDescriptors = beanDescriptor.getConstraintDescriptors(); 
            for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {
                if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {
                    return true;
                }
            }
            Set<PropertyDescriptor> propertyDescriptors = beanDescriptor.getConstrainedProperties();
            for(PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                constraintDescriptors = propertyDescriptor.getConstraintDescriptors();
                for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {
                    if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {
                        return true;
                    }
                }    
            }
            return false;
        }
    
    }
    

    ValidatorContextAwareConstraintValidator

    public interface ValidatorContextAwareConstraintValidator {
    
        void setValidatorContext(ValidatorContext validatorContext);
    
    }
    

    CollectionElementBean

        public class CollectionElementBean {
    
        /* add more properties on-demand */
        private Object notNull;
        private String notBlank;
        private String email;
    
        protected CollectionElementBean() {
        }
    
        @NotNull
        public Object getNotNull() { return notNull; }
        public void setNotNull(Object notNull) { this.notNull = notNull; }
    
        @NotBlank
        public String getNotBlank() { return notBlank; }
        public void setNotBlank(String notBlank) { this.notBlank = notBlank; }
    
        @Email
        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
    
    }
    

    ConstraintValidatorFactoryImpl

    public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {
    
        private ValidatorContext validatorContext;
    
        public ConstraintValidatorFactoryImpl(ValidatorContext nativeValidator) {
            this.validatorContext = nativeValidator;
        }
    
        @Override
        public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
            T instance = null;
    
            try {
                instance = key.newInstance();
            } catch (Exception e) { 
                // could not instantiate class
                e.printStackTrace();
            }
    
            if(ValidatorContextAwareConstraintValidator.class.isAssignableFrom(key)) {
                ValidatorContextAwareConstraintValidator validator = (ValidatorContextAwareConstraintValidator) instance;
                validator.setValidatorContext(validatorContext);
            }
    
            return instance;
        }
    
    }
    

    员工

    public class Employee {
    
        private String firstName;
        private String lastName;
        private List<String> emailAddresses;
    
        @NotNull
        public String getFirstName() { return firstName; }
        public void setFirstName(String firstName) { this.firstName = firstName; }
    
        public String getLastName() { return lastName; }
        public void setLastName(String lastName) { this.lastName = lastName; }
    
        @ValidCollection(elementType=String.class, constraints={Email.class})
        public List<String> getEmailAddresses() { return emailAddresses; }
        public void setEmailAddresses(List<String> emailAddresses) { this.emailAddresses = emailAddresses; }
    
    }
    

    团队

    public class Team {
    
        private String name;
        private Set<Employee> members;
    
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
    
        @ValidCollection(elementType=Employee.class)
        public Set<Employee> getMembers() { return members; }
        public void setMembers(Set<Employee> members) { this.members = members; }
    
    }
    

    购物车

    public class ShoppingCart {
    
        private List<String> items;
    
        @ValidCollection(elementType=String.class, constraints={NotBlank.class})
        public List<String> getItems() { return items; }
        public void setItems(List<String> items) { this.items = items; }
    
    }
    

    ValidCollectionTest

    public class ValidCollectionTest {
    
        private static final Logger logger = LoggerFactory.getLogger(ValidCollectionTest.class);
    
        private ValidatorFactory validatorFactory;
    
        @BeforeClass
        public void createValidatorFactory() {
            validatorFactory = Validation.buildDefaultValidatorFactory();
        }
    
        private Validator getValidator() {
            ValidatorContext validatorContext = validatorFactory.usingContext();
            validatorContext.constraintValidatorFactory(new ConstraintValidatorFactoryImpl(validatorContext));
            Validator validator = validatorContext.getValidator();
            return validator;
        }
    
        @Test
        public void beanConstrained() {
            Employee se = new Employee();
            se.setFirstName("Santiago");
            se.setLastName("Ennis");
            se.setEmailAddresses(new ArrayList<String> ());
            se.getEmailAddresses().add("segmail.com");
            Employee me = new Employee();
            me.setEmailAddresses(new ArrayList<String> ());
            me.getEmailAddresses().add("me@gmail.com");
    
            Team team = new Team();
            team.setMembers(new HashSet<Employee>());
            team.getMembers().add(se);
            team.getMembers().add(me);
    
            Validator validator = getValidator();
    
            Set<ConstraintViolation<Team>> violations = validator.validate(team);
            for(ConstraintViolation<Team> violation : violations) {
                logger.info(violation.getMessage());
            }
        }
    
        @Test
        public void beanNotConstrained() {
            ShoppingCart cart = new ShoppingCart();
            cart.setItems(new ArrayList<String> ());
            cart.getItems().add("JSR-303 Book");
            cart.getItems().add("");
    
            Validator validator = getValidator();
    
            Set<ConstraintViolation<ShoppingCart>> violations = validator.validate(cart, Default.class);
            for(ConstraintViolation<ShoppingCart> violation : violations) {
                logger.info(violation.getMessage());
            }
        }
    
    }
    

    输出

    02:16:37,581  INFO main validation.ValidCollectionTest:66 - {ValidCollection.message}
    02:16:38,303  INFO main validation.ValidCollectionTest:66 - may not be null
    02:16:39,092  INFO main validation.ValidCollectionTest:66 - not a well-formed email address
    
    02:17:46,460  INFO main validation.ValidCollectionTest:81 - may not be empty
    02:17:47,064  INFO main validation.ValidCollectionTest:81 - {ValidCollection.message}
    

    注意:- 当 bean 有约束时,不要指定 @ValidCollection 约束的 constraints 属性。当 bean 没有约束时,constraints 属性是必需的。

    【讨论】:

    • 多么棒的答案!我会尽快处理这个。谢谢你,becomputer06!
    • 非常详细详尽的回答!
    【解决方案2】:

    感谢 becomputer06 的精彩回答。 但我认为应该在 ValidCollection 定义中添加以下注释:

    @Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = ValidCollectionValidator.class)
    

    而且我仍然不明白如何处理原始类型包装器和约束注释(如@Size、@Min、@Max 等)的集合,因为值无法通过 becomputer06 的方式传递。

    当然,我可以为我的应用程序中的所有情况创建自定义约束注解,但无论如何我必须将这些注解的属性添加到 CollectionElementBean。而且这似乎是一个糟糕的解决方案。

    【讨论】:

      【解决方案3】:

      由于 Java Annotations 本身的限制,不可能编写像 @EachElement 这样的通用包装注释来包装任何约束注释。但是,您可以编写一个通用约束验证器类,它将每个元素的实际验证委托给现有的约束验证器。您必须为每个约束编写一个包装注释,但只有一个验证器。

      我已经在jirutka/validator-collection(在 Maven Central 中提供)中实现了这种方法。例如:

      @EachSize(min = 5, max = 255)
      List<String> values;
      

      这个库允许您轻松地为 any 验证约束创建“伪约束”来注释简单类型的集合,而无需为每个集合编写额外的验证器或不必要的包装类。所有标准 Bean Validation 约束和 Hibernate 特定约束都支持 EachX 约束。

      要为您自己的@Awesome 约束创建@EachAwesome,只需复制并粘贴注释类,将@Constraint 注释替换为@Constraint(validatedBy = CommonEachValidator.class) 并添加注释@EachConstraint(validateAs = Awesome.class)。就是这样!

      // common boilerplate
      @Documented
      @Retention(RUNTIME)
      @Target({METHOD, FIELD, ANNOTATION_TYPE})
      // this is important!
      @EachConstraint(validateAs = Awesome.class)
      @Constraint(validatedBy = CommonEachValidator.class)
      public @interface EachAwesome {
      
          // copy&paste all attributes from Awesome annotation here
          String message() default "";
          Class<?>[] groups() default {};
          Class<? extends Payload>[] payload() default {};
          String someAttribute();
      }
      

      编辑:针对当前版本的库进行了更新。

      【讨论】:

      • 这看起来很棒,伙计,可惜它不能输入:(。那会让一切变得更加优雅
      • @Stef 查看当前版本。 ;)
      • @JakubJirutka 我正在为我的自定义约束做同样的事情,但是我从CommonEachValidator 初始化中得到一个异常,说我正在使用Awesome.class,它本身没有验证器吗?它只使用@Pattern 约束,没有别的
      • @JakubJirutka 是的,对于自定义验证器,我收到了相同的消息。例如,如果有一个验证 Set 的出色框架的示例,那就太好了。这样一来,人们就可以复制它了……除非我遗漏了有关您的 doco 的某些内容。我不认为我是。提前致谢。
      • @JakubJirutka 好的,它现在对我有用(我的错误),但我没有收到关于我的 Set 中所有失败条目的错误消息......所以我有点回到原点...... .换句话说,如果我的 Set 有 1 个有效值和 2 个无效值,它会在第一个无效后停止验证,还是会验证所有这些值并返回整个 Set 的错误就是我要找的。在此先感谢...示例会很棒...因为我很确定这是一个常见的用例...无论验证失败,都可以验证所有内容。
      【解决方案4】:

      JSR-303 能够扩展内置约束的目标类型:参见7.1.2. Overriding constraint definitions in XML

      您可以实现一个ConstraintValidator&lt;Email, List&lt;String&gt;&gt;,它与给定的答案做同样的事情,委托给原始验证器。然后你可以保留你的模型定义并在List&lt;String&gt;上应用@Email

      【讨论】:

      • 有趣的方法。但是,我找不到对支持新 TYPE_USE 注释的验证器包的任何引用。我只发现这篇文章提到 Hibernate Validator 5.2 可能支持它:in.relation.to/2014/10/23/…
      【解决方案5】:

      我没有足够高的声誉来评论这个原始答案,但也许在这个问题上值得注意的是JSR-308 处于最终发布阶段,并将在发布时解决这个问题!但是,它至少需要 Java 8。

      唯一的区别是验证注释将进入类型声明。

      //@Email
      public List<@Email String> getEmailAddresses()
      {
         return this.emailAddresses;
      }
      

      请让我知道您认为我可以将这些信息放在哪里最适合其他正在寻找的人。谢谢!

      附:欲了解更多信息,check out this SO post

      【讨论】:

      • 这是正确的答案,前提是您还在您的方法中添加@Valid(而不是注释的//@Email
      • 文档链接?
      • 这里是 container element constraints 的文档和 Hibernate Validator。
      • @Valid 在从 Hibernate Validator 6 开始的方法或容器上不再需要。另外需要注意的是:这目前在 Kotlin 中不起作用,因为 Kotlin 不保留元素上的注释. youtrack.jetbrains.net/issue/KT-13228.
      【解决方案6】:

      一个非常简单的解决方法是可能的。您可以改为验证包含简单值属性的类的集合。为此,您需要在集合上使用 @Valid 注释。

      示例:

      public class EmailAddress {
      
        @Email
        String email;
      
        public EmailAddress(String email){
          this.email = email;
        }
      }
      
      public class Foo {
      
        /* Validation that works */
        @Valid
        List<EmailAddress> getEmailAddresses(){
          return this.emails.stream().map(EmailAddress::new).collect(toList());
        }
      
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-07-16
        • 1970-01-01
        • 2012-09-18
        • 1970-01-01
        • 2011-03-26
        • 2012-11-19
        • 2011-02-11
        • 1970-01-01
        相关资源
        最近更新 更多