在前面的教程中,我们使用Spring Boot和 Thymeleaf 开发了一个 CRUD Web 应用程序。
在本教程中,我们将扩展spring boot CRUD web 应用程序,并使用 spring boot、thymeleaf、spring data JDBC和 MySQL 数据库实现分页和排序操作。
如您所知,分页允许用户一次查看一小部分数据(一页),排序允许用户以更有条理的方式查看数据。分页和排序都可以帮助用户更轻松、更方便地消费信息。
让我们从可以从本教程下载的员工管理系统项目开始,该项目基于 Spring Boot、Spring Data JDBC、Thymeleaf 和 MySQL 数据库。
先决条件
MySQL 数据库
-- --------------------------------------------------------
-- 主机: 127.0.0.1
-- 服务器版本: 8.0.22 - MySQL Community Server - GPL
-- 服务器操作系统: Win64
-- HeidiSQL 版本: 11.3.0.6295
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- 导出 testdb 的数据库结构
CREATE DATABASE IF NOT EXISTS `testdb` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `testdb`;
-- 导出 表 testdb.employees 结构
CREATE TABLE IF NOT EXISTS `employees` (
`id` bigint NOT NULL AUTO_INCREMENT,
`email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`first_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`last_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 正在导出表 testdb.employees 的数据:~7 rows (大约)
/*!40000 ALTER TABLE `employees` DISABLE KEYS */;
INSERT IGNORE INTO `employees` (`id`, `email`, `first_name`, `last_name`) VALUES
(1, '1', '1', '1'),
(2, '2', '2', '2'),
(3, '3', '3', '3'),
(4, '4', '4', '4'),
(5, '5', '5', '5'),
(6, '6', '6', '6'),
(7, '7', '7', '7'),
(8, '8', '8', '8'),
(9, '9', '9', '9');
/*!40000 ALTER TABLE `employees` ENABLE KEYS */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;
项目要求
这是实现员工列表页面(员工管理系统) 的分页和排序的高级项目需求。
用户应该能够:
- 在员工列表页面上执行分页
- 在员工列表页面上进行排序
我们将建造什么?
我们将使用 Spring Boot、Thymeleaf、Spring Data JDBC和 MySQL 数据库实现分页和排序。
下面的截图总结了我们将在本教程中开发的分页和排序操作。
分页:
排序:
首先,我们将逐步实现分页操作,然后我们将完成排序操作。
一、分页实现
了解 Spring Data JDBC 的分页 API
要使用 Spring Data JDBC 提供的分页和排序 API,您的存储库接口必须扩展 PagingAndSortingRepository 接口,该接口定义了以下几个方法(T 指的是实体类):
@NoRepositoryBean
public interface PagingAndSortingRepository < T, ID > extends CrudRepository < T, ID > {
/**
* Returns all entities sorted by the given options.
*
* @param sort
* @return all entities sorted by the given options
*/
Iterable < T > findAll(Sort sort);
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object.
*
* @param pageable
* @return a page of entities
*/
Page < T > findAll(Pageable pageable);
}
例如,使用以下命令从数据库中获取第一页,每页 5 个项目:
int pageNumber = 1;
int pageSize = 5;
Pageable pageable = PageRequest.of(pageNumber, pageSize);
Page<Product> page = repository.findAll(pageable);
然后就可以得到实际内容如下:
List<Employee> listEmployees = page.getContent();
使用 Page 对象,您可以根据给定的页面大小了解数据库中的总行数和总页数:
long totalItems = page.getTotalElements();
int totalPages = page.getTotalPages();
此信息对于使用 Thymeleaf 模板在视图中实现分页很有用。
分页的后端更改
EmployeeService.java 接口更改
在此接口中添加以下方法:
Page<Employee> findPaginated(int pageNo, int pageSize);
完整代码:
package net.javaguides.springboot.service;
import java.util.List;
import org.springframework.data.domain.Page;
import net.javaguides.springboot.model.Employee;
public interface EmployeeService {
List < Employee > getAllEmployees();
void saveEmployee(Employee employee);
Employee getEmployeeById(long id);
void deleteEmployeeById(long id);
Page < Employee > findPaginated(int pageNo, int pageSize);
}
EmployeeServiceImpl.java 类更改
将以下方法添加到 EmployeeServiceImpl 类:
@Override
public Page<Employee> findPaginated(int pageNo, int pageSize) {
Pageable pageable = PageRequest.of(pageNo - 1, pageSize);
return this.employeeRepository.findAll(pageable);
}
EmployeeController.java 类更改
将以下处理程序方法添加到 EmployeeController 类以执行分页:
@GetMapping("/page/{pageNo}")
public String findPaginated(@PathVariable(value = "pageNo") int pageNo, Model model) {
int pageSize = 5;
Page < Employee > page = employeeService.findPaginated(pageNo, pageSize);
List < Employee > listEmployees = page.getContent();
model.addAttribute("currentPage", pageNo);
model.addAttribute("totalPages", page.getTotalPages());
model.addAttribute("totalItems", page.getTotalElements());
model.addAttribute("listEmployees", listEmployees);
return "index";
}
此外,我们需要对现有方法进行更改,如下所示:
// display list of employees
@GetMapping("/")
public String viewHomePage(Model model) {
return findPaginated(1, model);
}
分页的前端更改
将以下分页代码添加到 index.html:
<div th:if="${totalPages > 1}">
<div class="row col-sm-10">
<div class="col-sm-2">
Total Rows: [[${totalItems}]]
</div>
<div class="col-sm-1">
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/page/' + ${i}}">[[${i}]]</a>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
</div>
<div class="col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${currentPage + 1}}">Next</a>
<span th:unless="${currentPage < totalPages}">Next</span>
</div>
<div class="col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${totalPages}}">Last</a>
<span th:unless="${currentPage < totalPages}">Last</span>
</div>
</div>
</div>
完整代码:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="ISO-8859-1">
<title>Employee Management System</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
</head>
<body>
<div class="container my-2">
<h1>Employees List</h1>
<a th:href="@{/showNewEmployeeForm}" class="btn btn-primary btn-sm mb-3"> Add Employee </a>
<table border="1" class="table table-striped table-responsive-md">
<thead>
<tr>
<th>Employee First Name</th>
<th>Employee Last Name</th>
<th>Employee Email</th>
<th> Actions </th>
</tr>
</thead>
<tbody>
<tr th:each="employee : ${listEmployees}">
<td th:text="${employee.firstName}"></td>
<td th:text="${employee.lastName}"></td>
<td th:text="${employee.email}"></td>
<td> <a th:href="@{/showFormForUpdate/{id}(id=${employee.id})}" class="btn btn-primary">Update</a>
<a th:href="@{/deleteEmployee/{id}(id=${employee.id})}" class="btn btn-danger">Delete</a>
</td>
</tr>
</tbody>
</table>
<div th:if="${totalPages > 1}">
<div class="row col-sm-10">
<div class="col-sm-2">
Total Rows: [[${totalItems}]]
</div>
<div class="col-sm-1">
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/page/' + ${i}}">[[${i}]]</a>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
</div>
<div class="col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${currentPage + 1}}">Next</a>
<span th:unless="${currentPage < totalPages}">Next</span>
</div>
<div class="col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${totalPages}}">Last</a>
<span th:unless="${currentPage < totalPages}">Last</span>
</div>
</div>
</div>
</div>
</body>
</html>
2.排序实现
了解 Spring Data JPA 的排序 API
Spring Data JDBC提供了 PagingAndSortingRepository 接口,该接口支持使用以下 API 进行排序和分页:
@NoRepositoryBean
public interface PagingAndSortingRepository < T, ID > extends CrudRepository < T, ID > {
/**
* Returns all entities sorted by the given options.
*
* @param sort
* @return all entities sorted by the given options
*/
Iterable < T > findAll(Sort sort);
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object.
*
* @param pageable
* @return a page of entities
*/
Page < T > findAll(Pageable pageable);
}
用户将能够通过单击表格的列标题对员工列表进行排序。
首先,像这样创建一个 Sort 对象:
Sort sort = Sort.by(“fieldName”).ascending();
这将按 fieldName 升序对结果进行排序。 fieldName 必须与实体类中声明的字段名称匹配。
我们还可以按多个字段排序,例如:
Sort sort = Sort.by("fieldName1").ascending().and(Sort.by("fieldName2").descending());
然后我们通过 Sort 对象来创建一个 Pageable ,如下所示:
Pageable pageable = PageRequest.of(pageNum - 1, pageSize, sort);
让我们在示例部分了解更多。
用于排序的后端更改
EmployeeService.java 接口更改
让我们在现有方法中添加两个字段:
Page<Employee> findPaginated(int pageNo, int pageSize, String sortField, String sortDirection);
完整代码:
package net.javaguides.springboot.service;
import java.util.List;
import org.springframework.data.domain.Page;
import net.javaguides.springboot.model.Employee;
public interface EmployeeService {
List < Employee > getAllEmployees();
void saveEmployee(Employee employee);
Employee getEmployeeById(long id);
void deleteEmployeeById(long id);
Page < Employee > findPaginated(int pageNo, int pageSize, String sortField, String sortDirection);
}
EmployeeServiceImpl.java 类更改
以下方法中实现的排序逻辑:
@Override
public Page<Employee> findPaginated(int pageNo, int pageSize, String sortField, String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortField).ascending() :
Sort.by(sortField).descending();
Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
return this.employeeRepository.findAll(pageable);
}
完整代码:
package net.javaguides.springboot.service;
import java.util.List;
import java.util.Optional;
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.data.domain.Sort;
import org.springframework.stereotype.Service;
import net.javaguides.springboot.model.Employee;
import net.javaguides.springboot.repository.EmployeeRepository;
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Override
public List < Employee > getAllEmployees() {
return employeeRepository.findAll();
}
@Override
public void saveEmployee(Employee employee) {
this.employeeRepository.save(employee);
}
@Override
public Employee getEmployeeById(long id) {
Optional < Employee > optional = employeeRepository.findById(id);
Employee employee = null;
if (optional.isPresent()) {
employee = optional.get();
} else {
throw new RuntimeException(" Employee not found for id :: " + id);
}
return employee;
}
@Override
public void deleteEmployeeById(long id) {
this.employeeRepository.deleteById(id);
}
@Override
public Page < Employee > findPaginated(int pageNo, int pageSize, String sortField, String sortDirection) {
Sort sort = sortDirection.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortField).ascending() :
Sort.by(sortField).descending();
Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
return this.employeeRepository.findAll(pageable);
}
}
EmployeeController.java 更改
让我们更改现有方法以提供对排序的支持:
@GetMapping("/page/{pageNo}")
public String findPaginated(@PathVariable(value = "pageNo") int pageNo,
@RequestParam("sortField") String sortField,
@RequestParam("sortDir") String sortDir,
Model model) {
int pageSize = 5;
Page < Employee > page = employeeService.findPaginated(pageNo, pageSize, sortField, sortDir);
List < Employee > listEmployees = page.getContent();
model.addAttribute("currentPage", pageNo);
model.addAttribute("totalPages", page.getTotalPages());
model.addAttribute("totalItems", page.getTotalElements());
model.addAttribute("sortField", sortField);
model.addAttribute("sortDir", sortDir);
model.addAttribute("reverseSortDir", sortDir.equals("asc") ? "desc" : "asc");
model.addAttribute("listEmployees", listEmployees);
return "index";
}
还为主页提供默认的排序字段和排序方向:
// display list of employees
@GetMapping("/")
public String viewHomePage(Model model) {
return findPaginated(1, "firstName", "asc", model);
}
完整代码:
package net.javaguides.springboot.controller;
import java.util.List;
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.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import net.javaguides.springboot.model.Employee;
import net.javaguides.springboot.service.EmployeeService;
@Controller
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
// display list of employees
@GetMapping("/")
public String viewHomePage(Model model) {
return findPaginated(1, "firstName", "asc", model);
}
@GetMapping("/showNewEmployeeForm")
public String showNewEmployeeForm(Model model) {
// create model attribute to bind form data
Employee employee = new Employee();
model.addAttribute("employee", employee);
return "new_employee";
}
@PostMapping("/saveEmployee")
public String saveEmployee(@ModelAttribute("employee") Employee employee) {
// save employee to database
employeeService.saveEmployee(employee);
return "redirect:/";
}
@GetMapping("/showFormForUpdate/{id}")
public String showFormForUpdate(@PathVariable(value = "id") long id, Model model) {
// get employee from the service
Employee employee = employeeService.getEmployeeById(id);
// set employee as a model attribute to pre-populate the form
model.addAttribute("employee", employee);
return "update_employee";
}
@GetMapping("/deleteEmployee/{id}")
public String deleteEmployee(@PathVariable(value = "id") long id) {
// call delete employee method
this.employeeService.deleteEmployeeById(id);
return "redirect:/";
}
@GetMapping("/page/{pageNo}")
public String findPaginated(@PathVariable(value = "pageNo") int pageNo,
@RequestParam("sortField") String sortField,
@RequestParam("sortDir") String sortDir,
Model model) {
int pageSize = 5;
Page < Employee > page = employeeService.findPaginated(pageNo, pageSize, sortField, sortDir);
List < Employee > listEmployees = page.getContent();
model.addAttribute("currentPage", pageNo);
model.addAttribute("totalPages", page.getTotalPages());
model.addAttribute("totalItems", page.getTotalElements());
model.addAttribute("sortField", sortField);
model.addAttribute("sortDir", sortDir);
model.addAttribute("reverseSortDir", sortDir.equals("asc") ? "desc" : "asc");
model.addAttribute("listEmployees", listEmployees);
return "index";
}
}
排序的前端更改
索引.html
我们通过使用以下代码添加超链接来使表格的标题列可排序:
<th>
<a th:href="@{'/page/' + ${currentPage} + '?sortField=firstName&sortDir=' + ${reverseSortDir}}">
Employee First Name</a>
</th>
<th>
<a th:href="@{'/page/' + ${currentPage} + '?sortField=lastName&sortDir=' + ${reverseSortDir}}">
Employee Last Name</a>
</th>
<th>
<a th:href="@{'/page/' + ${currentPage} + '?sortField=email&sortDir=' + ${reverseSortDir}}">
Employee Email</a>
</th>
<th> Actions </th>
我们还需要更改分页部分以提供排序支持,例如:
<div th:if = "${totalPages > 1}">
<div class = "row col-sm-10">
<div class = "col-sm-2">
Total Rows: [[${totalItems}]]
</div>
<div class = "col-sm-1">
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/page/' + ${i}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">[[${i}]]</a>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
</div>
<div class = "col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${currentPage + 1}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">Next</a>
<span th:unless="${currentPage < totalPages}">Next</span>
</div>
<div class="col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${totalPages}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">Last</a>
<span th:unless="${currentPage < totalPages}">Last</span>
</div>
</div>
</div>
完整代码:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="ISO-8859-1">
<title>Employee Management System</title>
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
</head>
<body>
<div class="container my-2">
<h1>Employees List</h1>
<a th:href = "@{/showNewEmployeeForm}" class="btn btn-primary btn-sm mb-3"> Add Employee </a>
<table border="1" class = "table table-striped table-responsive-md">
<thead>
<tr>
<th>
<a th:href="@{'/page/' + ${currentPage} + '?sortField=firstName&sortDir=' + ${reverseSortDir}}">
Employee First Name</a>
</th>
<th>
<a th:href="@{'/page/' + ${currentPage} + '?sortField=lastName&sortDir=' + ${reverseSortDir}}">
Employee Last Name</a>
</th>
<th>
<a th:href="@{'/page/' + ${currentPage} + '?sortField=email&sortDir=' + ${reverseSortDir}}">
Employee Email</a>
</th>
<th> Actions </th>
</tr>
</thead>
<tbody>
<tr th:each="employee : ${listEmployees}">
<td th:text="${employee.firstName}"></td>
<td th:text="${employee.lastName}"></td>
<td th:text="${employee.email}"></td>
<td> <a th:href="@{/showFormForUpdate/{id}(id=${employee.id})}" class="btn btn-primary">Update</a>
<a th:href="@{/deleteEmployee/{id}(id=${employee.id})}" class="btn btn-danger">Delete</a>
</td>
</tr>
</tbody>
</table>
<div th:if = "${totalPages > 1}">
<div class = "row col-sm-10">
<div class = "col-sm-2">
Total Rows: [[${totalItems}]]
</div>
<div class = "col-sm-1">
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/page/' + ${i}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">[[${i}]]</a>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
</div>
<div class = "col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${currentPage + 1}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">Next</a>
<span th:unless="${currentPage < totalPages}">Next</span>
</div>
<div class="col-sm-1">
<a th:if="${currentPage < totalPages}" th:href="@{'/page/' + ${totalPages}+ '?sortField=' + ${sortField} + '&sortDir=' + ${sortDir}}">Last</a>
<span th:unless="${currentPage < totalPages}">Last</span>
</div>
</div>
</div>
</div>
</body>
</html>
EmployeeRepository.java
package net.javaguides.springboot.repository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import net.javaguides.springboot.model.Employee;
@Repository
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>net.javaguides</groupId>
<artifactId>springboot-thymeleaf-crud-web-app-spring-data-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-thymeleaf-crud-web-app-spring-data-jdbc</name>
<description>Demo project for Spring Boot and thymeleaf</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
GitHub 存储库链接
GitHub - allwaysoft/springboot-thymeleaf-crud-pagination-sorting-webapp-spring-data-jdbc