Mastering Custom Formatter SPI in Spring Boot 2.6.12
This guide explains how to use Spring's Formatter SPI and AnnotationFormatterFactory to create, register, and test custom type converters and annotation‑based formatters in a Spring Boot 2.6.12 application, complete with code samples and endpoint demonstrations.
Environment: Spring Boot 2.6.12
Formatter SPI Overview
Spring provides a unified type‑conversion API through ConversionService , which includes two SPI extensions: a converter SPI for generic type conversion (e.g., between java.util.Date and Long ) and a formatter SPI for parsing and printing localized field values.
Formatter Interface
<code>package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {}
</code>Printer and Parser Interfaces
<code>public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}
</code>Creating a Custom Formatter
Implement the Formatter interface for the target type (e.g., Users ), providing print and parse methods. Ensure the implementation is thread‑safe and throws ParseException or IllegalArgumentException on failure.
<code>public class Users {
private String name;
private Integer age;
}
public class UsersFormatter implements Formatter<Users> {
@Override
public String print(Users object, Locale locale) {
if (Objects.isNull(object)) {
return "";
}
return "【name = " + object.getName() + ", age = " + object.getAge() + "】";
}
@Override
public Users parse(String text, Locale locale) throws ParseException {
if (text == null || text.trim().isEmpty()) {
return null;
}
Users user = new Users();
// simple split, no validation
String[] values = text.split(",");
user.setName(values[0]);
user.setAge(Integer.parseInt(values[1]));
return user;
}
}
</code>Registering the Formatter
<code>@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new UsersFormatter());
}
}
</code>Testing the Endpoint
<code>@GetMapping("/save")
public Object save(Users users) {
return users;
}
</code>Calling /save?name=张三,age=30 returns the parsed Users object. The resulting output is shown below:
Annotation‑Based Formatter
To bind a formatter to a specific annotation, 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 and Formatter
<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface AgeFormat {}
public final class AgeFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<AgeFormat> {
@Override
public Set<Class<?>> getFieldTypes() {
Set<Class<?>> types = new HashSet<>();
types.add(Integer.class);
return types;
}
@Override
public Printer<Integer> getPrinter(AgeFormat annotation, Class<?> fieldType) {
return new AgeFormatter();
}
@Override
public Parser<Integer> getParser(AgeFormat annotation, Class<?> fieldType) {
return new AgeFormatter();
}
private class AgeFormatter implements Formatter<Integer> {
@Override
public String print(Integer object, Locale locale) {
return object == null ? "" : object.toString();
}
@Override
public Integer parse(String text, Locale locale) throws ParseException {
if (text == null || text.trim().isEmpty()) {
return -1;
}
return Integer.parseInt(text.substring(1)); // skip leading 's'
}
}
}
</code>Registering Annotation Formatter
<code>@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatterForFieldAnnotation(new AgeFormatAnnotationFormatterFactory());
}
}
</code>Applying the Annotation
<code>public class Users {
private String name;
@AgeFormat
private Integer age;
}
</code>Testing the Annotated Endpoint
<code>@GetMapping("/save2")
public Object save2(Users users) {
return users;
}
</code>Resulting output (note the leading ‘s’ in the age value) is shown below:
Formatter on Method Parameters
<code>public final class UsersFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<UsersFormat> {
@Override
public Set<Class<?>> getFieldTypes() {
Set<Class<?>> types = new HashSet<>();
types.add(Users.class);
return types;
}
@Override
public Printer<?> getPrinter(UsersFormat annotation, Class<?> fieldType) {
return new UsersFormatter();
}
@Override
public Parser<?> getParser(UsersFormat annotation, Class<?> fieldType) {
return new UsersFormatter();
}
}
</code> <code>@GetMapping("/save3")
public Object save3(@UsersFormat Users users) {
return users;
}
</code>Output of this endpoint is illustrated below:
These examples demonstrate how to create both simple and annotation‑driven formatters, register them with Spring MVC, and verify their behavior through REST endpoints.
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.