Backend Development 9 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring’s Formatter SPI: Custom Type Conversion Made Easy

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.

backendJavaspringannotationtype conversionConversionServiceFormatter
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.