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

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.