Spring Boot pagination with Thymeleaf Tutorial
Welcome, in this tutorial, we will explain the pagination in a spring boot application and for this, we will use thymeleaf.
1. Introduction
Before going further in this tutorial, we will look at the common terminology such as introduction to Spring Boot, Lombok, Thymeleaf, and Pagination.
1.1 Spring Boot
- Spring boot is a module that provides rapid application development feature to the spring framework including auto-configuration, standalone-code, and production-ready code
- It creates applications that are packaged as jar and are directly started using embedded servlet container (such as Tomcat, Jetty or, Undertow). Thus, no need to deploy the war files
- It simplifies the maven configuration by providing the starter template and helps to resolve the dependency conflicts. It automatically identifies the required dependencies and imports them in the application
- It helps in removing the boilerplate code, extra annotations, and xml configurations
- It provides a powerful batch processing and manages the rest endpoints
- It provides an efficient jpa-starter library to effectively connect the application with the relational databases
- It offers a Microservice architecture and cloud configuration that manages all the application related configuration properties in a centralized manner
1.2 Lombok
- Lombok is nothing but a small library which reduces the amount of boilerplate Java code from the project
- Automatically generates the getters and setters for the object by using the Lombok annotations
- Hooks in via the Annotation processor API
- Raw source code is passed to Lombok for code generation before the Java Compiler continues. Thus, produces properly compiled Java code in conjunction with the Java Compiler
- Under the
target/classes
folder you can view the compiled class files - Can be used with Maven, Gradle IDE, etc.
1.2.1 Lombok features
Feature | Details |
---|---|
val | Local variables are declared as final |
var | Mutable local variables |
@Slf4J | Creates an SLF4J logger |
@Cleanup | Will call close() on the resource in the finally block |
@Getter | Creates getter methods for all properties |
@Setter | Creates setter for all non-final properties |
@EqualsAndHashCode |
|
@ToString |
|
@NoArgsConstructor |
|
@RequiredArgsContructor |
|
@AllArgsConstructor |
|
@Data |
|
@Builder |
|
@Value |
|
1.3 Thymeleaf
- Thymeleaf is a server-side java template engine for the web applications
- It processes the HTML, XML, JS, CSS, and simple text to bring the elegant designing to a web application
- To use Thymeleaf, you must define the
spring-boot-starter-thymeleaf
dependency in thepom.xml
and mention thexmlns:th="https://thymeleaf.org"
library in the templates
1.4 Pagination
- Pagination is a process of dividing data into suitable chunks to save the resources
- To perform pagination in a spring boot application we will use the
PagingAndSortingRepository
interface to provide the additional methods to retrieve the results by using the pagination in spring boot (i.e. get the first page from the data with 10 items per size etc.)
2. Spring Boot pagination with Thymeleaf Tutorial
Here is a systematic guide for implementing this tutorial but before going any further I’m assuming that you are aware of the Spring boot basics.
2.1 Application Pre-requisite
To start with this tutorial, we are hoping that you at present have the Lombok plugin installed in the IDE of their favorite choice. If someone needs to go through the Lombok installation on IntelliJ IDE, please watch this video. For installation on Eclipse IDE, please watch this video.
2.2 Tools Used and Project Structure
We are using Eclipse Kepler SR2, JDK 8, and Maven. In case you’re confused about where you should create the corresponding files or folder, let us review the project structure of the spring boot application.
Let us start building the application!
3. Creating a Spring Boot application
Below are the steps involved in developing the application.
3.1 Maven Dependency
Here, we specify the dependency for the Spring Boot, Spring Data JPA, Thymeleaf, H2 database, Faker, and Lombok. Maven will automatically resolve the other dependencies. The updated file will have the following code.
pom.xml
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > < modelVersion >4.0.0</ modelVersion > < groupId >com.springboot.thymeleaf.pagination</ groupId > < artifactId >SpringbootThymeleafPaginationV2</ artifactId > < version >0.0.1-SNAPSHOT</ version > < name >Springboot thymeleaf pagination tutorial</ name > < description >A springboot tutorial to show the pagination in thymeleaf</ description > < parent > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-parent</ artifactId > < version >2.3.4.RELEASE</ version > </ parent > < properties > < java.version >1.8</ java.version > </ properties > < dependencies > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-data-jpa</ artifactId > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-thymeleaf</ artifactId > </ dependency > <!-- embedded database (h2) dependency. --> < dependency > < groupId >com.h2database</ groupId > < artifactId >h2</ artifactId > < scope >runtime</ scope > </ dependency > <!-- lombok dependency. --> < dependency > < groupId >org.projectlombok</ groupId > < artifactId >lombok</ artifactId > < scope >provided</ scope > </ dependency > <!-- faker dependency to generate some random data. --> < dependency > < groupId >com.github.javafaker</ groupId > < artifactId >javafaker</ artifactId > < version >1.0.2</ version > </ dependency > </ dependencies > < build > < plugins > < plugin > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-maven-plugin</ artifactId > </ plugin > </ plugins > </ build > </ project > |
3.2 Application Properties
Create a new properties file at the location: SpringbootThymeleafPaginationV2/src/main/resources/
and add the following code to it.
application.properties
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | server.port=10091 spring.application.name=springboot-thymeleaf-pagination-v2 # h2 database settings spring.datasource.username=sa spring.datasource.password= spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver # db-creation settings spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.show_sql=true ## browser url for h2 console - http://localhost:10091/h2-console spring.h2.console.enabled=true spring.h2.console.path=/h2-console |
3.3 Java Classes
Let us write all the java classes involved in this application.
3.3.1 Implementation/Main class
Add the following code to the main class to bootstrap the application from the main method. Always remember, the entry point of the spring boot application is the class containing @SpringBootApplication
annotation and the static main method.
SpringbootThymeleafPagination.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | package com.springboot.thymeleaf.pagination.v2; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // Causes Lombok to generate a logger field. @Slf4j // Serves two purposes i.e. configuration and bootstrapping. @SpringBootApplication public class SpringbootThymeleafPagination { public static void main(String[] args) { SpringApplication.run(SpringbootThymeleafPagination. class , args); log.info( "Springboot Pagination with Thymeleaf application is started successfully ." ); } } |
3.3.2 Model class
Add the following code to the Resident
model class.
Resident.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package com.springboot.thymeleaf.pagination.v2.model; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; import javax.persistence.*; import java.time.LocalDate; @Entity @Table (name = "resident" ) // Causes Lombok to generate toString(), equals(), hashCode(), getter() & setter(), and Required arguments constructor in one go. @Data // Causes Lombok to implement the Builder design pattern for the Pojo class. // Usage can be seen in DefaultResidentsLoader.java -> createNewResident() method. @Builder // Causes Lombok to generate a constructor with no parameters. @NoArgsConstructor // Causes Lombok to generate a constructor with 1 parameter for each field in your class. @AllArgsConstructor @Component public class Resident { @Id @GeneratedValue (strategy = GenerationType.AUTO) int id; @Column (name = "full_name" , nullable = false ) String fullName; @Column (name = "age" , nullable = false ) int age; @Column (name = "gender" , nullable = false ) String gender; @Column (name = "phone_number" , unique = true ) String phoneNumber; @Column (name = "email_address" , nullable = false , unique = true ) String emailAddress; @Column (name = "date_of_birth" , nullable = false ) LocalDate dateOfBirth; @Column (name = "home_address" ) String homeAddress; @Column (name = "nationality" ) String nationality; @Column (name = "first_language" ) String firstLanguage; } |
3.3.3 Configuration class
Add the following code to the bean class that will return the bean object for the faker
object. The usage of this object can be seen in the DefaultResidentsLoader.java
class which is used to load the dummy data into the database on the application startup.
BeanConfiguration.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | package com.springboot.thymeleaf.pagination.v2.configuration; import com.github.javafaker.Faker; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Locale; @Configuration public class BeanConfiguration { @Bean public Faker faker() { return new Faker( new Locale( "en-US" )); } } |
3.3.4 Data-Access-Object interface
Add the following code to the interface that extends the PagingAndSortingRepository
interface.
ResidentRepository.java
01 02 03 04 05 06 07 08 09 10 | package com.springboot.thymeleaf.pagination.v2.repository; import com.springboot.thymeleaf.pagination.v2.model.Resident; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.stereotype.Repository; @Repository public interface ResidentRepository extends PagingAndSortingRepository<Resident, Integer> { } |
3.3.5 Service class
Add the following code to the service class where we will call the DAO interface methods to save the data into the database and also fetch the data from the database.
ResidentService.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package com.springboot.thymeleaf.pagination.v2.service; import com.springboot.thymeleaf.pagination.v2.model.Resident; import com.springboot.thymeleaf.pagination.v2.repository.ResidentRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; // Causes Lombok to generate a logger field. @Slf4j @Service public class ResidentService { @Autowired private ResidentRepository repository; public void save( final Resident resident) { repository.save(resident); } public long getResidentsCount() { log.info( "Finding the total count of residents from the dB." ); return repository.count(); } public Page getPaginatedResidents( final int pageNumber, final int pageSize) { log.info( "Fetching the paginated residents from the dB." ); final Pageable pageable = PageRequest.of(pageNumber - 1 , pageSize); return repository.findAll(pageable); } } |
3.3.6 Bootstrap class
Add the following code to the bootstrap class to save the dummy data into the database on the application startup. This data will be saved in the H2 database.
DefaultResidentsLoader.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | package com.springboot.thymeleaf.pagination.v2.bootstrap; import com.github.javafaker.Faker; import com.springboot.thymeleaf.pagination.v2.model.Resident; import com.springboot.thymeleaf.pagination.v2.service.ResidentService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.time.LocalDate; import java.time.Period; import java.time.ZoneId; import java.util.Random; // Causes Lombok to generate a logger field. @Slf4j // Causes Lombok to generate a constructor with 1 parameter for each field that requires special handling. @RequiredArgsConstructor @Component public class DefaultResidentsLoader implements CommandLineRunner { private static final String[] GENDER = { "Male" , "Female" , "Transgender" , "Not to specify" }; private static final Random RANDOM = new Random(); private final ResidentService service; private final Faker faker; @Override public void run(String... args) throws Exception { loadResidentsData(); } private void loadResidentsData() { if (service.getResidentsCount() == 0 ) { for ( int x = 0 ; x < 100 ; x++) { service.save(createNewResident()); } log.info( "Default residents are successfully saved in the database." ); } else { log.info( "Default residents are already present in the database." ); } } private Resident createNewResident() { final String firstName = faker.name().firstName(); final String lastName = faker.name().lastName(); final String emailAddress = firstName.toLowerCase() + "." + lastName.toLowerCase() + "@company.com" ; final LocalDate birthdate = faker.date().birthday( 25 , 58 ).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); final int age = Period.between(birthdate, LocalDate.now()).getYears(); final String gender = GENDER[RANDOM.nextInt(GENDER.length)]; return Resident.builder() .fullName(firstName + " " + lastName) .age(age) .gender(gender) .phoneNumber(faker.phoneNumber().cellPhone()) .emailAddress(emailAddress) .dateOfBirth(birthdate) .homeAddress(faker.address().fullAddress()) .nationality(faker.nation().nationality()) .firstLanguage(faker.nation().language()) .build(); } } |
3.3.7 Index Controller class
Add the following code to the controller class designed to handle the incoming requests. The class is annotated with the @Controller
annotation were the HTTP GET
method would return the index
page of the application.
ResidentController.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | package com.springboot.thymeleaf.pagination.v2.controller; import com.springboot.thymeleaf.pagination.v2.dto.ResponseDto; import com.springboot.thymeleaf.pagination.v2.model.Resident; import com.springboot.thymeleaf.pagination.v2.service.ResidentService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import java.util.HashMap; import java.util.Map; // Causes Lombok to generate a logger field. @Slf4j @Controller public class ResidentController { private static final int DEFAULT_PAGE_NUMBER = 1 ; private static final int DEFAULT_PAGE_SIZE = 10 ; @Autowired private ResidentService service; // URL - http://localhost:10091/ @GetMapping (value = "/" ) public String viewIndexPage() { log.info( "Redirecting the index page to the controller method for fetching the residents in a paginated fashion." ); return "redirect:residents/paginated/" + DEFAULT_PAGE_NUMBER + "/" + DEFAULT_PAGE_SIZE; } @GetMapping (value = "/residents/paginated/{page}/{page-size}" ) public String getPaginatedResidents( @PathVariable (name = "page" ) final int pageNumber, @PathVariable (name = "page-size" ) final int pageSize, final Model model) { log.info( "Getting the residents in a paginated way for page-number = {} and page-size = {}." , pageNumber, pageSize); final Page<Resident> paginatedResidents = service.getPaginatedResidents(pageNumber, pageSize); model.addAttribute( "responseEntity" , createResponseDto(paginatedResidents, pageNumber)); return "index" ; } private ResponseDto createResponseDto( final Page<Resident> residentPage, final int pageNumber) { final Map<String, Integer> page = new HashMap<>(); page.put( "currentPage" , pageNumber); /* Here we are fetching the total number of records from the Page interface of the Spring itself. We can also customize this logic based on the total number of elements retrieved from the query. */ page.put( "totalPages" , residentPage.getTotalPages()); page.put( "totalElements" , ( int ) residentPage.getTotalElements()); return ResponseDto.create(residentPage.getContent(), page); } } |
4. Thymeleaf Changes
We will create a simple HTML page that will display the residents on the browser in smaller chunks (i.e. the paginated approach). Create a new HTML file at the location: SpringbootThymeleafPaginationV2/src/main/resources/templates/
and add the following code to it.
index.html
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | <! DOCTYPE html> < head > < meta charset = "UTF-8" > < title >Index page</ title > < link href = "https://examples.javacodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9zdGFja3BhdGguYm9vdHN0cmFwY2RuLmNvbS8=bootstrap/4.5.2/css/bootstrap.min.css" rel = "stylesheet" > < style type = "text/css" > th { text-align: center; font-weight: bold; border-top: none !important; } th, td { white-space: nowrap; } .mt-20 { margin-top: 20px; } .table-alignment { margin-left: -200px; } </ style > </ head > < body > < div class = "container" > < h3 class = "text-info text-center mt-20" >Pagination Example : Residents</ h3 > < table class = "table table-striped table-alignment mt-20 text-center" > < thead id = "residentsTable" > < tr > < th >Id</ th > < th >Full name</ th > < th >Age</ th > < th >Gender</ th > < th >Phone Number</ th > < th >Email Address</ th > < th >Date of Birth</ th > < th >Home Address</ th > < th >Nationality</ th > < th >First Language</ th > </ tr > </ thead > < tbody > < tr th:each = "resident : ${responseEntity.residents}" > < td th:text = "${resident.id}" ></ td > < td th:text = "${resident.fullName}" ></ td > < td th:text = "${resident.age}" ></ td > < td th:text = "${resident.gender}" ></ td > < td th:text = "${resident.phoneNumber}" ></ td > < td th:text = "${resident.emailAddress}" ></ td > < td th:text = "${resident.dateOfBirth}" ></ td > < td th:text = "${resident.homeAddress}" ></ td > < td th:text = "${resident.nationality}" ></ td > < td th:text = "${resident.firstLanguage}" ></ td > </ tr > </ tbody > </ table > <!-- Pagination control --> <!-- Hardcoding the default page-size as 10. User can create a dropdown to select the different page-sizes. --> < div class = "row" > < div th:if="${responseEntity.page['totalPages'] > 1}"> < div > Total Items: [[${responseEntity.page['totalPages']}]] </ div > < div > < span th:each = "i: ${#numbers.sequence(1, responseEntity.page['totalPages'])}" > < a th:href = "@{'/residents/paginated/' + ${i} + '/10'}" th:if = "${responseEntity.page['currentPage'] != i}" >[[${i}]]</ a > < span th:unless = "${responseEntity.page['currentPage'] != i}" >[[${i}]]</ span > </ span > </ div > < div > < a th:href = "@{'/residents/paginated/' + ${responseEntity.page['currentPage'] + 1} + '/10'}" th:if = "${responseEntity.page['currentPage'] < responseEntity.page['totalPages']}" > Next </ a > < span th:unless = "${responseEntity.page['currentPage'] < responseEntity.page['totalPages']}" >Next</ span > </ div > < div > < a th:href = "@{'/residents/paginated/' + ${responseEntity.page['totalPages']} + '/10'}" th:if = "${responseEntity.page['currentPage'] < responseEntity.page['totalPages']}" > Last </ a > < span th:unless = "${responseEntity.page['currentPage'] < responseEntity.page['totalPages']}" >Last</ span > </ div > </ div > </ div > </ div > </ body > </ html > |
5. Run the Application
To execute the application, right-click on the SpringbootThymeleafPagination.java
class, Run As -> Java Application
.
6. Project Demo
Open the browser of your choice and hit the following URL. The result will be displayed in a paginated manner (i.e. smaller chunks) and you can click on the page number to retrieve the results as per the page number.
That is all for this tutorial and I hope the article served you whatever you were looking for. Happy Learning and do not forget to share!
7. Summary
In this section, you learned,
- Spring Boot, Thymeleaf, Lombok and it features, and Pagination concepts
- Pagination implementation in Spring Boot and displaying the elements on the browser using Thymeleaf
You can download the sample application as an Eclipse project in the Downloads section.
8. Download the Eclipse Project
This was an example of Spring Boot pagination with Thymeleaf.
You can download the full source code of this example here: Spring Boot pagination with Thymeleaf Tutorial