Преамбула
В статье я хотел бы рассмотреть написание собственных конвертеров типов и форматтеров полей Spring Framework (в том числе с использованием аннотаций).
Статья написана by junior for junior, поэтому прошу отнестись к изложенному ниже с изрядной долей снисхождения 🙂
Конвертер — конвертирует один тип данных в другой
Форматтер — конвертирует только тип String в какой-то другой тип (и обратно в String)
Неплохое объяснение со stackoverflow
Рассматривать работу конвертеров и форматтеров будем на совершенно банальном примере — в @RequestParam контроллера приходит строка с датой или временем и надо ее сконвертировать в LocalDate или LocalTime. На месте строки с датой/временем может быть все, что угодно, например, описание сущности базы данных. Но думаю это усложнило бы пример, поэтому для простоты пускай остаются дата и время.
@RestController @RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE) public class SomeRestController { // autowired services and others // ... @Override @GetMapping("/filter") public List<SomeDto> getFiltered( @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime startTime, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime endTime) { // ... } }
В приведенной выше реализации все конвертируется с помощью аннотаций форматирования Spring Framework. Для тренировки откажемся от использования стандартных аннотаций и напишем свои собственные конвертеры, потом форматтеры и аннотации.
Конфигурирование буду указывать только в xml, как самое мутное.
Конвертер
В самом простом случае для создания собственного конвертера надо создать класс, имплементирующий интерфейс Converter<S, T>. S — тип источника данных, T — тип данных, который должен быть получен в результате конвертации.
В нашем случае источником выступает тип String, а результирующими типами будут LocalDate и LocalTime. Таким образом, классов конвертеров у нас будет два. Вариант реализации:
import org.springframework.core.convert.converter.Converter; import java.time.LocalDate; import java.time.format.DateTimeFormatter; public class StringToLocalDateConverter implements Converter<String, LocalDate> { private String datePattern = "yyyy-MM-dd"; public String getDatePattern() { return datePattern; } public void setDatePattern(String datePattern) { this.datePattern = datePattern; } @Override public LocalDate convert(String dateString) { return LocalDate.parse(dateString, DateTimeFormatter.ofPattern(datePattern)); } }
import org.springframework.core.convert.converter.Converter; import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class StringToLocalTimeConverter implements Converter<String, LocalTime> { private String timePattern = "HH:mm"; public String getTimePattern() { return timePattern; } public void setTimePattern(String timePattern) { this.timePattern = timePattern; } @Override public LocalTime convert(String timeString) { return LocalTime.parse(timeString, DateTimeFormatter.ofPattern(timePattern)); } }
Теперь надо рассказать Спрингу про наши конвертеры. Для этого надо создать бины конвертеров, зарегистрировать экземпляр класса ConversionService с именем conversionService и добавить в него наши конвертеры.
<bean id="stringToLocalTimeConverter" class="ru.jsft.util.converter.StringToLocalTimeConverter"/> <bean id="stringToLocalDateConverter" class="ru.jsft.util.converter.StringToLocalDateConverter"/> <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <ref bean="stringToLocalDateConverter"/> <ref bean="stringToLocalTimeConverter"/> </set> </property> </bean>
Остался последний штрих — сказать Spring MVC, что надо использовать наш ConversionService
в xml-конфигурации mvc:annotation-driven необходимо дополнить указанием conversionService
<mvc:annotation-driven conversion-service="conversionService"/>
Теперь можно переписать метод контроллера
@RestController @RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE) public class SomeRestController { // autowired services and others // ... @Override @GetMapping("/filter") public List<SomeDto> getFiltered( @RequestParam(required = false) LocalDate startDate, @RequestParam(required = false) LocalTime startTime, @RequestParam(required = false) LocalDate endDate, @RequestParam(required = false) LocalTime endTime) { // ... } }
Конспект
-
Создали классы конвертеров, имплементирующие Converter<S, T>
-
Создали бины классов (через xml, аннотацию или конфигурационный класс)
-
Создали через конфигурацию бин conversionService и указали в нем наши конвертеры
-
Указали Spring MVC, что надо пользоваться нашим conversionService
Форматтер
Теперь проделаем то же самое, только через форматирование строк.
Необходимо создать класс, имплементирующий интерфейс Formatter< T > где T — тип данных, которые будут получены в результате форматирования входящей строки. Напомню — форматтер работает только со String, поэтому входящий тип данных не нужен.
Вариант реализации
import org.springframework.format.Formatter; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Locale; public class CustomDateFormatter implements Formatter<LocalDate> { @Override public LocalDate parse(String text, Locale locale) { return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd")); } @Override public String print(LocalDate localDate, Locale locale) { return localDate.toString(); } }
import org.springframework.format.Formatter; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Locale; public class CustomTimeFormatter implements Formatter<LocalTime> { @Override public LocalTime parse(String text, Locale locale) { return LocalTime.parse(text, DateTimeFormatter.ofPattern("HH:mm")); } @Override public String print(LocalTime localTime, Locale locale) { return localTime.toString(); } }
parse() — метод возвращает конвертированное из String значение
print() — в этом методе осуществляется обратная конвертация, значение в String. Здесь просто отконвертируем в строку штатными средствами.
Расскажем Спрингу про форматтеры. Укажем Spring MVC использовать наш conversionService.
Обратите внимание — для бина conversionService используется класс, отличный от использованного для конвертеров. При использовании этого класса, кстати, можно кроме форматтеров добавить и конвертеры. См. документацию.
<bean id="customDateFormatter" class="ru.jsft.util.formatter.CustomDateFormatter"/> <bean id="customTimeFormatter" class="ru.jsft.util.formatter.CustomTimeFormatter"/> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatters"> <set> <ref bean="customDateFormatter"/> <ref bean="customTimeFormatter"/> </set> </property> </bean> <mvc:annotation-driven conversion-service="conversionService"/>
Метод контроллера будет выглядеть так же, как и в случае с использованием конвертеров
@RestController @RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE) public class SomeRestController { // autowired services and others // ... @Override @GetMapping("/filter") public List<SomeDto> getFiltered( @RequestParam(required = false) LocalDate startDate, @RequestParam(required = false) LocalTime startTime, @RequestParam(required = false) LocalDate endDate, @RequestParam(required = false) LocalTime endTime) { // ... } }
Конспект
-
Создали классы форматтеров, имплементирующие Formatter< T >
-
Создали бины классов (через xml, аннотацию или конфигурационный класс)
-
Создали через конфигурацию бин conversionService и указали в нем наши форматтеры
-
Указали Spring MVC, что надо пользоваться нашей conversionService
Форматирование с использованием аннотаций
Теперь давайте сделаем так, чтобы форматирование происходило только там, где мы укажем соответствующие аннотации. В случае с предыдущими вариантами реализации применение конвертеров/контроллеров происходит по всему коду.
Я хочу, чтобы аннотация с помощью параметра могла применяться как для конвертации в LocalDate, так и в LocalTime. То есть не делать две разные аннотации, а сделать одну, но с уточняющим параметром (как это реализовано в случае @DateTimeFormat(iso = DateTimeFormat.ISO.DATE))
Первым делом создадим интерфейс для новой аннотации. В интерфейсе объявим параметр, в котором будет храниться указание, во что надо конвертировать — в LocalDate или в LocalTime. В качестве типа параметра укажем объявленный тут же enum. Значение по умолчанию для параметра я специально не делал.
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface CustomDateTimeFormat { Type type(); public enum Type { DATE, TIME } }
Теперь надо привязать аннотацию к форматтеру. Это делается с помощью реализации класса, имплементирующего интерфейс AnnotationFormatterFactory< A extends Annotation >
import org.springframework.format.AnnotationFormatterFactory; import org.springframework.format.Formatter; import org.springframework.format.Parser; import org.springframework.format.Printer; import java.time.LocalDate; import java.time.LocalTime; import java.util.HashSet; import java.util.List; import java.util.Set; public class CustomDateTimeFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<CustomDateTimeFormat> { @Override public Set<Class<?>> getFieldTypes() { return new HashSet<>(List.of(LocalDate.class, LocalTime.class)); } @Override public Printer<?> getPrinter(CustomDateTimeFormat annotation, Class<?> fieldType) { return getFormatter(annotation, fieldType); } @Override public Parser<?> getParser(CustomDateTimeFormat annotation, Class<?> fieldType) { return getFormatter(annotation, fieldType); } private Formatter<?> getFormatter(CustomDateTimeFormat annotation, Class<?> fieldType) { switch (annotation.type()) { case DATE -> { return new CustomDateFormatter(); } case TIME -> { return new CustomTimeFormatter(); } } return null; } }
Тут давайте разберемся поподробнее с переопределенными методами интерфейса AnnotationFormatterFactory.
getFieldTypes() — метод возвращает список классов-типов данных, с которыми будет использоваться аннотация. Обратите внимание, если вы аннотируете тип, которого не будет в этом списке, то, несмотря на наличие аннотации, ничего не произойдет.
getPrinter() и getParser() — первый возвращает Printer для вывода значения аннотированного поля, второй возвращает Parser для разбора полученного значения. В обоих случаях у нас код будет одинаковый. Идея в том, что если у аннотации стоит параметр type == DATE, то вернется уже написанный нами ранее экземпляр класса CustomDateFormatter. А для type == TIME вернется экземпляр CustomTimeFormatter соответственно. Таким образом мы добиваемся того, что аннотация одна, а возвращаемый результат — разный.
Ну вот, теперь осталось познакомить Спринг с нашей привязкой аннотации. Не забудем указать Spring MVC наш conversionService.
<bean id="customDateTimeFormatAnnotationFormatterFactory" class="ru.jsft.util.formatter.CustomDateTimeFormatAnnotationFormatterFactory"/> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatters"> <set> <ref bean="customDateTimeFormatAnnotationFormatterFactory"/> </set> </property> </bean> <mvc:annotation-driven conversion-service="conversionService"/>
Теперь изменим метод контроллера, чтобы входящие параметры форматировались с использованием аннотаций
@RestController @RequestMapping(value = "/api/v1/some", produces = MediaType.APPLICATION_JSON_VALUE) public class SomeRestController { // autowired services and others // ... @Override @GetMapping("/filter") public List<SomeDto> getFiltered( @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.DATE) LocalDate startDate, @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.TIME) LocalTime startTime, @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.DATE) LocalDate endDate, @RequestParam(required = false) @CustomDateTimeFormat(type = CustomDateTimeFormat.Type.TIME) LocalTime endTime) { // ... } }
Конспект
-
Классы форматтеров у нас уже были
-
Создали интерфейс-аннотацию
-
Создали класс-привязку аннотации к форматтеру
-
Создали через конфигурацию бин conversionService и указали в нем класс, привязывающий аннотации к форматтерам
-
Указали Spring MVC, что надо пользоваться нашей conversionService
Заключение
Искренне надеюсь, что эта статья окажется полезной тем, кто только начинает разбираться с темой конвертирования типов и форматирования полей с помощью Spring Framework. Я понимаю, что за бортом осталась неразобранной значительная часть информации по этим темам. Но у меня не было цели перевести документацию к Спрингу или создать некое всеобъемлющее руководство. Эта статья — лишь способ помочь сдвинуться с мертвой точки.
Спасибо, что дочитали до конца.
ссылка на оригинал статьи https://habr.com/ru/articles/703402/
Добавить комментарий