A redirect is a useful technique to avoid submitting the same form multiple times. As a result of URL redirection, the client receives the result page via a new HTTP request—ideally using the GET method. The browser's address bar then displays the updated URL instead of the original form URL. Consequently, if a user refreshes the page, they will not trigger another POST submission.
Example
In this complete example of 'user registration' we are going to demonstrate how we can achieve that.
Create the backing object
package com.logicbig.example;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
public class User implements Serializable{
private Long id;
@Size(min = 5, max = 20)
private String name;
@Size(min = 6, max = 15)
@Pattern(regexp = "\\S+", message = "Spaces are not allowed")
private String password;
@NotEmpty
@Email
private String emailAddress;
.............
}
In above backing object we used Jakarta Bean Validation constraints annotations.
The Controller
package com.logicbig.example;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping
public class UserRegistrationController {
@Autowired
private UserService userService;
@GetMapping(value = "register")
public String handleGetRequest(Model model) {
model.addAttribute("user", new User());
return "user-registration";
}
@PostMapping(value = "register")
public String handlePostRequest(@Valid @ModelAttribute("user") User user,
BindingResult bindingResult,
RedirectAttributes ra) {
if (bindingResult.hasErrors()) {
return "user-registration";
}
userService.saveUser(user);
ra.addFlashAttribute("user", user);
return "redirect:/registration-success";
}
@GetMapping(value = "registration-success")
public String handleRegistrationDone(@ModelAttribute("user") User user) {
System.out.println("user....: " + user);
return "registration-done";
}
}
In the example above, we used RedirectAttributes#addFlashAttribute. Please check out this tutorial for more details.
user-registration.jsp
src/main/webapp/WEB-INF/views/user-registration.jsp<%@taglib uri="http://www.springframework.org/tags/form" prefix="frm"%>
<html>
<head>
<style>
.error {
color: red;
}
</style>
</head>
<body>
<h3> Registration Form <h3>
<br/>
<frm:form action="register" method="post" modelAttribute="user">
<pre>
Name <frm:input path="name" />
<frm:errors path="name" cssClass="error" />
Email address <frm:input path="emailAddress" />
<frm:errors path="emailAddress" cssClass="error" />
Password <frm:password path="password" />
<frm:errors path="password" cssClass="error" />
<input type="submit" value="Submit" />
</pre>
</frm:form>
</body>
</html>
registration-done.jsp
src/main/webapp/WEB-INF/views/registration-done.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<body>
<h3> Registration done </h3>
<p>User Name: ${user.name}</p>
<p>User email: ${user.emailAddress}
</body>
</html>
Running the example
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
Enter the URL
http://localhost:8080/spring-form-registration/register
The URL in the address bar will change as the result of redirection:
http://localhost:8080/spring-redirect-attributes/test2/5?attr=attrVal
On validation errors:
On successful submission
Notice the URL has been redirected on the successful form submission.
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitWebConfig(MyWebConfig.class)
public class RegistrationControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvcTester;
@Autowired
private UserService userService;
@BeforeEach
public void setup() {
this.mockMvcTester = MockMvcTester.from(this.wac);
}
@Test
public void testUserController() {
assertThat(this.mockMvcTester.post().uri("/register")
.param("name", "KatieK")
.param("emailAddress",
"kat@exmaple.com")
.param("password", "abcadf")
.exchange())
.hasStatus(HttpStatus.FOUND)
.hasRedirectedUrl("/registration-success");
User user = userService.getUserById(1L);
assertThat(user).isNotNull();
assertThat(user.getName()).isEqualTo("KatieK");
assertThat(user.getEmailAddress()).isEqualTo("kat@exmaple.com");
assertThat(user.getPassword()).isEqualTo("abcadf");
}
@Test
public void testUserErrorController() {
assertThat(this.mockMvcTester.post().uri("/register")
.param("name", "")
.param("emailAddress",
"")
.param("password", "")
.exchange())
.hasStatusOk()
.hasViewName("user-registration");
assertThat(this.mockMvcTester.post().uri("/register")
.param("name", "")
.param("emailAddress",
"")
.param("password", "")
.exchange())
.model().extractingBindingResult("user")
.hasErrorsCount(4);
}
}
mvn clean test -Dtest="RegistrationControllerTest" Output$ mvn clean test -Dtest="RegistrationControllerTest" [INFO] Scanning for projects... [INFO] [INFO] -----------< com.logicbig.example:spring-form-registration >------------ [INFO] Building spring-form-registration 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ spring-form-registration --- [INFO] Deleting D:\example-projects\spring-mvc\spring-form-registration\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ spring-form-registration --- [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-form-registration\src\main\resources [INFO] [INFO] --- compiler:3.15.0:compile (default-compile) @ spring-form-registration --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 6 source files with javac [debug target 25] to target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-form-registration --- [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-form-registration\src\test\resources [INFO] [INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ spring-form-registration --- [INFO] Recompiling the module because of changed dependency. [INFO] Compiling 2 source files with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ spring-form-registration --- [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.RegistrationControllerTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.682 s -- in com.logicbig.example.RegistrationControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 11.269 s [INFO] Finished at: 2026-08-07T13:01:07+08:00 [INFO] ------------------------------------------------------------------------ Aug 07, 2026 1:01:05 PM org.hibernate.validator.internal.util.Version <clinit> INFO: HV000001: Hibernate Validator 9.0.1.Final INFO: Completed initialization in 8 ms INFO: Completed initialization in 1 ms
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)
- hibernate-validator 9.0.1.Final (Hibernate's Jakarta Validation reference implementation)
- expressly 6.0.0 (Jakarta Expression Language 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
|