Есть свои плюсы и минусы в том, чтобы рассматриваться валидацию как бизнес-логику, и 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
.
Рассмотрим следующий пример небольшого объекта данных:
public class Person {
private String name;
private int age;
// обычные геттеры и сеттеры...
}
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
:
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");
}
}
}
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
, как показано в следующем примере:
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();
}
}
}
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
, соответственно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