Mastering Spring Formatter: Custom Data Conversion in Spring Boot 2.7
This tutorial explains Spring's core.convert package, introduces the Formatter interface, and provides step‑by‑step examples of creating custom formatters and annotation‑based formatters in Spring Boot, including code snippets, registration, and test endpoints with output screenshots.
Environment: Springboot 2.7.18
1. Introduction
Spring core.convert package is a generic type conversion system that provides a unified ConversionService API and a strong‑typed Converter SPI for converting between types. For localized string formatting, Spring introduced Formatter starting with version 3.0.
Formatter
Formatter implements field formatting logic and is strongly typed. Interface definition:
<code>public interface Formatter<T> extends Printer<T>, Parser<T> { }</code>Formatter extends Printer and Parser . Their definitions are:
<code>// Convert from target type to String
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
// Parse from String to target type
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}</code>To create a custom Formatter, implement the interface for the desired type.
2. Practical Examples
2.1 Custom Formatter
Parse an input like "张三,30" into a Users object.
<code>public class Users {
private String name;
private Integer age;
// getters, setters
}</code> <code>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().length() == 0) {
return null;
}
Users user = new Users();
String[] values = text.split(",");
user.setName(values[0]);
user.setAge(Integer.parseInt(values[1]));
return user;
}
}</code>Register the formatter:
<code>@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new UsersFormatter());
}
}</code>Test endpoint:
<code>@GetMapping("/save")
public Object save(Users users) {
return users;
}</code>Result:
2.2 Annotation‑Based Formatter
Use AnnotationFormatterFactory to bind a custom annotation to a formatter.
<code>public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}</code>getFieldTypes: returns field types that can use the annotation.
getPrinter: returns a Printer for the annotated field.
getParser: returns a Parser for the annotated field.
Custom annotation:
<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface AgeFormat {}
</code>Formatter factory implementation:
<code>public final class AgeFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<AgeFormat> {
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));
}
}
}</code>Register the factory:
<code>@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatterForFieldAnnotation(new AgeFormatAnnotationFormatterFactory());
}
}</code>Apply the annotation to a field:
<code>public class Users {
private String name;
@AgeFormat
private Integer age;
}</code>Test endpoint:
<code>@GetMapping("/save2")
public Object save2(Users users) {
return users;
}</code>Result:
Another example using a custom UsersFormat annotation follows the same steps, with endpoint:
<code>@GetMapping("/save")
public Object save(@UsersFormat Users users) {
return users;
}</code>Result:
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.