Є свої плюси та мінуси в тому, щоб розглядати валідацію як
бізнес-логіку, і 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 " +
"потрібна і не буде 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
відповідно.