SPRING-SOURCE.RU

Глава 5. Проверка, data-binding (связывание данных), BeanWrapper и PropertyEditors

5.1. Введение

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

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

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

Spring использует PropertyEditors повсюду. Понятие PropertyEditors - это часть JavaBean спецификации. Так же как и в случае с BeanWrapper, объяснять использование PropertyEditors лучше всего в этой главе, так как он тесно связан с BeanWrapper и DataBinder.

5.2. Проверка с использованием Spring интерфейса Validator

Особенность интерфейса Validator заключается в том, что он проверяет объекты. Интерфейс Validator работает, используя объект Errors и при проверке, validator может сообщать об ошибках в Errors объект.

Давайте рассмотрим маленький объект данных:

public class Person {

  private String name;
  private int age;

  // the usual getters and setters...
}
		

Мы собираемся создать проверку для класса Person, реализуя два метода в org.springframework.validation.Validator интерфейсе:

Реализация интерфейса Validator является очень простой задачей, особенно, когда вы знаете вспомогательный класс ValidationUtils, который также поставляется вместе с Spring Framework.

public class PersonValidator implements Validator {
    
    /**
    * This Validator validates just Person instances
    */
    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");
        }
    }
}		
		

Как вы можете видеть, static rejectIfEmpty(..) метод, в классе ValidationUtils, используется для отклонения свойства "name" в случае, если оно null или пустая строка. Для получения дополнительной информации по ValidationUtils классу обращайтесь к Javadoc документации.

Хотя, конечно, можно реализовать единственный Validator класс для проверки каждого вложенного объекта в rich объект, это может быть неплохой инкапсуляцией логики проверки для каждого вложенного класса объекта в его собственной реализации Validator. Простой пример "rich" объекта будет Customer, который имеет два String свойства (имя и фамилия) и сложный Adress объект. Объект Adress может использоваться независимо от объекта Customer и имеет индивидуальный AddressValidator. Если вы хотите чтобы CustomerValidator мог повторно использовать логику, содержащейся в AddressValidator классе без copy-and- paste, вы можете сделать иньекцию зависимостей 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;
    }

    /**
    * This Validator validates Customer instances, and any subclasses of Customer too
    */
    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();
        }
    }
}
		

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

Коды к сообщениями об ошибке

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

Дополнительную информацию о MessageCodesResolver можно найти в Javadoc.

Бин манипуляция и BeanWrapper

Пакет org.springframework.beans придерживается JavaBeans стандарта предоставляемого Sun. JavaBean это простой класс с конструктором без аргументов по умолчанию. JavaBean следует правилам формирования имен, где свойство, например, c именем bingoMadness имеет setBingoMadness(..) и getBingoMadness(..) методы. Для более подробной информации смотрите официальную документацию по JavaBeans на java.sun.com.

Очень важный класс в bean пакете, это BeanWrapper интерфейс и его реализация BeanWrapperImpl. BeanWrapper предлагает поддержку для вложенных свойств, включая установку sub-свойств на безлимитной глубине. BeanWrapper предоставляет функциональность для записи и чтения значений свойств, получает дескрипторы свойств и опрашивает свойства для определения, доступны ли свойства для чтения или записи. Также BeanWrapper поддерживает возможность для дополнения стандартных JavaBeans классами PropertyChangeListeners и VetoableChangeListeners без необходимости наличия поддерживающего кода в целевом классе. Так же BeanWrapper поддерживает установку индексируемых свойств. BeanWrapper обычно не используется непосредственно в коде приложения, а используется контейнером приложения.

Setting и getting основного и вложенного свойства

Класс BeanWrapper направляет класс-бин для совершения действий, таких как чтение и запись свойств. Рассмотрим процедуры чтения и записи базовых и вложенных свойств. Чтение и запись происходит с использованием методов setPropertyValue(s) и getPropertyValue().

Для указания свойств необходимо учитывать соглашения, представленные в табл. 1.

Примеры свойств для записи-чтения в классе BeanWrapper

Выражение Действие
name Указывает на свойство name, соответствующее методам getName() или isName() и setName()
account.name Указывает на вложенное свойство name свойства account, соответствующее например методам getAccount(). setName() или getAccount().getName()
account[2] Указывает на 3-й элемент индеексируемого свойства account. Индексируемые свойства могут быть типа array, list или других известных JDK коллекций.
аccount[COMPANYNAME] Указывает на значение карты, индексируемого ключом COMPANY-NAME Map-свойства account

Ниже вы можете найти примеры работы с BeanWrapper для set и get свойств.

Давайте обсудим следующие два класса:

public class Company {
    private String name;
    private Employee managingDirector;

    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Employee getManagingDirector() {
        return this.managingDirector;
    }
    public void setManagingDirector(Employee managingDirector) {
        this.managingDirector = managingDirector;
    }
}
		

public class Employee {
    private String name;
    private float salary;

    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public float getSalary() {
        return salary;
    }
    public void setSalary(float salary) {
        this.salary = salary;
    }
}
		

Следующий код показывает несколько примеров того, как получать и манипулировать некоторыми свойствами экземпляров Companies и Employees:

BeanWrapper company = BeanWrapperImpl(new Company());
// устанавливаем имя компании..
company.setPropertyValue("name", "Some Company Inc.");
// ... это может быть сделано и вот так:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// хорошо, давайте создадим директора и свяжем его с компанией:
BeanWrapper jim = BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// получение зарплаты managingDirector через компанию
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
		

Продолжение следует. Далее описываются PropertyEditor