In this example we are going to define our own formatter using spring Formatter SPI. Spring Formatter interface extends Printer and Parser interfaces:
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
We have to implement print() and parse() methods of Printer and Parser respectively.
Example
we are going to demonstrate how to format an object of Address instance in Customer object.
Creating Backing object classes
package com.logicbig.example;
import java.util.Objects;
public class Address {
private String street;
private String city;
private String county;
private String zipCode;
.............
}
Creating Custom Formatter
package com.logicbig.example;
import org.springframework.format.Formatter;
import java.text.ParseException;
import java.util.Locale;
public class AddressFormatter implements Formatter<Address> {
public enum Style {
FULL,
REGION
}
private Style style = Style.FULL;
public void setStyle(Style style) {
this.style = style;
}
@Override
public Address parse(String text,
Locale locale) throws ParseException {
if (text != null) {
String[] parts = text.split(",");
if (style == Style.FULL && parts.length == 4) {
Address address = new Address();
address.setStreet(parts[0].trim());
address.setCity(parts[1].trim());
address.setZipCode(parts[2].trim());
address.setCounty(parts[3].trim());
return address;
} else if (style == Style.REGION && parts.length == 3) {
Address address = new Address();
address.setCity(parts[0].trim());
address.setZipCode(parts[1].trim());
address.setCounty(parts[4].trim());
return address;
}
}
return null;
}
@Override
public String print(Address a,
Locale l) {
if (a == null) {
return "";
}
switch (style) {
case FULL:
return String.format(
l,
"%s, %s, %s, %s",
a.getStreet(),
a.getCity(),
a.getZipCode(),
a.getCounty());
case REGION:
return String.format(l,
"%s, %s, %s",
a.getCity(),
a.getZipCode(),
a.getCounty());
}
return a.toString();
}
}
Registering The Formatter
package com.logicbig.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableWebMvc
@Configuration
@ComponentScan
public class MyWebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/views/", ".jsp");
}
@Override
public void addFormatters(FormatterRegistry registry) {
AddressFormatter addressFormatter = new AddressFormatter();
addressFormatter.setStyle(AddressFormatter.Style.REGION);
registry.addFormatter(addressFormatter);
}
}
Alternatively we could have used @InitBinder approach, if we want to customize AddressFormatter per request basis.
Creating Controller
package com.logicbig.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequestMapping("/customers")
public class CustomerController {
@Autowired private CustomerDataService customerDataService;
@GetMapping
private String handleRequest(Model model) {
model.addAttribute("customerList", customerDataService.getAllUsers());
return "customers";
}
}
customer.jsp
<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<html>
<body>
<h3>Customer List </h3>
<table style="width:100%">
<c:forEach var="customer" items="${customerList}" varStatus="status">
<tr>
<td>
<spring:eval expression="customer.id" />
</td>
<td>
<spring:eval expression="customer.name" />
</td>
<td>
<spring:eval expression="customer.address" />
</td>
</tr>
</c:forEach>
</table>
</body>
</html>
Running Example
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
$ curl -s http://localhost:8080/spring-custom-formatter/customers
<html> <body> <h3>Customer List </h3>
<table style="width:100%">
<tr> <td> 1 </td> <td> John Smith </td> <td> New York, 10001, USA </td> </tr>
<tr> <td> 2 </td> <td> Jane Doe </td> <td> Los Angeles, 90001, USA </td> </tr>
<tr> <td> 3 </td> <td> Bob Johnson </td> <td> Chicago, 60601, USA </td> </tr>
</table>
</body> </html>
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult;
import org.springframework.web.context.WebApplicationContext;
import java.text.ParseException;
import java.util.List;
import java.util.Locale;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class CustomerControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvc;
@BeforeEach
public void setup() {
this.mockMvc = MockMvcTester.from(this.wac);
}
@Test
public void testUserController() throws ParseException {
MvcTestResult result = this.mockMvc.get().uri("/customers").exchange();
assertThat(result)
.hasStatusOk()
.hasViewName("customers")
.model()
.containsKey("customerList");
List<Customer> customerList =
(List<Customer>) result.getMvcResult().getModelAndView()
.getModel().get("customerList");
assertThat(customerList).hasSize(3);
Customer customer = customerList.get(1);
assertThat(customer.getId()).isEqualTo(2L);
assertThat(customer.getName()).isEqualTo("Jane Doe");
assertThat(customer.getAddress().getStreet()).isEqualTo("456 Oak Ave");
assertThat(customer.getAddress().getCity()).isEqualTo("Los Angeles");
assertThat(customer.getAddress().getCounty()).isEqualTo("USA");
assertThat(customer.getAddress().getZipCode()).isEqualTo("90001");
AddressFormatter addressFormatter = new AddressFormatter();
String formattedAddress = addressFormatter.print(customer.getAddress(),
Locale.getDefault());
assertThat(formattedAddress).isEqualTo(
"456 Oak Ave, Los Angeles, 90001, USA");
Address parsedAddress = addressFormatter.parse(formattedAddress,
Locale.getDefault());
assertThat(parsedAddress).isEqualTo(customer.getAddress());
}
}
mvn clean test -Dtest="CustomerControllerTest" Output$ mvn clean test -Dtest="CustomerControllerTest" [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for com.logicbig.example:spring-custom-formatter:war:1.0-SNAPSHOT [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-war-plugin is missing. @ line 43, column 21 [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed projects. [WARNING] [INFO] [INFO] ------------< com.logicbig.example:spring-custom-formatter >------------ [INFO] Building spring-custom-formatter 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ spring-custom-formatter --- [INFO] Deleting D:\example-projects\spring-mvc\spring-custom-formatter\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ spring-custom-formatter --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-custom-formatter\src\main\resources [INFO] [INFO] --- compiler:3.5.1:compile (default-compile) @ spring-custom-formatter --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 8 source files to D:\example-projects\spring-mvc\spring-custom-formatter\target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-custom-formatter --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-custom-formatter\src\test\resources [INFO] [INFO] --- compiler:3.5.1:testCompile (default-testCompile) @ spring-custom-formatter --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 1 source file to D:\example-projects\spring-mvc\spring-custom-formatter\target\test-classes [INFO] /D:/LogicBig/example-projects/spring-mvc/spring-custom-formatter/src/test/java/com/logicbig/example/CustomerControllerTest.java: D:\example-projects\spring-mvc\spring-custom-formatter\src\test\java\com\logicbig\example\CustomerControllerTest.java uses unchecked or unsafe operations. [INFO] /D:/LogicBig/example-projects/spring-mvc/spring-custom-formatter/src/test/java/com/logicbig/example/CustomerControllerTest.java: Recompile with -Xlint:unchecked for details. [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ spring-custom-formatter --- [INFO] Using auto detected provider org.apache.maven.surefire.junit4.JUnit4Provider [WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.CustomerControllerTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.647 s -- in com.logicbig.example.CustomerControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.174 s [INFO] Finished at: 2026-05-02T19:27:24+08:00 [INFO] ------------------------------------------------------------------------
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.6 (Spring TestContext Framework)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- jakarta.servlet.jsp.jstl 3.0.1 (Jakarta Standard Tag Library Implementation)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|