Учебные материалы

Урок 4: Конвертация типов, форматирование значений и валидация полей

Этот урок освещает работу с Spring Framework и основан на оригинальной документации §7. Validation, Data Binding, and Type Conversion.

Что вы создадите

Вы создадите некоторое количество классов, в которых будет рассмотрена функциональность Spring Framework по части конвертации типов, форматирования значений и валидации полей классов.

Что вам потребуется

Настройка проекта

Добавьте в вашу Maven-конфигурацию следующее:

pom.xml

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
</dependency>

Введение

Spring Framework поддерживает Bean Validation 1.0 (JSR-303) и Bean Validation 1.1 (JSR-349). Валидация не обязательно должна быть привязана к web-слою, она должна быть проста для локализации и возможности подключения любого доступного валидатора, поэтому для этих целей Spring включает в себя интерфейс Validator, который пригоден к использованию в любом слое приложения. Связывание данных (Data binding) необходимо для динамического связывания пользовательского ввода с доменной моделью приложения. Для этих целей Spring предоставляет класс DataBinder. Поскольку они оба расположены в пакете org.springframework.validation, то вам не нужно тянуть за собой MVC фреймворк.

BeanWrapper - является фундаментальным понятием в Spring Framework и используется во многих местах. Кроме того, возможно вам и не потребуется использовать его напрямую. DataBinder и низкоуровневый BeanWrapper оба используют PropertyEditors для разбора и форматирования значений. Концепция PropertyEditors является частью JavaBeans спецификации. Начиная с Spring Framework 3, доступен пакет org.springframework.core.convert, который предоставляет различные инструменты для конвертации и форматирования значений как стандартных, так и пользовательских типов.

Манипуляция бина и BeanWrapper

Пакет org.springframework.beans придерживается стандарта JavaBeans, где JavaBeans - это просто класс с конструктором по умолчанию без аргументов, который следует соглашению по наименованию свойств и для которых имеются методы получения и установки(getter и setter методы).

Одним довольно важным классом является интерфейс BeanWrapper и его реализация BeanWrapperImpl, который предоставляет функциональность для установки и получения значений свойств, получения дескрипторов свойств, получение свойств по индексу и запрос на возможность чтения или записи свойства. Также он поддерживает вложенные свойства без ограничения на глубину вложенности. Обычно BeanWrapper не используется напрямую в коде приложения, а с помощью DataBinder и BeanFactory.

Установка и получение значений свойств

Для установки и получения значения свойства используются методы setPropertyValue(s) и getPropertyValue(s) соответственно, Например:

Person person = new Person();
BeanWrapper beanWrapper = new BeanWrapperImpl(person);
beanWrapper.setPropertyValue("name", "Вася");
PropertyValue propertyValue = new PropertyValue("age", "25");
beanWrapper.setPropertyValue(propertyValue);
logger.info("Person from Person - name: " + beanWrapper.getPropertyValue("name") + ", age: " + beanWrapper.getPropertyValue("age"));
logger.info("Person from Person - name: " + person.getName() + ", age: " + person.getAge());

В таблице ниже приведены примеры аргументов методов установки и получения значений свойств:

Выражение
Объяснение
name
Соответствует методам getName, isName() и setName()
account.name
Соответствует методам getAccount().getName() и getAccount().setName()
account[2]
Соответствует методам установки и получения значений свойства в соответствии с его реальным типом коллекции или массива. Индексация с 0
account[COMPANYNAME]
Соответствует методам установки и получения значений свойства из Map

PropertyEditor реализации

Spring использует концепцию PropertyEditor для эффективной конвертации между Object и String, например, для конвертации значения типа Date в строковое значение 2007-14-09. В таблице 7.2 перечислены реализации PropertyEditor, включенные в состав Spring Framework и которые автоматически определяются и применяются в вашей конфигурации.

