Spring Type Conversion and Field Formatting ā writing the first converter or formatter
Preamble
In the article, I would like to consider writing your own Spring Framework type converters and field formatters (including using annotations).
The article was written by junior for junior, so please treat the following with a fair amount of indulgence š
Converter ā converts one data type to another
Formatter ā convert only type String to some other type
Nice explanation from stackoverflow
We will consider the work of converters and formatters using a completely banal example ā a string with a date or time comes to @RequestParam of the controller and you need to convert it to LocalDate or LocalTime. In place of the string with the date / time, there can be anything, for example, a description of the database entity. But I think this would complicate the example, so for simplicity, let the date and time remain.
@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) {
// ...
}
}
In the implementation above, everything is converted using Spring Frameworkās formatting annotations. For practice, we will abandon the use of standard annotations and write our own converters, then formatters and annotations.
I will specify the configuration only in xml, as the most muddy.
Converter
In the simplest case, to create your own converter, you need to create a class that implements the Converter interface. S ā data source type, T ā data type to be obtained as a result of conversion.
In our case, the source is a String type, and the resulting types are LocalDate and LocalTime. Thus, we will have two converter classes. Implementation option:
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));
}
}
Now we need to tell Spring about our converters. To do this, we need to create converter beans, register an instance of the ConversionService class with the name conversionService and add our converters to it.
<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>
The final touch is to tell Spring MVC to use our ConversionService
mvc:annotation-driven xml-configuration must be supplemented with the conversionService indication
<mvc:annotation-driven conversion-service="conversionService"/>
Now you can rewrite the controller method
@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) {
// ...
}
}
Abstract
-
Created converter classes that implement Converter
-
Created class beans (via xml, annotation or configuration class)
-
We created a conversionService bean through the configuration and specified our converters in it
-
Tell Spring MVC to use our conversionService
Formatter
Now letās do the same, only through string formatting.
It is necessary to create a class that implements the Formatter< T > interface, where T is the type of data that will be obtained as a result of formatting the input string. Let me remind you that the formatter only works with String, so the input data type is not needed.
Implementation option
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.format(DateTimeFormatter.ofPattern("yyyy-MM-dd");
}
}
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.format(DateTimeFormatter.ofPattern("HH:mm"));
}
}
Letās tell Spring about formatters. Letās tell Spring MVC to use our conversionService.
Note that the conversionService bean uses a different class than the one used for the converters. When using this class, by the way, you can add converters in addition to formatters. See documentation.
<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"/>
The controller method will look the same as in the case of using converters
@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) {
// ...
}
}
Abstract
-
Created formatter classes that implement Formatter< T >
-
Created class beans (via xml, annotation or configuration class)
-
We created a conversionService bean through the configuration and specified our formatters in it
-
Tell Spring MVC to use our conversionService
Formatting with Annotations
Now letās make it so that the formatting happens only where we specify the appropriate annotations. In the case of the previous implementation options, the use of converters / controllers occurs throughout the code.
I want the parameter annotation to be able to be applied to both convert to LocalDate and LocalTime. That is, do not make two different annotations, but make one, but with a refinement parameter (as it is implemented in the case @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
)
First of all, letās create an interface for the new annotation. In the interface, we will declare a parameter that will store an indication of what to convert to ā in LocalDate or in LocalTime. As the type of the parameter, we specify the enum declared right there. I didnāt set the default value for the parameter.
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
}
}
Now we need to bind the annotation to the formatter. This is done by implementing a class that implements the AnnotationFormatterFactory< A extends Annotation > interface.
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;
}
}
Here, letās take a closer look at the overridden methods of the AnnotationFormatterFactory interface.
getFieldType() ā the method returns a list of data type classes with which the annotation will be used. Note that if you annotate a type that is not in this list, then nothing will happen despite the presence of the annotation.
getPrinter() and getParser() ā the first returns Printer to display the value of the annotated field, the second returns Parser to parse the received value. In both cases, our code will be the same. The idea is that if the annotation has the type == DATE parameter, then the instance of the CustomDateFormatter class already written by us will be returned. And for type == TIME, an instance of CustomTimeFormatter will be returned accordingly. Thus, we achieve that the annotation is one, and the returned result is different.
Well, now it remains to introduce Spring to our annotation binding. Donāt forget to point Spring MVC to our 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"/>
Now letās change the controller method so that the incoming parameters are formatted using annotations
@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) {
// ...
}
}
Abstract
-
We already had formatter classes
-
Created interface annotation
-
Created an annotation binding class to the formatter
-
We created a conversionService bean through the configuration and specified in it a class that binds annotations to formatters
-
Tell Spring MVC to use our conversionService
Conclusion
I sincerely hope that this article will be useful to those who are just starting to deal with the topic of type conversion and field formatting using the Spring Framework. I understand that a significant part of the information on these topics has not been sorted out. But it wasnāt my intention to translate the documentation for Spring or create some sort of comprehensive guide. This article is just a way to help get the ball rolling.
Thank you for reading to the end.