Есть свои плюсы и минусы в том, чтобы рассматриваться валидацию как бизнес-логику, и Spring предлагает правила проектирования для валидации (и привязки данных), которые не исключают ни того, ни другого. В частности, валидация не должна быть привязана к веб-уровню, должна быть легко локализуемой, а также должна быть возможность подключить любой доступный валидатор. Учитывая эти требования, Spring предоставляет контракт Validator, который является базовым и может использоваться на каждом уровне приложения.

Привязывание данных полезно для обеспечения динамической привязки вводимой пользователем информации к доменной модели приложения (или любым объектам, которые используются для обработки вводимой пользователем информации). Spring предоставляет удачно сформулированный DataBinder для выполнения именно этой задачи. Validator и DataBinder составляют пакет validation, который в основном используется на веб-уровне, но не ограничивается им.

BeanWrapper является фундаментальной концепцией в Spring Framework и используется во многих случаях. Однако, вероятно, BeanWrapper не придется использовать напрямую. Однако, поскольку это справочная документация, мы сочли необходимым дать некоторые пояснения. Мы даем разъяснения по BeanWrapper в этой главе, поскольку, если вы вообще соберетесь его использовать, то, скорее всего, будете делать это при попытке привязать данные к объектам.

DataBinder и BeanWrapper более низкого уровня из Spring используют реализацию PropertyEditorSupport для анализа и форматирования значений свойств. Типы PropertyEditor и PropertyEditorSupport являются частью спецификации классов JavaBeans и также описаны в этой главе. В Spring 3 появился пакет core.convert, который предоставляет общие средства преобразования типов, а также высокоуровневый пакет "format" для форматирования значений полей пользовательского интерфейса. Вы можете использовать эти пакеты как более простые альтернативы реализации PropertyEditorSupport. Они также описаны в этой главе.

Spring поддерживает Java Bean Validation через настраиваемую инфраструктуру и адаптер к собственному контракту Validator из Spring. Приложения могут единожды активировать валидацию бина глобально и использовать её исключительно для нужд валидации. На веб-уровне приложения могут дополнительно регистрировать локальные экземпляры Validator из Spring для DataBinder, что может быть полезно для подключения специальной логики валидации.

Валидация с использованием интерфейса валидатора Spring

Spring предлагает интерфейс Validator, который можно использовать для валидации объектов. Интерфейс Validator работает с использованием объекта Errors, поэтому во время проверки достоверности валидаторы могут сообщать об ошибках валидации в объект Errors.

Рассмотрим следующий пример небольшого объекта данных:

Java
public class Person {
    private String name;
    private int age;
    // обычные геттеры и сеттеры...
}
Kotlin
class Person(val name: String, val age: Int)

Следующий пример обеспечивает логику работы валидации для класса Person путем реализации следующих двух методов интерфейса org.springframework.validation.Validator:

  • supports(Class): Может ли этот Validator осуществлять валидацию экземпляров предоставленного Class?

  • validate(Object, org.springframework.validation.Errors): Валидирует заданный объект и, в случае выявления ошибок, регистрирует их в заданном объекте Errors.

Реализация Validator довольно проста, особенно если вы знаете о вспомогательном классе ValidationUtils, который также предоставляет Spring Framework. В следующем примере реализован Validator для экземпляров Person:

Java
public class PersonValidator implements Validator {
    /**
     * Этот валидатор проверяет только экземпляры Person.
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}
Kotlin
class PersonValidator : Validator {
    /**
     * Этот валидатор проверяет только экземпляры Person.
     */
    override fun supports(clazz: Class<*>): Boolean {
        return Person::class.java == clazz
    }
    override fun validate(obj: Any, e: Errors) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty")
        val p = obj as Person
        if (p.age < 0) {
            e.rejectValue("age", "negativevalue")
        } else if (p.age > 110) {
            e.rejectValue("age", "too.darn.old")
        }
    }
}