При создании собственного PropertyEditor следует учитывать, что т.к. Spring использует java.beans.PropertyEditorManager для поиска редакторов, ваш редактор должен быть расположен в том же пакете, что и ваш конвертируемый класс, начинаться его названием и оканчиваться на Editor, например, у вас есть класс Foo в пакете com.package, для которого вам необходимо создать PropertyEditor. Для этого вам необходимо создать класс FooEditor в пакете com.package, который будет реализовывать интерфейс/класс PropertyEditor/PropertyEditorSupport. Если же вы хотите создать собственный PropertyEditor с другим названием и в другом пакете, вам необходимо его явно зарегистрировать несколькими способами:

  • Использовать метод registerCustomEditor(), если используется интерфейс ConfigurableBeanFactory
  • Создать бин в вашей конфигурации типа CustomEditorConfigurer, при настройке которого передать необходимые PropertyEditor
  • Создать собственную реализацию PropertyEditorRegistrar, в котором указать набор PropertyEditor, используемых в различных ситуациях(например, если вы используете Spring MVC, то можно зарегистрировать свои собственные DataBinder), а затем передать его в ваш бин CustomEditorConfigurer

Конвертация типов данных

Начиная с 3-й версии, Spring включает в свой состав пакет core.convert, который предоставляет систему контвертации типов. Система описывает SPI для реализации механизмов конвертирования и может быть альтернативой PropertyEditor. SPI прост и строго типизирован:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);

}

Для создания собственного конвертора или переопределения существующего в Spring для стандартных типов, достаточно реализовать данный интерфейс:

src/main/java/lessons/converter/StringToInteger.java

final class StringToInteger implements Converter<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source) + 1000;
    }

}

Если вам требуется более сложная реализация конвертации, то необходимо реализовать интерфейс GenericConverter, который поддерживает множество типов(т.к. менее типизирован) и предоставляет доступ информации о типе, аннотациям и сигнатуре полей.

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

В случаях, когда вам необходимо выполнять конвертацию при выполнении каких-либо условий, необходимо реализовать ConditionalGenericConverter. Хорошим примером его реализации может быть IdToEntityConverter, который конвертирует между идентификатором сущности в объект самой сущности при условии, что тип сущности имеет статический метод поиска, например findAccount(Long).

public interface ConditionalGenericConverter extends GenericConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

Для обеспечения унифицированного API конвертации типов используется ConversionService, а также его реализации, среди которых представлен многоцелевой GenericConversionService, подходящий для большинства случаев.

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

Для доступа через DI к ConversionService необходимо зарегистрировать бин типа org.springframework.context.support.ConversionServiceFactoryBean в вашей конфигурации и передать в него список ваших конвертеров.

src/main/java/lessons/ConversionConfiguration.java

@Configuration
public class ConversionConfiguration {

    @Bean
    ConversionServiceFactoryBean conversionService() {
        ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
        Set<Converter> converters = new HashSet<>();
        converters.add(new StringToInteger());
        bean.setConverters(converters);
        return bean;
    }
}

Для демострации работы запустите метод ValidationBindingConversionStarter#conversionTypes():

src/main/java/lessons//starter/ValidationBindingConversionStarter.java

public class ValidationBindingConversionStarter {
    //....

    private void conversionTypes() {
        ApplicationContext context = new AnnotationConfigApplicationContext(ConversionConfiguration.class);
        ConversionService service = (ConversionService) context.getBean(ConversionService.class);
        Integer convertedObject = service.convert("1", Integer.class);
        logger.info("Integer value: " + convertedObject); //Integer value: 1001
    }
}

На консоли мы увидели, что наш конвертер сработал и добавил, сконвертировал строку "1" в число и добавил 1000.

Форматирование полей

При разрабатке приложения, обычно существуют требования к окружению, где будет работать приложение. К примеру, если это web-приложение, то в таком случае в основном требуется конвертировать значения какого-либо типа в строки и обратно результаты ввода от пользователя в типы, используемые приложением в процессе работы. Также, во многих случаях требуется учитывать локаль пользователя, например при отображении даты или суммы чего-либо. Для этих целей в Spring существует пакет org.springframework.format и основные интерфейсы этого SPI:

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}
package org.springframework.format;

public interface Printer<T> {
    String print(T fieldValue, Locale locale);
}
package org.springframework.format;

public interface Parser<T> {
    T parse(String clientValue, Locale locale) throws ParseException;
}

Для реализации собственного форматтера достаточно реализовать интерфейс Formatter.

src/main/java/lessons/formatter/IntegerFormatter.java

public class IntegerFormatter implements Formatter<Integer> {

