Mastering Spring’s Formatter SPI: Custom Type Conversion Made Easy
This article explains Spring's core.convert package, the ConversionService API, Formatter SPI, default and custom formatters, annotation-based formatting, and how to configure and use them in Spring MVC and Spring Boot applications.
Environment
Spring 5.3.12.RELEASE
Core.convert Package
The ConversionService API and strongly‑typed Converter SPI provide a unified type‑conversion system used by the Spring container, SpEL, and DataBinder to bind bean property values.
Formatter SPI
For typical client‑side scenarios (e.g., converting String to a target type and back, with localization), the core Converter SPI is insufficient. Spring 3 introduced the Formatter SPI as a robust alternative to PropertyEditor .
The ConversionService offers a common API for both Converter and Formatter SPIs.
1. Formatter SPI Interface
<code>package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {}
</code>Formatter extends Printer and Parser :
<code>@FunctionalInterface
public interface Printer<T> {
String print(T object, Locale locale);
}
@FunctionalInterface
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
</code>Default Formatter Implementations
Spring provides several built‑in formatters:
NumberStyleFormatter , CurrencyStyleFormatter , PercentStyleFormatter (use java.text.NumberFormat )
DateFormatter (uses java.text.DateFormat to format java.util.Date )
Custom Formatter Example
<code>public class StringToNumberFormatter implements Formatter<Number> {
@Override
public String print(Number object, Locale locale) {
return "结果是:" + object.toString();
}
@Override
public Number parse(String text, Locale locale) throws ParseException {
return NumberFormat.getInstance().parse(text);
}
}
</code>Register and use it with FormattingConversionService :
<code>FormattingConversionService fcs = new FormattingConversionService();
// Without a custom formatter the conversion would fail
fcs.addFormatterForFieldType(Number.class, new StringToNumberFormatter());
Number number = fcs.convert("100.5", Number.class);
System.out.println(number);
</code>Formatter Internals
Internally, a Formatter is adapted to a GenericConverter :
<code>public class FormattingConversionService extends GenericConversionService implements FormatterRegistry, EmbeddedValueResolverAware {
public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
addConverter(new PrinterConverter(fieldType, formatter, this));
addConverter(new ParserConverter(fieldType, formatter, this));
}
// ... inner classes PrinterConverter and ParserConverter implement GenericConverter
}
</code>2. Annotation‑Based Formatting
To bind an annotation to a formatter, implement AnnotationFormatterFactory :
<code>public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
</code>Custom Annotation Formatter Example
<code>public class StringToDateFormatter implements AnnotationFormatterFactory<DateFormatter> {
@Override
public Set<Class<?>> getFieldTypes() {
return new HashSet<>(Arrays.asList(Date.class));
}
@Override
public Printer<?> getPrinter(DateFormatter annotation, Class<?> fieldType) {
return getFormatter(annotation, fieldType);
}
@Override
public Parser<?> getParser(DateFormatter annotation, Class<?> fieldType) {
return getFormatter(annotation, fieldType);
}
private StringFormatter getFormatter(DateFormatter annotation, Class<?> fieldType) {
String pattern = annotation.value();
return new StringFormatter(pattern);
}
class StringFormatter implements Formatter<Date> {
private String pattern;
public StringFormatter(String pattern) { this.pattern = pattern; }
@Override
public String print(Date date, Locale locale) {
return DateTimeFormatter.ofPattern(pattern, locale).format(date.toInstant());
}
@Override
public Date parse(String text, Locale locale) throws ParseException {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, locale);
return Date.from(LocalDate.parse(text, formatter).atStartOfDay()
.atZone(ZoneId.systemDefault()).toInstant());
}
}
}
</code>Registration and Usage
<code>public class Main {
@DateFormatter("yyyy年MM月dd日")
private Date date;
public static void main(String[] args) throws Exception {
FormattingConversionService fcs = new FormattingConversionService();
fcs.addFormatterForFieldAnnotation(new StringToDateFormatter());
Main main = new Main();
Field field = main.getClass().getDeclaredField("date");
TypeDescriptor targetType = new TypeDescriptor(field);
Object result = fcs.convert("2022年01月21日", targetType);
System.out.println(result);
field.setAccessible(true);
field.set(main, result);
System.out.println(main.date);
}
}
</code>3. FormatterRegistry
The FormatterRegistry SPI allows registration of printers, parsers, formatters, and annotation‑based formatters:
<code>public interface FormatterRegistry extends ConverterRegistry {
void addPrinter(Printer<?> printer);
void addParser(Parser<?> parser);
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> factory);
}
</code>4. Configuring Type Conversion in Spring MVC
<code>@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// register custom formatters here
}
}
</code>Spring Boot provides a default FormattingConversionService bean:
<code>public class WebMvcAutoConfiguration {
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
@Bean
@Override
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(
new DateTimeFormatters()
.dateFormat(format.getDate())
.timeFormat(format.getTime())
.dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}
}
}
</code>End of article.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.