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
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {}Formatter extends Printer and Parser :
@FunctionalInterface
public interface Printer<T> {
String print(T object, Locale locale);
}
@FunctionalInterface
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}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
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);
}
}Register and use it with FormattingConversionService :
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);Formatter Internals
Internally, a Formatter is adapted to a GenericConverter :
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
}2. Annotation‑Based Formatting
To bind an annotation to a formatter, implement AnnotationFormatterFactory :
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}Custom Annotation Formatter Example
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());
}
}
}Registration and Usage
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);
}
}3. FormatterRegistry
The FormatterRegistry SPI allows registration of printers, parsers, formatters, and annotation‑based formatters:
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);
}4. Configuring Type Conversion in Spring MVC
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// register custom formatters here
}
}Spring Boot provides a default FormattingConversionService bean:
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;
}
}
}End of article.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
