Урок 4: Конвертация типов, форматирование значений и валидация полей
Этот урок освещает работу с Spring Framework и основан на оригинальной документации §7. Validation, Data Binding, and Type Conversion.
Что вы создадите
Вы создадите некоторое количество классов, в которых будет рассмотрена функциональность Spring Framework по части конвертации типов, форматирования значений и валидации полей классов.
Что вам потребуется
- Любимый текстовый редактор или IDE
- JDK 7 и выше
- Maven 3.0+
- Исходный код предыдущего урока
Настройка проекта
Добавьте в вашу 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());
В таблице ниже приведены примеры аргументов методов установки и получения значений свойств:
getName
, isName()
и setName()
getAccount().getName()
и getAccount().setName()
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