Spring Type Conversion and Field Formatting — пишем первый конвертер или форматтер

от автора

Преамбула

В статье я хотел бы рассмотреть написание собственных конвертеров типов и форматтеров полей 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) {              // ...     }   }

Конспект

  1. Создали классы конвертеров, имплементирующие Converter<S, T>

  2. Создали бины классов (через xml, аннотацию или конфигурационный класс)

  3. Создали через конфигурацию бин conversionService и указали в нем наши конвертеры

  4. Указали 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) {              // ...     }   } 

Конспект

  1. Создали классы форматтеров, имплементирующие Formatter< T >

  2. Создали бины классов (через xml, аннотацию или конфигурационный класс)

  3. Создали через конфигурацию бин conversionService и указали в нем наши форматтеры

  4. Указали 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) {              // ...     }   }

Конспект

  1. Классы форматтеров у нас уже были

  2. Создали интерфейс-аннотацию

  3. Создали класс-привязку аннотации к форматтеру

  4. Создали через конфигурацию бин conversionService и указали в нем класс, привязывающий аннотации к форматтерам

  5. Указали Spring MVC, что надо пользоваться нашей conversionService

Заключение

Искренне надеюсь, что эта статья окажется полезной тем, кто только начинает разбираться с темой конвертирования типов и форматирования полей с помощью Spring Framework. Я понимаю, что за бортом осталась неразобранной значительная часть информации по этим темам. Но у меня не было цели перевести документацию к Спрингу или создать некое всеобъемлющее руководство. Эта статья — лишь способ помочь сдвинуться с мертвой точки.

Спасибо, что дочитали до конца.


ссылка на оригинал статьи https://habr.com/ru/articles/703402/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *