MVC Application using Spring Boot and Thymeleaf
In this article, I will be developing a Model-View-Controller (MVC) application using Spring Boot, H2 Database and Thymeleaf (Java Template Engine).
Spring Boot Framework will serve as back-end(created in Microservice Using Spring Boot, Hibernate and H2 Database, same code will be reused with few modifications).
I will be using the same Relational Database known as H2 Database, for persisting(storing) the data, used in the above mentioned project.
Thymeleaf (Java Template Engine) will be the User Interface (UI) for this application.
If you have no time to read this article, but want to try the code for yourself, GitHub location is provided here. Go ahead and clone the code.
Overview
We will build a MVC application using Spring Boot, H2 Database and Thymeleaf (Java Template Engine) for a Todo Task Application in that:
- Each Todo Task has id, title, description, creation date, due date, status and comments.
- User can Create New Task, Update Existing Task, View All Tasks as List, View a Task and Delete a Task.
What is Thymeleaf?
Thymeleaf is a Java Template Engine for processing and creating HTML5, XML, JavaScript, CSS and Text. It is able to apply a set of transformations to template files in order to display data and/or text produced by your applications.
The library is extremely extensible and its natural templating capability ensures templates can be prototyped without a backend – which makes development very fast when compared with other popular template engines such as JSP.
Thymeleaf has a good integration with Spring MVC Framework, making it first choice library for Frontend Development in Java and Spring Technologies.
Prerequisites
There are some prerequisites that are required for creating the Microservice Spring Boot Application.
Familiarity with Technology and Frameworks
It is assumed that you have prior basic knowledge or familiarity with Java Technology, HTML, Thymeleaf, Spring, Spring Boot, Hibernate Frameworks, working with RDBMS databases and basic SQL commands, because I will not be covering the basics of these in this article.
If you are not familiar, then it is advised to get the basic knowledge of these before continuing.
JDK 1.8+
Download the latest version of the Java JDK (1.17 is the latest LTS version, at the time of writing this article) from here. Click on the downloaded .exe and complete the installation.
Since it is an Oracle Proprietary product you will need to sign-in/create Oracle Account, before downloading.
Integrated Development Environment (IDE) for Code Development
You can use any IDE of your choice. I will be using the IntelliJ IDEA Community Edition.
If you wish to use the IntelliJ IDEA Community Edition, download the latest version from here.
Click on the downloaded .exe and complete the installation.
Other related software
Webserver– Tomcat is embedded in the Spring Boot application and can be used as Webserver for running this project.
Project Management Tool (Maven)– Maven is embedded in the Spring Boot application and can be used for building and managing the Spring Boot project.
Adding Thymeleaf Template Engine to Spring Boot Project
We will be updating the Microservice Application already created in Microservice Using Spring Boot, Hibernate and H2 Database. We will make the necessary changes to add Thymeleaf (Java Template Engine) , as a View Layer to this application.
Update pom.xml
- Add the following Thymeleaf (Java Template Engine) dependencies
1<dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-thymeleaf</artifactId>
4</dependency>
Create Fragment Templates
Before creating endpoints, let us first understand one important feature of the Thymeleaf- Fragment Templates, also known as Frames.
Default location for storing all the Thymeleaf templates is: /src/main/resources/templates
Fragment templates provides consistency across the pages to have same header, footer contents and also maintain similar feel to the pages.
There are two ways of creating fragment templates in Thymeleaf
Using the layout
and layout:decorator
Dialect Directive
Define the header, footer and body section in the main template and replace only the body section in the subsequent html files, using the Thymeleaf layout
and layout:decorator
dialect directive.
Let us see an example:
In order to get this to work, we need to have a below Maven dependency in the pom.xml:
1<dependency>
2 <groupId>nz.net.ultraq.thymeleaf</groupId>
3 <artifactId>thymeleaf-layout-dialect</artifactId>
4</dependency>
Create a BaseLayout.html
file in the location: /src/main/resources/templates/Layouts
1<!DOCTYPE html>
2<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
3 <head>
4 <meta charset="UTF-8"/>
5 <title>Base Layout</title>
6 </head>
7 <body>
8 <header th:fragment="custom-header">This is a custom header</header>
9 <section layout:fragment="custom-body-content">This is the main body section</section>
10 <footer th:fragment="custom-footer">This is a custom footer</footer>
11 </body>
12</html>
This creates a Main Fragment page, on which body section will get replaced.
Create a home.html
file in the location: /src/main/resources/templates
1<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
2 layout:decorator="Layouts/BaseLayout">
3 <head>
4 <title layout:title-pattern="$DECORATOR_TITLE | $CONTENT_TITLE">Dashboard</title>
5 </head>
6 <body>
7 <section layout:fragment="custom-body-content">
8 <div class="container-fluid">
9 This the home page for the example
10 </div>
11 </section>
12 </body>
13</html>
Contents inside the <section layout:fragment="custom-body-content">
attribute in the home.html
file will replace the contents in the BaseLayout.html
file with the same <section>
attribute during the runtime, since we have mapping for the layout using the Thymeleaf directive.
Using the fragment replace
directive
Define the header, footer and body section in the main template and add the header and footer sections in the subsequent html files using the Thymeleaf replace
directive.
Let us see an example.
Create a header.html
file in the location: /src/main/resources/templates/fragments
1<html xmlns:th="http://www.thymeleaf.org">
2 <head>
3 <div th:fragment="header-css">
4 This is header-css fragment
5 </div>
6 </head>
7 <body>
8 <div th:fragment="header">
9 This is header fragment
10 </div>
11 </body>
12</html>
Similarly create a footer.html
file and place it in the location: /src/main/resources/templates/fragments
1<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
2 <head>
3 </head>
4 <body>
5 <div th:fragment="footer">
6 <footer>
7 This is footer fragment
8 </footer>
9 </div>
10 </body>
11</html>
Create a home.html
file in the location: /src/main/resources/templates/
and use these header and footer fragments.
1<html xmlns:th="http://www.thymeleaf.org">
2 <head>
3 <title>Spring Boot Thymeleaf + Spring Security</title>
4 <div th:replace="fragments/header :: header-css"/>
5 </head>
6 <body>
7 <div th:replace="fragments/header :: header"/>
8 <div class="container">
9 This is body content
10 </div>
11 <div th:replace="fragments/footer :: footer"/>
12 </body>
13</html>
As you can see in the above code, header and footer sections are inserted into the page using the replace
attribute from the Thymeleaf library.
We have seen 2 different methods of creating and using the fragment templates in the Thymeleaf. You can create and use either or none of them, based on your requirement.
Create Thymeleaf Application
For our example we will be using the Using the fragment replace directive explained above.
Final Project Structure
Final Project Structure would look something like given below:
Create Header and Footer files
Let us create header.html
and footer.html
files in their respective locations, as explained earlier.
- Create
header.html
file in the location:/src/main/resources/templates/fragments
1<html xmlns:th="http://www.thymeleaf.org">
2 <head>
3 <div th:fragment="header-css">
4 <!-- this is header-css -->
5 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
6 integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
7 crossorigin="anonymous">
8 </div>
9 </head>
10<body>
11 <div th:fragment="header">
12 <!-- this is header -->
13 <nav class="navbar navbar-inverse">
14 <div class="container">
15 <div id="navbar" class="collapse navbar-collapse">
16 <ul class="nav navbar-nav">
17 <li class="active"><a th:href="@{/}">Home</a></li>
18 </ul>
19 </div>
20 </div>
21 </nav>
22 </div>
23</body>
24</html>
- Create a
footer.html
file and place it in location:/src/main/resources/templates/fragments
1<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
2<head>
3</head>
4<body>
5<div th:fragment="footer">
6 <div class="container" align="center">
7 <footer>
8 <!-- this is footer -->
9 2017-<script>document.write(new Date().getFullYear())</script>
10 </footer>
11 </div>
12</div>
13</body>
14</html>
Create required html files
- Add
/src/main/resources/templates/index.html
1<html lang="en" xmlns:th="http://www.thymeleaf.org">
2<head>
3 <meta charset="UTF-8">
4 <title>To do Tracker</title>
5 <div th:replace="fragments/header :: header-css"></div>
6</head>
7<body>
8 <div th:replace="fragments/header :: header"></div>
9 <div class="container" align="center">
10 <a th:href="@{/singleItemView/edit/0}"><button class="btn btn-success">CREATE</button></a>
11 <br/>
12 <table class='table-striped' border='1' align="center">
13 <thead>
14 <tr>
15 <th>Title</th>
16 <th>Description</th>
17 <th>Due Date</th>
18 <th>Status</th>
19 <th>No. Of Comments</th>
20 <th>View</th>
21 <th>Edit</th>
22 <th>Delete</th>
23 </tr>
24 </thead>
25 <tbody>
26 <tr th:each="tasks : ${tasksItemList}">
27 <td th:text="${tasks.title}"></td>
28 <td th:text="${tasks.description}"></td>
29 <td th:text="${tasks.dueDate}"></td>
30 <td th:text="${tasks.status}"></td>
31 <td align="center" th:text="${tasks.todoTaskCommentsSet.size()}"></td>
32 <td>
33 <a th:href="@{/singleItemView/view/__${tasks.systemTasksId}__}"><button class="btn btn-success">VIEW</button></a>
34 </td>
35 <td>
36 <a th:href="@{/singleItemView/edit/__${tasks.systemTasksId}__}"><button class="btn btn-success">EDIT</button></a>
37 </td>
38 <td>
39 <a th:href="@{/deleteById/__${tasks.systemTasksId}__}"><button class="btn btn-warning">DELETE</button></a>
40 </td>
41 </tr>
42 </tbody>
43 </table>
44 </div>
45 <br/>
46 <div th:replace="fragments/footer :: footer"></div>
47</body>
48</html>
Highlighted lines(5,8,46) are using the th:replace
directive dialect of the Thymeleaf to have the header and footer from the fragments templates, inserted into this template file.
- Add
/src/main/resources/templates/singleItemPage.html
1<html lang="en" xmlns:th="http://www.thymeleaf.org">
2<head>
3 <meta charset="UTF-8">
4 <title>To do Tracker</title>
5 <div th:replace="fragments/header :: header-css"></div>
6 <script type="text/javascript">
7 function addNewComments(){
8 var lTable = document.getElementById("commentsTable");
9 lTable.style.display = (lTable.style.display == "table") ? "none" : "table";
10 }
11 </script>
12</head>
13<body>
14 <div th:replace="fragments/header :: header"></div>
15 <div class="container" align="center">
16 <form method="post" th:action="@{/createOrUpdate}" th:object="${tasksItem}">
17 <table class='table-striped' border='1'>
18 <tbody>
19 <input type="hidden" name="systemTasksId" id="systemTasksId" th:value="*{systemTasksId}"/>
20 <tr>
21 <th>Title</th>
22 <td>
23 <span th:if="${action == 'view'}" th:text="${tasksItem.title}"></span>
24 <span th:unless="${action == 'view'}">
25 <input type="text" name='title' id='title' th:value='*{title}' size="35"/>
26 </span>
27 </td>
28 </tr><tr>
29 <th>Description</th>
30 <td>
31 <span th:if="${action == 'view'}" th:text="${tasksItem.description}"></span>
32 <span th:unless="${action == 'view'}">
33 <textarea name='description' id='description' rows="3" cols="38" th:value='*{description}' th:text="${tasksItem.description}"></textarea>
34 </span>
35 </td>
36 </tr><tr th:if="${tasksItem.systemTasksId > 0}">
37 <th>Creation Date</th>
38 <td>
39 <span th:text="${#temporals.format(tasksItem.creationDate, 'dd-MM-yyyy')}"></span>
40 </td>
41 </tr><tr>
42 <th>Due Date</th>
43 <td>
44 <span th:if="${action == 'view'}" th:text="${#temporals.format(tasksItem.dueDate, 'dd-MM-yyyy')}"></span>
45 <span th:unless="${action == 'view'}">
46 <input type="date" th:field="*{dueDate}" th:value="*{dueDate}"/>
47 </span>
48 </td>
49 </tr><tr>
50 <th>Status</th>
51 <td>
52 <span th:if="${action == 'view'}"th:text='${tasksItem.status}'></span>
53 <span th:unless="${action == 'view'}">
54 <select th:field="*{status}">
55 <option value=''>--Select Status--</option>
56 <option th:each="status : ${todoStatus}" th:value="${status}" th:text="${status}">
57 </option>
58 </select>
59 </span>
60 </td>
61 </tr>
62 <tr th:if="${tasksItem.systemTasksId > 0}">
63 <th>Comments</th>
64 <td>
65 <table class='table-bordered'>
66 <thead th:if="${tasksItem.todoTaskCommentsSet.size() > 0}">
67 <tr>
68 <th>Creation Date</th>
69 <th>Description</th>
70 </tr>
71 </thead>
72 <tbody>
73 <tr th:each="comments : ${tasksItem.todoTaskCommentsSet}"
74 th:if="${tasksItem.todoTaskCommentsSet.size() > 0}">
75 <td>
76 <span th:text="${#temporals.format(comments.creationDate, 'dd-MM-yyyy')}"></span>
77 </td>
78 <td th:text="${comments.taskComments}"></td>
79 </tr>
80 <tr>
81 <td colspan="2" th:if="${action == 'edit' and tasksItem.systemTasksId > 0}" th:object="${todoTaskComments}">
82 <input type="button" class="btn btn-success" onclick=addNewComments()
83 value="Add New Comments" />
84 <table class='table-bordered' id="commentsTable" style="display:none;">
85 <tr>
86 <th>Description</th>
87 <td>
88 <textarea th:field="*{taskComments}" rows="3" cols="38"></textarea>
89 </td>
90 </tr>
91 </table>
92 </td>
93 </tr>
94 </tbody>
95 </table>
96 </td>
97 </tr>
98 <tr th:if="${action == 'edit'}">
99 <td colspan="2" align="center">
100 <button class="btn btn-success">Submit</button>
101 </td>
102 </tr>
103 </tbody>
104 </table>
105 </form>
106 </div>
107 <br/>
108 <div th:replace="fragments/footer :: footer"></div>
109</body>
110</html>
Update the existing Controller = controller.TodoController
- Remove all the RequestMappings(like @GetMapping, @PostMapping, @PutMapping, @DeleteMapping) from this class, since it is serving as only Intermediary Controller class.
1@RestController
2public class TodoController implements TodoApplicationConstants {
3
4 @Autowired
5 private TodoService todoService;
6
7 public ResponseEntity<List<Tasks>> getAllTasks() {
8 try {
9 List<Tasks> tasksList = todoService.findAll();
10 return new ResponseEntity<>(tasksList, HttpStatus.OK);
11 } catch (Exception ex) {
12 return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
13 }
14 }
15
16 public ResponseEntity<Tasks> getTasksById(@PathVariable("id") long id) {
17 Optional<Tasks> task = todoService.findById(id);
18 if (task.isPresent()) {
19 return new ResponseEntity<>(task.get(), HttpStatus.OK);
20 } else {
21 return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
22 }
23 }
24
25 public ResponseEntity<HttpStatus> deleteTaskById(@PathVariable("id") long id) {
26 try {
27 if(todoService.deleteById(id))
28 return new ResponseEntity<>(HttpStatus.NO_CONTENT);
29 else
30 return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
31 } catch (Exception ex) {
32 return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
33 }
34 }
35
36 public ResponseEntity<Tasks> updateTaskItem(@RequestBody Tasks task) {
37 task = todoService.createOrUpdate(task);
38 return new ResponseEntity<>(task, HttpStatus.OK);
39 }
40
41 public ResponseEntity<Tasks> createTodoTask(@RequestBody Tasks task) {
42 task = todoService.createOrUpdate(task);
43 return new ResponseEntity<>(task, HttpStatus.CREATED);
44 }
45
46 public ResponseEntity<List<String>> getTaskStatus() {
47 List<String> taskStatusList = todoService.getTodoStatusAsList();
48 return new ResponseEntity<>(taskStatusList, HttpStatus.OK);
49 }
50}
Create a new Controller Class = controller.TodoViewController
- This is the Controller Class that will be receiving the requests from Thymeleaf template html files and hence it will have all the RequestMappings(like @GetMapping, @PostMapping, @PutMapping, @DeleteMapping).
1@Controller
2public class TodoViewController implements TodoApplicationConstants {
3
4 @Autowired
5 private TodoController todoController;
6
7 @GetMapping
8 public String gotoHome(ModelMap model) {
9 model.put("tasksItemList", todoController.getAllTasks().getBody());
10 return "index";
11 }
12
13 @GetMapping("/singleItemView/{action}/{todoId}")
14 public String goToSingleItemPage(ModelMap model, @PathVariable("action") String action,
15 @PathVariable("todoId") Long todoId){
16 Tasks tasksItem = Tasks.builder().title("").description("").dueDate(null).status("").build();
17 if(todoId > 0) {
18 tasksItem = todoController.getTasksById(todoId).getBody();
19 }
20 model.put("tasksItem", tasksItem);
21 model.put("todoTaskComments", new TodoTaskComments());
22 model.put("action",action);
23 model.put("todoStatus",todoController.getTaskStatus().getBody());
24 return "singleItemPage";
25 }
26
27 @PostMapping(value = "/createOrUpdate", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
28 public String sendToCreate(Tasks tasksItem, TodoTaskComments todoTaskComments) {
29 if (tasksItem.getSystemTasksId() != null && tasksItem.getSystemTasksId() > 0 ) {
30 Set<TodoTaskComments> todoTaskCommentsSet = new HashSet<>();
31 todoTaskCommentsSet.add(todoTaskComments);
32 tasksItem.setTodoTaskCommentsSet(todoTaskCommentsSet);
33
34 todoController.updateTaskItem(tasksItem);
35 } else {
36 Tasks response = todoController.createTodoTask(tasksItem).getBody();
37 }
38 return "redirect:/";
39 }
40
41 @GetMapping("/deleteById/{id}")
42 public String sendToDeleteById(@PathVariable long id) {
43 todoController.deleteTaskById(id);
44 return "redirect:/";
45 }
46}
Let us look at some of the Highlights:
- Line# 1: We are using
@Controller
annotation.- Usually for Rest Client APIs, we use
@RestController
. - However, in order to inform the Thymeleaf (Java Template Engine) to redirect the requests to Thymeleaf templates, we need to use
@Controller
. - If we use
@RestController
, since it is a combination of@Controller
and@ResponseBody
, whatever we have in the return statement, will be displayed on the UI.
- Usually for Rest Client APIs, we use
- Line# 10: Method returns the String value “index”.
- Because we are using
@Controller
, Thymeleaf (Java Template Engine) will identify it as a call to the Thymeleaf template and tries to find the template with the nameindex.html
in/src/main/resources/templates
. - Since we have the file
index.html
at that location, it will render that file.
- Because we are using
Running the Thymeleaf Spring Boot Project
With these changes, we are ready to run the project.
Run the server using the Configuration settings as shown here.
When we start the project and go to browser and type http://localhost:8080/todo-app/tasks, we will see the index.html
in action.
-
When you click on Create Button
-
Enter the values and Click on the Submit Button in the Create Page
-
Click on View Button to go to the below page
-
On the Home page, click on the Edit Button to get the Edit Page, where you can update the existing contents or add new comment.
-
On the Home page, click on Delete Button and delete the task.
Conclusion
With this setup, we have come to an end of this article and we have seen how we can developing a Model-View-Controller (MVC) Application using Spring Boot, H2 Database and Thymeleaf (Java Template Engine).
Complete code for this project can be found at GitHub here. Go ahead and clone it.
Instructions on how to clone the code repository and run the project are provided on the GitHub project page.