Starting Spring 5, data binding can work with immutable classes now. Previously there must be a no argument constructor and suitable setters to initialize the bean against the request parameters.
From Spring issue #19763:
We're now automatically detecting data classes with a single public constructor, resolving the constructor arguments against the request parameters, as long as the parameter names are retained or an @ConstructorProperties annotation is declared. This works for Spring MVC as well as WebFlux.
@ConstructorProperties is a Java annotation meant to use on a constructor to indicate how the parameters of that constructor correspond to the constructed bean's getter methods.
Java 8+ Parameters Flag
With -parameters compiler flag, we can omit @ConstructorProperties if constructor parameter names match exactly.
Example
Immutable Beans
package com.logicbig.example;
import java.beans.ConstructorProperties;
public class CustomerInfo {
private String customerId;
private String zipCode;
@ConstructorProperties({"id", "zip"})
public CustomerInfo(String customerId, String zipCode) {
this.customerId = customerId;
this.zipCode = zipCode;
}
public String getCustomerId() {
return customerId;
}
public String getZipCode() {
return zipCode;
}
@Override
public String toString() {
return "CustomerInfo{" +
"customerId='" + customerId + '\'' +
", zipCode='" + zipCode + '\'' +
'}';
}
}
public class OrderInfo {
private final String id;
private final String zip;
public OrderInfo(String id, String zip) {
this.id = id;
this.zip = zip;
}
public String getId() {
return id;
}
public String getZip() {
return zip;
}
@Override
public String toString() {
return "OrderInfo{" +
"id='" + id + '\'' +
", zip='" + zip + '\'' +
'}';
}
}
Example Controller
@Controller
public class CustomerController {
@ResponseBody
@GetMapping("/customer")
public String getCustomerInfo(CustomerInfo ci) {
return ci.toString();
}
@ResponseBody
@GetMapping("/order")
public String getCustomerInfo(OrderInfo oi) {
return oi.toString();
}
}
JavaConfig
@EnableWebMvc
@Configuration
@ComponentScan
public class AppConfig {
}
Running
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
Output:
$ curl -s "http://localhost:8080/customer?id=23&zip=1111" CustomerInfo{customerId='23', zipCode='1111'}
$ curl -s "http://localhost:8080/order?id=23&zip=1111" OrderInfo{id='23', zip='1111'}
In versions before Spring 5.0, following exception would be thrown:
SEVERE: Servlet.service() for servlet [springDispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.logicbig.example.CustomerInfo]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.logicbig.example.CustomerInfo.
()] with root cause java.lang.NoSuchMethodException: com.logicbig.example.CustomerInfo. () at java.lang.Class.getConstructor0(Class.java:3082) ......
Integration Tests
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringJUnitWebConfig(AppConfig.class)
class CustomerControllerIntegrationTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.build();
}
@Test
void testCustomerInfoBinding() throws Exception {
mockMvc.perform(get("/customer")
.param("id", "23")
.param("zip", "1111"))
.andExpect(status().isOk())
.andExpect(content().string(
"CustomerInfo{customerId='23', zipCode='1111'}"));
}
@Test
@DisplayName("Should bind query parameters to OrderInfo immutable bean")
void testOrderInfoBinding() throws Exception {
mockMvc.perform(get("/order")
.param("id", "23")
.param("zip", "1111"))
.andExpect(status().isOk())
.andExpect(content().string("OrderInfo{id='23', zip='1111'}"));
}
}
mvn clean test -Dtest="CustomerControllerIntegrationTest" Output$ mvn clean test -Dtest="CustomerControllerIntegrationTest" [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for com.logicbig.example:spring-5-immutable-class-data-binding:war:1.0-SNAPSHOT [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-war-plugin is missing. @ line 42, 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-5-immutable-class-data-binding >----- [INFO] Building spring-5-immutable-class-data-binding 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ spring-5-immutable-class-data-binding --- [INFO] Deleting D:\example-projects\spring-mvc\data-binding\spring-5-immutable-class-data-binding\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ spring-5-immutable-class-data-binding --- [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\data-binding\spring-5-immutable-class-data-binding\src\main\resources [INFO] [INFO] --- compiler:3.8.0:compile (default-compile) @ spring-5-immutable-class-data-binding --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 5 source files to D:\example-projects\spring-mvc\data-binding\spring-5-immutable-class-data-binding\target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-5-immutable-class-data-binding --- [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\data-binding\spring-5-immutable-class-data-binding\src\test\resources [INFO] [INFO] --- compiler:3.8.0:testCompile (default-testCompile) @ spring-5-immutable-class-data-binding --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 1 source file to D:\example-projects\spring-mvc\data-binding\spring-5-immutable-class-data-binding\target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ spring-5-immutable-class-data-binding --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [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.CustomerControllerIntegrationTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.503 s -- in com.logicbig.example.CustomerControllerIntegrationTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.801 s [INFO] Finished at: 2026-05-13T18:44:40+08:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 4 ms INFO: Completed initialization in 1 ms
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 5.0.0.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)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- JDK 25
- Maven 3.9.11
|