1. 概述

在这个教程中,我们将学习如何使用Java的验证API在对象反序列化后进行验证。

2. 手动触发验证

Java的bean验证API定义在JSR 380中。它的一个常见用法是在Spring控制器中的@Valid注解参数。然而,在本文中,我们将专注于控制器之外的验证。

首先,让我们编写一个方法,用于检查对象的内容是否符合其验证约束。为此,我们将从默认验证工厂获取Validator,然后将validate()方法应用于对象。这个方法返回一个Set<ConstraintViolation>ConstraintViolation封装了一些关于验证错误的提示。为了简化,如果发生任何验证问题,我们将简单地抛出一个ConstraintViolationException

<T> void validate(T t) {
    Set<ConstraintViolation<T>> violations = validator.validate(t);
    if (!violations.isEmpty()) {
        throw new ConstraintViolationException(violations);
    }
}

如果我们对一个对象调用此方法,如果对象不遵守任何验证约束,它将抛出异常。这个方法可以在已存在且附加有约束的对象的任何时间点被调用。

3. 将验证纳入反序列化过程

现在我们的目标是将验证纳入反序列化过程。具体来说,我们将重写Jackson的反序列化器,以便在反序列化后立即执行验证。这将确保每次我们反序列化一个对象时,如果它不符合规范,都将不允许进一步处理。

首先,我们需要重写默认的BeanDeserializerBeanDeserializer是一种可以反序列化对象的类。我们希望调用基础的反序列化方法,然后对创建的实例应用我们的validate()方法。我们的BeanDeserializerWithValidation看起来像这样:

public class BeanDeserializerWithValidation extends BeanDeserializer {

    protected BeanDeserializerWithValidation(BeanDeserializerBase src) {
        super(src);
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        Object instance = super.deserialize(p, ctxt);
        validate(instance);
        return instance;
    }

}

接下来,我们需要实现自己的BeanDeserializerModifier。这将允许我们在BeanDeserializerWithValidation中定义的行为上修改反序列化过程:

public class BeanDeserializerModifierWithValidation extends BeanDeserializerModifier {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        if (deserializer instanceof BeanDeserializer) {
            return new BeanDeserializerWithValidation((BeanDeserializer) deserializer);
        }

        return deserializer;
    }

}

最后,我们需要创建一个ObjectMapper并将其BeanDeserializerModifier注册为一个ModuleModule是扩展Jackson默认功能的一种方式。让我们将它包装在一个方法中:

ObjectMapper getObjectMapperWithValidation() {
    SimpleModule validationModule = new SimpleModule();
    validationModule.setDeserializerModifier(new BeanDeserializerModifierWithValidation());
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(validationModule);
    return mapper;
}

4. 示例用法:从文件读取并验证对象

现在我们将展示如何使用自定义的ObjectMapper。首先,定义一个Student对象。一个Student有一个名字,名字的长度必须在5到10个字符之间:

public class Student {

    @Size(min = 5, max = 10, message = "Student's name must be between 5 and 10 characters")
    private String name;

    public String getName() {
        return name;
    }

}

现在,创建一个validStudent.json文件,其中包含一个有效Student对象的JSON表示:

{
  "name": "Daniel"
}

我们将从这个文件中读取内容到一个InputStream。首先,定义一个方法,该方法从InputStream解析到一个Student对象,并同时进行验证。为此,我们想要使用我们的ObjectMapper

Student readStudent(InputStream inputStream) throws IOException {
    ObjectMapper mapper = getObjectMapperWithValidation();
    return mapper.readValue(inputStream, Student.class);
}

现在我们可以编写一个测试,其中我们将:

  • 首先从文件中读取内容到InputStream
  • InputStream转换为Student对象
  • 检查Student对象的内容是否与预期相符

这个测试看起来像这样:

@Test
void givenValidStudent_WhenReadStudent_ThenReturnStudent() throws IOException {
    InputStream inputStream = getClass().getClassLoader().getResourceAsStream(("validStudent.json");
    Student result = readStudent(inputStream);
    assertEquals("Daniel", result.getName());
}

类似地,我们可以创建一个invalid.json文件,其中包含一个名字少于5个字符的Student对象的JSON表示:

{
  "name": "Max"
}

现在我们需要调整测试,以检查确实抛出了ConstraintViolationException。此外,我们还可以检查错误消息是否正确:

@Test
void givenStudentWithInvalidName_WhenReadStudent_ThenThrows() {
    InputStream inputStream = getClass().getClassLoader().getResourceAsStream("invalidStudent.json");
    ConstraintViolationException constraintViolationException = assertThrows(ConstraintViolationException.class, () -> readStudent(inputStream));
    assertEquals("name: Student's name must be between 5 and 10 characters", constraintViolationException.getMessage());
}

5. 总结

在这篇文章中,我们了解了如何重写Jackson的配置,以便在反序列化后立即验证对象。因此,我们可以保证之后不可能再对无效对象进行操作。

如往常一样,相关的代码可在GitHub上找到。