Spring MVC Forms
Introduction
Forms are an essential part of web applications, allowing users to submit data to the server. Spring MVC provides a robust system for handling form submissions, validating input data, and displaying error messages. This guide will walk you through creating, processing, and validating forms in Spring MVC applications.
In this tutorial, you'll learn:
- How to create HTML forms linked to Spring controllers
- Working with form backing objects
- Form data binding
- Validation and displaying error messages
- File uploads and complex form processing
Understanding Spring MVC Form Handling
Spring MVC's form handling mechanism is centered around mapping HTML form elements to Java objects through a process called data binding. This allows for a clean separation between your data models and the presentation layer.
Core Components of Spring MVC Form Handling
- Form Backing Object: A Java bean that holds the form data
- Controller: Processes form submissions and performs business logic
- View: Displays the form and any validation messages
- Validator: Validates the form data
Creating a Basic Form
Let's start with a simple example of a user registration form.
Step 1: Create a Form Backing Object
First, we need to create a Java class that will hold our form data:
package com.example.demo.model;
public class UserForm {
private String username;
private String email;
private String password;
// Getters and setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
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;
}
}
Step 2: Create a Controller
Next, we'll create a controller to handle the form:
package com.example.demo.controller;
import com.example.demo.model.UserForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class UserController {
@GetMapping("/register")
public String showForm(Model model) {
// Add a new UserForm object to the model
model.addAttribute("userForm", new UserForm());
return "register-form";
}
@PostMapping("/register")
public String processForm(@ModelAttribute("userForm") UserForm userForm, Model model) {
// Process the form data
model.addAttribute("message", "Registration successful!");
model.addAttribute("user", userForm);
return "registration-success";
}
}
Step 3: Create the Form View
Now we'll create a Thymeleaf template for our form. Create a file called register-form.html
in the src/main/resources/templates
directory:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>User Registration</title>
</head>
<body>
<h1>User Registration Form</h1>
<form action="#" th:action="@{/register}" th:object="${userForm}" method="post">
<div>
<label for="username">Username:</label>
<input type="text" th:field="*{username}" id="username" />
</div>
<div>
<label for="email">Email:</label>
<input type="email" th:field="*{email}" id="email" />
</div>
<div>
<label for="password">Password:</label>
<input type="password" th:field="*{password}" id="password" />
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</body>
</html>
Step 4: Create Success View
Create a registration-success.html
template:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Registration Success</title>
</head>
<body>
<h1>Registration Successful!</h1>
<p th:text="${message}">Message</p>
<h2>User Details:</h2>
<p>Username: <span th:text="${user.username}">username</span></p>
<p>Email: <span th:text="${user.email}">email</span></p>
</body>
</html>
How It Works
- When a user navigates to
/register
, theshowForm
method in our controller is called - This method adds an empty
UserForm
object to the model and returns the view name - The Thymeleaf template engine renders the form, binding form fields to the
UserForm
object - When the form is submitted, Spring binds the submitted values to a new
UserForm
instance - The
processForm
method receives this populated object and processes it
Form Validation
Spring MVC integrates with Java's Bean Validation API to provide form validation. Let's enhance our example with validation.
Step 1: Add Validation Dependencies
Add these dependencies to your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Step 2: Add Validation Annotations to the Model
Update the UserForm
class with validation annotations:
package com.example.demo.model;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
public class UserForm {
@NotEmpty(message = "Username cannot be empty")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String username;
@NotEmpty(message = "Email cannot be empty")
@Email(message = "Please provide a valid email address")
private String email;
@NotEmpty(message = "Password cannot be empty")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
// Getters and setters (same as before)
}
Step 3: Update the Controller to Handle Validation
package com.example.demo.controller;
import com.example.demo.model.UserForm;
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 javax.validation.Valid;
@Controller
public class UserController {
@GetMapping("/register")
public String showForm(Model model) {
model.addAttribute("userForm", new UserForm());
return "register-form";
}
@PostMapping("/register")
public String processForm(@Valid @ModelAttribute("userForm") UserForm userForm,
BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
// If there are validation errors, return to the form
return "register-form";
}
// Process the valid form data
model.addAttribute("message", "Registration successful!");
model.addAttribute("user", userForm);
return "registration-success";
}
}
Step 4: Update the Form to Display Validation Errors
Update the register-form.html
template:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>User Registration</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>User Registration Form</h1>
<form action="#" th:action="@{/register}" th:object="${userForm}" method="post">
<div>
<label for="username">Username:</label>
<input type="text" th:field="*{username}" id="username" />
<span th:if="${#fields.hasErrors('username')}" th:errors="*{username}" class="error"></span>
</div>
<div>
<label for="email">Email:</label>
<input type="email" th:field="*{email}" id="email" />
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error"></span>
</div>
<div>
<label for="password">Password:</label>
<input type="password" th:field="*{password}" id="password" />
<span th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="error"></span>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</body>
</html>
Advanced Form Features
Form Select Dropdowns
Let's add a dropdown for selecting a user role:
- Update the
UserForm
class:
public class UserForm {
// Previous fields
private String role;
// Getter and setter for role
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
// Previous getters and setters
}
- Update the controller to provide available roles:
@Controller
public class UserController {
@ModelAttribute("availableRoles")
public List<String> getAvailableRoles() {
return Arrays.asList("User", "Editor", "Admin");
}
// Previous methods
}
- Add the select dropdown to the form:
<div>
<label for="role">Role:</label>
<select th:field="*{role}" id="role">
<option value="">Select a role</option>
<option th:each="role : ${availableRoles}" th:value="${role}" th:text="${role}"></option>
</select>
</div>
File Upload
To handle file uploads, follow these steps:
- Add multipart configuration to your application properties:
# src/main/resources/application.properties
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
- Update the
UserForm
class to include a file field:
import org.springframework.web.multipart.MultipartFile;
public class UserForm {
// Previous fields
private MultipartFile profileImage;
public MultipartFile getProfileImage() {
return profileImage;
}
public void setProfileImage(MultipartFile profileImage) {
this.profileImage = profileImage;
}
// Previous getters and setters
}
- Update the form template to include a file input:
<form action="#" th:action="@{/register}" th:object="${userForm}" method="post" enctype="multipart/form-data">
<!-- Previous form fields -->
<div>
<label for="profileImage">Profile Image:</label>
<input type="file" th:field="*{profileImage}" id="profileImage" />
</div>
<!-- Submit button -->
</form>
- Handle the uploaded file in the controller:
@PostMapping("/register")
public String processForm(@Valid @ModelAttribute("userForm") UserForm userForm,
BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
return "register-form";
}
// Handle file upload
MultipartFile profileImage = userForm.getProfileImage();
if (profileImage != null && !profileImage.isEmpty()) {
try {
// Create a file path
String uploadsDir = "/uploads/";
String realPathtoUploads = request.getServletContext().getRealPath(uploadsDir);
// Create directory if it doesn't exist
File dir = new File(realPathtoUploads);
if (!dir.exists()) {
dir.mkdir();
}
// Create the file on server
String filename = profileImage.getOriginalFilename();
File serverFile = new File(dir.getAbsolutePath() + File.separator + filename);
profileImage.transferTo(serverFile);
model.addAttribute("uploadedFile", filename);
} catch (Exception e) {
model.addAttribute("errorMessage", "Error uploading file: " + e.getMessage());
}
}
// Process the rest of the form
model.addAttribute("message", "Registration successful!");
model.addAttribute("user", userForm);
return "registration-success";
}
Form Command Objects vs. Direct Request Parameters
In Spring MVC, you have two main approaches to handling form data:
Command Objects (Form Backing Objects)
As demonstrated in our examples, using a command object allows Spring to bind form fields to a Java object. This approach:
- Keeps your code organized
- Makes validation easier
- Works well with complex forms
Direct Request Parameters
For simpler cases, you can directly access request parameters:
@PostMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password, Model model) {
// Process login
return "login-result";
}
Real-World Example: Customer Registration Form
Let's create a more comprehensive example for a customer registration form:
Customer Model
package com.example.demo.model;
import java.time.LocalDate;
import javax.validation.constraints.*;
public class CustomerForm {
@NotEmpty(message = "First name is required")
private String firstName;
@NotEmpty(message = "Last name is required")
private String lastName;
@NotNull(message = "Date of birth is required")
@Past(message = "Date of birth must be in the past")
private LocalDate dateOfBirth;
@NotEmpty(message = "Email is required")
@Email(message = "Please provide a valid email")
private String email;
@NotEmpty(message = "Phone number is required")
@Pattern(regexp = "^\\d{10}$", message = "Phone number must be 10 digits")
private String phoneNumber;
@NotEmpty(message = "Address is required")
private String address;
private String customerType;
private boolean receiveNewsletter;
// Getters and setters
// (omitted for brevity)
}
Customer Controller
@Controller
@RequestMapping("/customers")
public class CustomerController {
@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("customerForm", new CustomerForm());
return "customer/registration-form";
}
@ModelAttribute("customerTypes")
public List<String> getCustomerTypes() {
return Arrays.asList("Regular", "Premium", "VIP");
}
@PostMapping("/register")
public String registerCustomer(@Valid @ModelAttribute("customerForm") CustomerForm customerForm,
BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
return "customer/registration-form";
}
// Save the customer or perform business logic
// For this example, we'll just add the customer to the model
model.addAttribute("customer", customerForm);
return "customer/registration-success";
}
}
Customer Registration Form Template
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Customer Registration</title>
<style>
.error { color: red; }
.form-group { margin-bottom: 15px; }
</style>
</head>
<body>
<h1>Customer Registration</h1>
<form th:action="@{/customers/register}" th:object="${customerForm}" method="post">
<div class="form-group">
<label for="firstName">First Name:</label>
<input type="text" th:field="*{firstName}" id="firstName" />
<span th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}" class="error"></span>
</div>
<div class="form-group">
<label for="lastName">Last Name:</label>
<input type="text" th:field="*{lastName}" id="lastName" />
<span th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}" class="error"></span>
</div>
<div class="form-group">
<label for="dateOfBirth">Date of Birth:</label>
<input type="date" th:field="*{dateOfBirth}" id="dateOfBirth" />
<span th:if="${#fields.hasErrors('dateOfBirth')}" th:errors="*{dateOfBirth}" class="error"></span>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" th:field="*{email}" id="email" />
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error"></span>
</div>
<div class="form-group">
<label for="phoneNumber">Phone Number:</label>
<input type="tel" th:field="*{phoneNumber}" id="phoneNumber" />
<span th:if="${#fields.hasErrors('phoneNumber')}" th:errors="*{phoneNumber}" class="error"></span>
</div>
<div class="form-group">
<label for="address">Address:</label>
<textarea th:field="*{address}" id="address" rows="3"></textarea>
<span th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="error"></span>
</div>
<div class="form-group">
<label for="customerType">Customer Type:</label>
<select th:field="*{customerType}" id="customerType">
<option value="">Select a type</option>
<option th:each="type : ${customerTypes}" th:value="${type}" th:text="${type}"></option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" th:field="*{receiveNewsletter}" />
Receive Newsletter
</label>
</div>
<div class="form-group">
<button type="submit">Register</button>
</div>
</form>
</body>
</html>
Summary
In this tutorial, you've learned how to:
- Create form backing objects to capture form data
- Process form submissions with Spring MVC controllers
- Display forms using Thymeleaf templates
- Implement form validation with Bean Validation API
- Work with different form input types (text, select, checkbox, file uploads)
- Create comprehensive real-world forms
Spring MVC's form handling capabilities provide a powerful and flexible way to manage user input in your web applications. By following the patterns and practices outlined in this guide, you can create robust, user-friendly forms that effectively capture and validate user data.
Additional Resources
Exercises
- Create a contact form that includes name, email, subject, and message fields with appropriate validation.
- Enhance the customer registration form to include a multi-select option for "areas of interest."
- Create a product submission form that allows uploading multiple product images.
- Implement a custom validator for a credit card form that validates the card number format.
- Create a multi-step registration form using session attributes to maintain state between form submissions.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)