【问题标题】:Create runtime constraints and validate a map of fields创建运行时约束并验证字段映射
【发布时间】:2020-10-27 22:44:25
【问题描述】:

我有一个输入字段映射,我想通过在运行时创建动态约束来使用 JSR-303 进行验证。

假设我的地图结构如下:

name: Foo
description: Foo description

现在,如果我有一组规则存储在某种配置中:

name:
    required: true
    minLength: 3
    maxLength: 30

description:
    required: false
    maxLength: 200

我想使用上述配置在运行时创建的约束来验证我的输入映射。我知道我们可以create custom ConstraintMappings using Hibernate Validator 如下(示例取自 Hibernate Validator 文档):

HibernateValidatorConfiguration configuration = Validation
    .byProvider(HibernateValidator.class)
    .configure();

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type(Car.class)
        .property("manufacturer", FIELD)
            .constraint( new NotNullDef())
        .property("licensePlate", FIELD)
            .ignoreAnnotations(true)
            .constraint(new NotNullDef())
            .constraint(new SizeDef().min(2).max(14))

Validator validator = configuration.addMapping(constraintMapping)
        .buildValidatorFactory()

但显然这不适用于java.util.Maps,因为地图不是 JavaBeans。那么我该怎么做这样的事情呢?我考虑过的几件事(每种方法的缺点):

  1. 在运行时使用 ByteBuddy 或类似的东西创建类,然后使用 Hibernate Validator 进行验证(听起来有点矫枉过正)
  2. 使用一系列if 条件手动验证并抛出ConstraintViolationException(我将在这里重新发明很多轮子)

我想了解你在这种情况下会怎么做。