Метод static rejectIfEmpty(..) класса ValidationUtils используется для исключения свойства name, если оно равно null или пустой строке. Просмотрите javadoc по ValidationUtils, чтобы узнать, какие функции он предоставляет помимо тех, что были показаны в примере ранее.

Конечно, можно реализовать единственный класс Validator для валидации каждого из вложенных объектов в полнофункциональном объекте, но, возможно, лучше будет инкапсулировать логику валидации для каждого вложенного класса объектов в свою собственную реализацию Validator. Простым примером "полнофункционального" объекта может быть Customer, который состоит из двух свойств String (первое и второе имя) и сложного объекта Address. Объекты Address могут использоваться независимо от объектов Customer, поэтому был реализован отдельный AddressValidator. Если вы хотите, чтобы ваш CustomerValidator повторно использовал логику, содержащуюся в классе AddressValidator, не прибегая к копированию и вставке, можно внедрить зависимость в AddressValidator или создать экземпляр внутри вашего CustomerValidator, как показано в следующем примере:

Java
public class CustomerValidator implements Validator {
    private final Validator addressValidator;
    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException("The supplied [Validator] is " +
                "required and must not be null.");
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("The supplied [Validator] must " +
                "support the validation of [Address] instances.");
        }
        this.addressValidator = addressValidator;
    }
    /**
     * Этот валидатор проверяет экземпляры Customer, а также любые подклассы Customer.
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}
Kotlin
class CustomerValidator(private val addressValidator: Validator) : Validator {
    init {
        if (addressValidator == null) {
            throw IllegalArgumentException("The supplied [Validator] is required and must not be null.")
        }
        if (!addressValidator.supports(Address::class.java)) {
            throw IllegalArgumentException("The supplied [Validator] must support the validation of [Address] instances.")
        }
    }
    /*
    * Этот валидатор проверяет экземпляры Customer, а также любые подклассы Customer.
    */
    override fun supports(clazz: Class<>): Boolean {
        return Customer::class.java.isAssignableFrom(clazz)
    }
    override fun validate(target: Any, errors: Errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required")
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required")
        val customer = target as Customer
        try {
            errors.pushNestedPath("address")
            ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors)
        } finally {
            errors.popNestedPath()
        }
    }
}

Об ошибках валидации сообщается в объект Errors, переданный валидатору. В случае Spring Web MVC вы можете использовать тег <spring:bind/> для проверки сообщений об ошибках, но также можно проверить объект Errors самостоятельно. Более подробную информацию о методах, которые он предлагает, можно найти в javadoc.

Разрешение кода в сообщения об ошибках

Мы рассмотрели привязку к базе данных и валидацию. В этом разделе рассматривается вывод сообщений, соответствующих ошибкам валидации. В примере, показанном в предыдущем разделе, мы исключили поля name и age. Если нам нужно вывести сообщения об ошибках с помощью MessageSource, то сделать это можно, используя код ошибки, который мы указываем при отклонении поля ("name" и "age" в данном случае). Если вызвать (прямо или косвенно, например, с помощью класса ValidationUtils) rejectValue или один из других методов reject из интерфейса Errors, то базовая реализация не только зарегистрирует переданный код, но и зарегистрирует ряд дополнительных кодов ошибок. MessageCodesResolver определяет, какие коды ошибок регистрирует интерфейс Errors. По умолчанию используется DefaultMessageCodesResolver, который (например) не только регистрирует сообщение с указанным вами кодом, но и регистрирует сообщения, включающие имя поля, которое вы передали методу отклонений (reject). Так, если поле исключается с помощью rejectValue("age", "too.darn.old"), помимо кода too.darn.old, Spring также регистрирует too.darn.old.age и too.darn.old.age.int (первый включает имя поля, а второй - тип поля). Это сделано для удобства разработчиков при работе с сообщениями об ошибках.

Более подробную информацию о MessageCodesResolver и стратегии по умолчанию можно найти в javadoc по MessageCodesResolver и DefaultMessageCodesResolver, соответственно.