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