【问题讨论】:

    标签: java bean-validation hibernate-validator


    【解决方案1】:

    有太多问题要回答,但我会尝试。

    动态创建的约束映射

    如示例所述,它是可用的。有两种方法可以创建自定义映射。

    以编程方式创建自定义约束映射

    假设有一个 POJO

    import java.util.Map;
    
    public class Foo {
        private Map<String, String> rules;
    
        public Map<String, String> getRules() {
            return rules;
        }
    
        public void setRules(Map<String, String> rules) {
            this.rules = rules;
        }
    }
    

    描述的约束是:

    • 地图不能为空
    • 最大。地图大小为 2
    • 地图的键至少应为 3 个字符
    • 值不能为空并且
    • 值必须为最大值。 5 个字符

    像这样:

    @NotNull
    @Size(max=3)
    private Map<@Size(min=3)String, @NotBlank @Size(max=5)String> rules;
    
    

    约束映射为:

    var configuration = Validation
            .byProvider(HibernateValidator.class)
            .configure();
    
    ConstraintMapping constraintMapping = configuration.createConstraintMapping();
    
    constraintMapping
            .type(Foo.class)
            .field("rules")
            .constraint(new NotNullDef())
            .constraint(new SizeDef().max(2))
            .containerElementType(0)
                .constraint(new SizeDef().min(3))
            .containerElementType(1)
                .constraint(new NotBlankDef())
                .constraint(new SizeDef().max(5))
            ;
    
    var validator = configuration.addMapping(constraintMapping)
            .buildValidatorFactory()
            .getValidator();
    

    工作示例是:

    public class FooValidatorTest {
        @Test
        void name() {
            var configuration = Validation
                    .byProvider(HibernateValidator.class)
                    .configure();
    
            ConstraintMapping constraintMapping = configuration.createConstraintMapping();
    
            constraintMapping
                    .type(Foo.class)
                    .field("rules")
                    .constraint(new NotNullDef())
                    .constraint(new SizeDef().max(2))
                    .containerElementType(0)
                        .constraint(new SizeDef().min(3))
                    .containerElementType(1)
                        .constraint(new NotBlankDef())
                        .constraint(new SizeDef().max(5))
            ;
            var validator = configuration.addMapping(constraintMapping)
                    .buildValidatorFactory()
                    .getValidator();
            assertAll(
                    () -> {
                        var obj = new Foo();
                        /*
                        expected violation(s)
                            map is null
                         */
    
                        var result = validator.validate(obj).stream().toList();
                        assertAll("Only @NotNull should violated",
                                () -> assertEquals(1, result.size()),
                                () -> assertEquals("{jakarta.validation.constraints.NotNull.message}",
                                        result.get(0).getMessageTemplate()
                                )
                        );
                    },
                    () -> {
                        var obj = new Foo();
                        /*
                        expected violation(s)
                            map contains too much elements (max: 2)
                        note: keys and values are not validated
                         */
                        obj.setRules(Map.of("name1", "desc1", "name2", "desc2", "name3", "desc3"));
                        var result = validator.validate(obj).stream().toList();
                        assertAll("Only @Size should violated",
                                () -> assertEquals(1, result.size()),
                                () -> assertEquals("{jakarta.validation.constraints.Size.message}",
                                        result.get(0).getMessageTemplate()
                                )
                        );
                    },
                    () -> {
                        var obj = new Foo();
                        /*
                        expected violation(s)
                            first key is too short (min: 3)
                            first value is blank
                            second value is too long (max:5)
                         */
                        obj.setRules(Map.of("aa", "", "bbb", "longValue"));
                        var result = validator.validate(obj).stream().toList();
                        var templates = result.stream().map(ConstraintViolation::getMessageTemplate).toList();
                        var expectedTemplates = new String[]{
                                "{jakarta.validation.constraints.NotBlank.message}",
                                "{jakarta.validation.constraints.Size.message}",
                                "{jakarta.validation.constraints.Size.message}"
                        };
                        assertAll(
                                () -> assertEquals(3, result.size()),
                                () -> assertThat(templates, containsInAnyOrder(expectedTemplates))
                        );
                    }
            );
        }
    }
    

    使用 xml 创建自定义约束映射

    .addMapping 方法有另一个接受输入流的签名。

    xml定义为:

    <?xml version="1.0" encoding="UTF-8"?>
    <constraint-mappings
            xmlns="https://jakarta.ee/xml/ns/validation/mapping"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation=
                    "https://jakarta.ee/xml/ns/validation/mapping https://jakarta.ee/xml/ns/validation/validation-mapping-3.0.xsd"
            version="3.0">
        <bean class="Foo">
            <field name="rules">
                <container-element-type type-argument-index="0">
                    <container-element-type>
                        <constraint annotation="jakarta.validation.constraints.Size">
                            <element name="min">3</element>
                        </constraint>
                    </container-element-type>
                </container-element-type>
                <container-element-type type-argument-index="1">
                    <container-element-type>
                        <constraint annotation="jakarta.validation.constraints.NotBlank"/>
                        <constraint annotation="jakarta.validation.constraints.Size">
                            <element name="max">5</element>
                        </constraint>
                    </container-element-type>
                </container-element-type>
                <constraint annotation="jakarta.validation.constraints.NotNull"/>
                <constraint annotation="jakarta.validation.constraints.Size">
                    <element name="max">2</element>
                </constraint>
            </field>
        </bean>
    </constraint-mappings>
    

    而且这个修改是必要的:

    var validator = configuration
            .addMapping(
                getClass().getClassLoader()
                    .getResourceAsStream("foo_mapping.xml")
            )
            .buildValidatorFactory()
            .getValidator();
    

    加载外部类

    在运行时字节码生成完成后,这些类应该加载并且加载的类应该传递给validator。 所以首先要做的是创建一个类加载器(ucl)。之后可以传递给HibernateValidatorConfiguration

    var configuration = Validation
            .byProvider(HibernateValidator.class)
            .configure()
            .externalClassLoader(ucl);
    

    从技术上讲,它可以动态创建和加载 POJO 并运行自定义验证映射,但我认为这不是一个好方法。 每次创建类或创建自定义映射时,都应该创建一个新的 Validator 实例。虽然实例化一个新的验证器很便宜,但它需要一个新的ValidatorFactory,这非常昂贵(~400ms)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-05-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-14
      • 1970-01-01
      • 2015-10-20
      • 1970-01-01
      相关资源
      最近更新 更多