    @Override
    public Integer parse(String text, Locale locale) throws ParseException {
        return Integer.valueOf(text.split(" ")[1]);
    }

    @Override
    public String print(Integer integer, Locale locale) {
        return "Integer: " + integer;
    }
}

Также возможно создать свою аннотацию для форматирования, чтобы применять её к какому-либо полю класса модели. Для этого вам необходимо реализовать интерфейс AnnotationFormatterFactory. Ниже представлена такая реализация Spring аннотации NumberFormat и пример её применения:

public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory<NumberFormat> {

    public Set<Class<?>> getFieldTypes() {
        return new HashSet<Class<?>>(asList(new Class<?>[] {
            Short.class, Integer.class, Long.class, Float.class,
            Double.class, BigDecimal.class, BigInteger.class }));
    }

    public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    private Formatter<Number> configureFormatterFrom(NumberFormat annotation,
            Class<?> fieldType) {
        if (!annotation.pattern().isEmpty()) {
            return new NumberFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyFormatter();
            } else {
                return new NumberFormatter();
            }
        }
    }
}
public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;

}

Также как и конверторы, форматтеры можно зарегистрировать в вашей конфигурации, только вместо ConversionServiceFactoryBean необходимо использовать FormattingConversionServiceFactoryBean, у которого есть аналогичный метод setFormatters().

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

Spring предоставляет интерфейс Validator для валидации объектов, а также объекта типа Errors, который содержит в себе результаты проверки. Для начала создайте класс, объекты которого мы будем проверять:

src/main/java/lessons/entity/Person.java

public class Person {

    private String name;
    private int age;

    // getters and setters...
}

Теперь необходимо создать класс проверок, реализующий методы интерфейса Validator:

src/main/java/lessons/validator/PersonValidator.java

public class PersonValidator implements Validator {

    //Проверяет, применима данная проверка классу Person
    @Override
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");
        Person p = (Person) target;
        if (p.getAge() < 0) {
            errors.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            errors.rejectValue("age", "too.darn.old");
        }
    }
}

Как видите, статический метод rejectIfEmpty(...) класса ValidationUtils использован для отклонения свойства name, если его значение равно null или пусто. В то время как вы можете реализовать единый класс для валидации вложенных объектов, лучше реализовать для каждого из типов вложенных объектов валидатор и вызывать их в процессе валидации общего класса методом ValidationUtils.invokeValidator(...).

Как вы могли заметить, в каждый из методов reject* последним аргументом передаются строковые значения кодов ошибок, такие как name.empty и др., которые используются для доступа к значениям подробного описания ошибки. Сами сообщения по этим кодам находятся в тех же properties-файлах и их можно получить через MessageSource, как это было показано в ранее опубликованном уроке.

Для демонстрации примера воспользуемся классом DataBinder, который сохраняет в себе информацию об ошибках в процессе валидации.

src/main/java/lessons/starter/ValidationBindingConversionStarter.java

public class ValidationBindingConversionStarter {

    //....


    private void personValidate() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("ValidationMessages");

        Person person = new Person();
        person.setAge(150);
        PersonValidator personValidator = new PersonValidator(new AddressValidator());
        DataBinder dataBinder = new DataBinder(person);
        dataBinder.setValidator(personValidator);
        dataBinder.validate();
        logger.info("Count errors: " + dataBinder.getBindingResult().getErrorCount());

        ObjectError error = dataBinder.getBindingResult().getAllErrors().get(0);
        logger.info("Error \"" +error.getCode() + "\": " + messageSource.getMessage(error.getCode(), null, Locale.ROOT));

        error = dataBinder.getBindingResult().getAllErrors().get(1);
        MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
        String[] codes = codesResolver.resolveMessageCodes(error.getCode(), "age"); //указываем, по какому полю объекта мы хотим получить коды ошибок
        logger.info("Error \"" +codes[0] + "\": " + messageSource.getMessage(codes[0], null, Locale.ROOT));
    }
}

Итог

В данном уроке вы познакомились с механизмами конвертации типов и их форматирования, а также с основами валидации данных, поддерживаемой Spring Framework. Более подробная информация о применении JSR-303/JSR-349 аннотации и классов будет изложена в уроках по части Spring MVC.

comments powered by Disqus