Lab: Spring MVC with Thymeleaf
This lab will guide you through creating a simple web application using Spring Boot, Spring MVC, and Thymeleaf. By the end of this exercise, you will understand how the Model-View-Controller pattern works in Spring applications and how Thymeleaf integrates with Spring to render dynamic web pages.
Prerequisites
- Basic Java programming knowledge
- Familiarity with HTML and CSS
- Maven or Gradle build tools installed
- Java JDK 11 or higher installed
- An IDE (IntelliJ IDEA, Eclipse, or VS Code)
Lab Objectives
After completing this lab, you will be able to:
- Set up a Spring Boot project with Spring MVC and Thymeleaf
- Understand the Model-View-Controller architecture
- Create controllers to handle HTTP requests
- Use the Model to pass data to views
- Create Thymeleaf templates to display dynamic content
- Process form submissions
- Implement basic validation
Step 1: Project Setup
Let’s begin by creating a Spring Boot project:
- Go to Spring Initializr
- Configure the project:
- Project: Maven
- Language: Java
- Spring Boot: 3.0.x (or latest stable)
- Project Metadata:
- Group: com.example
- Artifact: spring-thymeleaf-lab
- Name: spring-thymeleaf-lab
- Description: Spring MVC with Thymeleaf Lab
- Package name: com.example.spring-thymeleaf-lab
- Packaging: Jar
- Java: 17 (or your installed version)
- Add Dependencies:
- Spring Web
- Spring Boot DevTools
- Thymeleaf
- Validation
- Click “Generate” to download the project ZIP file
- Extract and open in your preferred IDE
Step 2: Understanding the Project Structure
Take a moment to explore the project structure:
src/main/java/com/example/springthymeleaflab/
├── SpringThymeleafLabApplication.java (main application class)
src/main/resources/
├── application.properties (configuration file)
├── static/ (for static resources like CSS, JS, images)
└── templates/ (for Thymeleaf templates)
This structure follows Spring Boot conventions. The SpringThymeleafLabApplication.java
contains the main method and is annotated with @SpringBootApplication
.
Step 3: Create a Controller
Let’s create our first controller:
- Create a new package
controllers
undercom.example.springthymeleaflab
- Create a new class
HomeController.java
in this package:
package com.example.springthymeleaflab.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Welcome to Spring MVC with Thymeleaf!");
return "home";
}
}
Key points to explain:
@Controller
marks this class as a Spring MVC controller@GetMapping("/")
maps HTTP GET requests to “/” to this methodModel
is used to pass data to the viewreturn "home"
tells Spring to use a template named “home.html”
Step 4: Create a Thymeleaf Template
- Create a file
home.html
insrc/main/resources/templates/
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring MVC with Thymeleaf</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
h1 {
color: #3498db;
}
</style>
</head>
<body>
<div class="container">
<h1>Home Page</h1>
<p th:text="${message}">Default message (this will be replaced)</p>
</div>
</body>
</html>
Key points to explain:
xmlns:th="http://www.thymeleaf.org"
enables Thymeleaf namespaceth:text="${message}"
is a Thymeleaf expression that will be replaced with the value of the “message” attribute from the Model- The default text “Default message” is only displayed when viewing the HTML directly without Thymeleaf processing
Step 5: Run the Application
- Run the application (in your IDE or with
mvn spring-boot:run
) - Open a browser and go to http://localhost:8080
- You should see the welcome message from the controller displayed on the page
Step 6: Create a Model Class
Now, let’s create a simple domain model:
- Create a new package
models
undercom.example.springthymeleaflab
- Create a
Student.java
class:
package com.example.springthymeleaflab.models;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class Student {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Please provide a valid email address")
private String email;
@NotBlank(message = "Major is required")
private String major;
// Default constructor
public Student() {
}
// Constructor with fields
public Student(String name, String email, String major) {
this.name = name;
this.email = email;
this.major = major;
}
// 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 getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", email='" + email + '\'' +
", major='" + major + '\'' +
'}';
}
}
Key points to explain:
- We’ve created a simple
Student
class with validation annotations @NotBlank
,@Size
, and@Email
are validation annotations from the Jakarta Validation API- Getters and setters are required for data binding in Spring MVC
Step 7: Create a Student Controller
Create a StudentController.java
in the controllers package:
package com.example.springthymeleaflab.controllers;
import com.example.springthymeleaflab.models.Student;
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 jakarta.validation.Valid;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping("/students")
public class StudentController {
// In-memory storage for students (in a real app, this would be a database)
private List<Student> students = new ArrayList<>();
@GetMapping("/list")
public String listStudents(Model model) {
model.addAttribute("students", students);
return "students/list";
}
@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("student", new Student());
return "students/register";
}
@PostMapping("/register")
public String registerStudent(@Valid @ModelAttribute("student") Student student,
BindingResult result, Model model) {
if (result.hasErrors()) {
return "students/register";
}
students.add(student);
return "redirect:/students/list";
}
}
Key points to explain:
@RequestMapping("/students")
is a base path for all methods in this controller@GetMapping
and@PostMapping
map HTTP methods to controller methods@Valid
triggers validation of theStudent
objectBindingResult
holds validation results- We’re using a simple ArrayList to store students (in a real app, you’d use a database)
redirect:
prefix tells Spring to send a redirect response
Step 8: Create Thymeleaf Templates for Student Operations
- Create a directory
students
undersrc/main/resources/templates/
- Create
list.html
in this directory:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Student List</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
h1 {
color: #3498db;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
.btn {
display: inline-block;
padding: 8px 16px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 4px;
}
.empty-message {
font-style: italic;
color: #777;
}
</style>
</head>
<body>
<div class="container">
<h1>Student List</h1>
<a th:href="@{/students/register}" class="btn">Register New Student</a>
<div th:if="${students.empty}" class="empty-message">
<p>No students registered yet.</p>
</div>
<table th:unless="${students.empty}">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Major</th>
</tr>
</thead>
<tbody>
<tr th:each="student : ${students}">
<td th:text="${student.name}">John Doe</td>
<td th:text="${student.email}">[email protected]</td>
<td th:text="${student.major}">Computer Science</td>
</tr>
</tbody>
</table>
<p>
<a th:href="@{/}" class="btn">Back to Home</a>
</p>
</div>
</body>
</html>
- Create
register.html
in the same directory:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Register Student</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
h1 {
color: #3498db;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input,
select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.error {
color: red;
font-size: 0.9em;
margin-top: 5px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-cancel {
background-color: #95a5a6;
margin-left: 10px;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Register Student</h1>
<form
th:action="@{/students/register}"
th:object="${student}"
method="post"
>
<div class="form-group">
<label for="name">Name:</label>
<input type="text" th:field="*{name}" id="name" />
<div
th:if="${#fields.hasErrors('name')}"
class="error"
th:errors="*{name}"
>
Name Error
</div>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" th:field="*{email}" id="email" />
<div
th:if="${#fields.hasErrors('email')}"
class="error"
th:errors="*{email}"
>
Email Error
</div>
</div>
<div class="form-group">
<label for="major">Major:</label>
<select th:field="*{major}" id="major">
<option value="">Select a major</option>
<option value="Computer Science">Computer Science</option>
<option value="Information Technology">
Information Technology
</option>
<option value="Data Science">Data Science</option>
<option value="Software Engineering">Software Engineering</option>
<option value="Cybersecurity">Cybersecurity</option>
</select>
<div
th:if="${#fields.hasErrors('major')}"
class="error"
th:errors="*{major}"
>
Major Error
</div>
</div>
<div class="form-group">
<button type="submit" class="btn">Register</button>
<a th:href="@{/students/list}" class="btn btn-cancel">Cancel</a>
</div>
</form>
</div>
</body>
</html>
Key points to explain:
th:each
is used for iteration (like a for-each loop)th:if
andth:unless
are conditional statementsth:field
binds form elements to model attributesth:errors
displays validation error messages#fields.hasErrors()
checks if there are validation errors@{...}
syntax creates URL links with proper context paths
Step 9: Update the Home Page
Update the home.html
file to include links to the student pages:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring MVC with Thymeleaf</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
h1 {
color: #3498db;
}
.btn {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>Home Page</h1>
<p th:text="${message}">Default message</p>
<h2>Spring MVC with Thymeleaf Demo</h2>
<p>
This application demonstrates the basic concepts of Spring MVC and
Thymeleaf.
</p>
<div>
<a th:href="@{/students/list}" class="btn">View Students</a>
<a th:href="@{/students/register}" class="btn">Register Student</a>
</div>
</div>
</body>
</html>
Step 10: Run and Test the Complete Application
- Run the application
- Open a browser and go to http://localhost:8080
- Navigate through the app:
- Click “Register Student”
- Fill in the form (try both valid and invalid data)
- View the student list
Conceptual Explanation
During the lab, be sure to understand these key concepts:
- Spring MVC Architecture:
- Model: Java objects that hold application data (like our
Student
class) - View: The Thymeleaf templates that render the UI
- Controller: Java classes that handle HTTP requests and responses
- Data Flow in Spring MVC:
- Browser sends a request to a specific URL
- DispatcherServlet (Spring’s front controller) processes the request
- The request is mapped to the appropriate controller method
- The controller processes the request and puts data in the Model
- The controller returns a view name
- Thymeleaf renders the view with data from the Model
- The response is sent back to the browser
- Thymeleaf Key Features:
- Natural templating: HTML files remain valid even when not processed by Thymeleaf
- Expression syntax:
${...}
for variables,*{...}
for object properties - Link URL creation:
@{...}
- Iteration:
th:each
- Conditionals:
th:if
,th:unless
- Form binding:
th:field
,th:object
Homework Assignments
To reinforce learning, assign these extensions:
- Add the ability to edit existing students
- Add a delete functionality for students
- Add a search feature to find students by name or major
- Create a dashboard that shows statistics (number of students per major)
- Improve the validation with custom error messages
Conclusion
This lab provides a comprehensive introduction to Spring MVC with Thymeleaf. You will gain an understanding of how the MVC pattern works in Spring applications and how Thymeleaf integrates with Spring to create dynamic web pages.
By Wahid Hamdi