Forms are the main way users interact with the server through a web UI. Whether it's registering, adding data, or doing a search, forms are the bridge between frontend and backend. Spring MVC gives you handy tools to process user data from forms, validate it, and send it back to views if something goes wrong.
Form handling: step-by-step process
To get a clear picture of how forms are handled in Spring MVC, let's break the whole process into main steps:
- Create the HTML form using Thymeleaf.
- Set up the model to carry data.
- Create a controller to handle form submissions.
- Validate data using the
@Validannotation. - Handle errors and return data back to the form.
Let's build a form: HTML + Thymeleaf
Start with a simple example — user registration. Here's the HTML snippet for the form:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Registration</title>
</head>
<body>
<h1>Registration</h1>
<form th:action="@{/register}" method="post" th:object="${user}">
<label for="name">Name:</label>
<input type="text" id="name" th:field="*{name}" />
<br/>
<label for="email">Email:</label>
<input type="email" id="email" th:field="*{email}" />
<br/>
<label for="password">Password:</label>
<input type="password" id="password" th:field="*{password}" />
<br/>
<button type="submit">Register</button>
</form>
</body>
</html>
Let's break down the main parts:
th:action="@{/register}"— the URL where the form will be submitted. Here we point to the controller route/register.th:object="${user}"— the object bound to this form (the model). In our case it's theuserobject.th:field="*{name}"— binds the form field to theuser.nameproperty.
The form will send an HTTP POST request with the data the user entered.
Data model: creating a DTO class
Now let's create the User object to bind data coming from the form. It's a POJO class with the necessary fields:
package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class User {
@NotBlank(message = "Name must not be empty")
@Size(min = 2, max = 30, message = "Name must be between 2 and 30 characters")
private String name;
@NotBlank(message = "Email must not be empty")
@Email(message = "Please enter a valid email")
private String email;
@NotBlank(message = "Password must not be empty")
@Size(min = 6, message = "Password must be at least 6 characters long")
private String password;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Notable points:
- Annotations from the
jakarta.validation.constraintspackage help us validate fields. For example,@Emailwill check the email format, and@Sizelimits text length.
Controller to handle the data
Now let's create a controller that shows the form page and handles submitted data.
package com.example.demo.controller;
import com.example.demo.dto.User;
import jakarta.validation.Valid;
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.PostMapping;
@Controller
public class RegistrationController {
@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("user", new User());
return "register";
}
@PostMapping("/register")
public String processRegistrationForm(@Valid User user, BindingResult result, Model model) {
if (result.hasErrors()) {
return "register"; // Return the user to the form if there are errors
}
// Here you can save the data to the database or send it somewhere else
model.addAttribute("message", "Registration successful!");
return "success";
}
}
@GetMapping("/register"): shows the form page. We add an emptyUserobject to the model so Thymeleaf can work with it.@PostMapping("/register"): handles the POST request. Note the@Validannotation before theUserparameter. That tells Spring to validate this object.BindingResult result: holds validation results. If there are errors, we return the user back to the form.
Server-side validation
Say the user entered something invalid. We need to show errors on the page:
<form th:action="@{/register}" method="post" th:object="${user}">
<label for="name">Name:</label>
<input type="text" id="name" th:field="*{name}" />
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
<br/>
<label for="email">Email:</label>
<input type="email" id="email" th:field="*{email}" />
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
<br/>
<label for="password">Password:</label>
<input type="password" id="password" th:field="*{password}" />
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>
<br/>
<button type="submit">Register</button>
</form>
What's happening?
If validation on the User object finds errors, the method #fields.hasErrors() will return true for fields with errors. The th:errors attribute will automatically output the error message defined in the model.
Global form errors (for example, password match)
If you need to add a general form error, you can do it via BindingResult:
if (!user.getPassword().equals(user.getConfirmPassword())) {
result.rejectValue("password", "error.user", "Passwords do not match");
}
Or a global error:
if (user.getEmail().contains("spam")) {
result.reject("email.spam", "Email cannot contain 'spam'");
}
Pitfalls and common mistakes
Why must BindingResult always come right after @Valid?
Spring runs validation and writes results into the BindingResult object. If you change the order of method arguments, you'll get an IllegalStateException.
Be careful with field names!
Form field names (for example, th:field="*{name}") must exactly match the field names in the User object. Otherwise the data won't bind correctly.
What's next?
Now you know how to handle forms, bind them to objects, and validate data. These skills are useful for creating registration/login UIs, and for creating or editing entities in your app.
GO TO FULL VERSION