1. 引言
在本文中,我们将介绍基于Spring Boot JPA、Bootstrap 和FreeMarker的分页组件。将数据库结果划分为页是许多应用程序中常用的功能。本教程将介绍如何使用FreeMarker模板创建一个漂亮的分页组件。
2. 依赖关系
2.1. Maven 依赖
POC(概念证明)应用程序是使用以下依赖项作为 Maven 项目创建的:
- 启动:弹簧启动器-启动器-web:2.1.5.发布 - 弹簧启动 Web 启动器,
- 启动器:弹簧启动器-数据-jpa:2.1.5.发布 - 弹簧启动数据 JPA 启动器,
- FreeMarker模板引擎,
- 组织:龙目岛:1.18.2 - POJO 类的生成设置器/获取器方法,
- com.h2 数据库:h2:1.4.200 - H2 数据库引擎,
- 组织液基:液基-核心:3.8.8 - 管理和执行数据库更改,
- org.webjars:引导:4.0.0-2 - 用于引导框架的 webjar 库。
2.2. 前端库
- Bootstrap - 用于创建响应式网站的前端框架。
项目 Maven 文件具有以下结构: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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>thymeleaf-bootstrap-pagination</artifactId>
<properties>
<java.version>17</java.version>
</properties>
<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/>
</parent>
<!-- Add typical dependencies for a web application -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>4.16.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>0.45</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime<scope >
</dependency>
</dependencies>
<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3. 模型、视图、控制器层
使用 H2 数据库和 Liquibase 管理数据库更改的示例应用程序。
因此,首先我们需要在application.properties
文件中配置这些库:
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
spring.datasource.username=h2
spring.datasource.password=pass
spring.liquibase.change-log=classpath:/db-scripts/db-changelog-master.xml
在数据库层中,我们创建一个具有字段(title
和 body
)的Post
实体。
package com.frontbackend.thymeleaf.bootstrap.posts.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity(name = "POSTS")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class Post {
@Id
private Long id;
private String title;
private String body;
}
从JpaRepository
扩展的数据访问对象 (PostDAO
) 中,我们将使用采用Pageable
参数的findAll
方法。Pageable
类存储限制和排序结果记录所需的所有信息。
package com.frontbackend.thymeleaf.bootstrap.posts.control.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.Post;
public interface PostDAO extends JpaRepository<Post, Long> {
}
PostService
类负责准备PageRequest
类,并用Paged
实例包装结果:
package com.frontbackend.thymeleaf.bootstrap.posts.control.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import com.frontbackend.thymeleaf.bootstrap.posts.control.dao.PostDAO;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.Post;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.paging.Paged;
import com.frontbackend.thymeleaf.bootstrap.posts.entity.paging.Paging;
@Service
public class PostService {
private final PostDAO postDAO;
@Autowired
public PostService(PostDAO postDAO) {
this.postDAO = postDAO;
}
public Paged<Post> getPage(int pageNumber, int size) {
Sort sort = Sort.by("id");
sort = sort.ascending();
PageRequest request = PageRequest.of(pageNumber - 1, size, sort);
Page<Post> postPage = postDAO.findAll(request);
return new Paged<>(postPage, Paging.of(postPage.getTotalPages(), pageNumber, size));
}
}
Paged
、PageItem、
PageItemType
和 Paging
是用于准备分页组件的实用程序类。
Paged
类具有以下结构:
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
import org.springframework.data.domain.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Paged<T> {
private Page<T> page;
private Paging paging;
}
PageItem
具有以下结构:
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageItem {
private PageItemType pageItemType;
private int index;
private boolean active;
}
PageItemType
是包含两个值的枚举:
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
public enum PageItemType {
DOTS,
PAGE
}
Paging
类负责计算页面和点在分页组件中的显示方式:
package com.frontbackend.thymeleaf.bootstrap.posts.entity.paging;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Paging {
private static final int PAGINATION_STEP = 3;
private boolean nextEnabled;
private boolean prevEnabled;
private int pageSize;
private int pageNumber;
private List<PageItem> items = new ArrayList<>();
public void addPageItems(int from, int to, int pageNumber) {
for (int i = from; i < to; i++) {
items.add(PageItem.builder()
.active(pageNumber != i)
.index(i)
.pageItemType(PageItemType.PAGE)
.build());
}
}
public void last(int pageSize) {
items.add(PageItem.builder()
.active(false)
.pageItemType(PageItemType.DOTS)
.build());
items.add(PageItem.builder()
.active(true)
.index(pageSize)
.pageItemType(PageItemType.PAGE)
.build());
}
public void first(int pageNumber) {
items.add(PageItem.builder()
.active(pageNumber != 1)
.index(1)
.pageItemType(PageItemType.PAGE)
.build());
items.add(PageItem.builder()
.active(false)
.pageItemType(PageItemType.DOTS)
.build());
}
public static Paging of(int totalPages, int pageNumber, int pageSize) {
Paging paging = new Paging();
paging.setPageSize(pageSize);
paging.setNextEnabled(pageNumber != totalPages);
paging.setPrevEnabled(pageNumber != 1);
paging.setPageNumber(pageNumber);
if (totalPages < PAGINATION_STEP * 2 + 6) {
paging.addPageItems(1, totalPages + 1, pageNumber);
} else if (pageNumber < PAGINATION_STEP * 2 + 1) {
paging.addPageItems(1, PAGINATION_STEP * 2 + 4, pageNumber);
paging.last(totalPages);
} else if (pageNumber > totalPages - PAGINATION_STEP * 2) {
paging.first(pageNumber);
paging.addPageItems(totalPages - PAGINATION_STEP * 2 - 2, totalPages + 1, pageNumber);
} else {
paging.first(pageNumber);
paging.addPageItems(pageNumber - PAGINATION_STEP, pageNumber + PAGINATION_STEP + 1, pageNumber);
paging.last(totalPages);
}
return paging;
}
}
GET 请求由PostController
类处理。要更改当前页面和页面大小,我们需要添加两个请求参数:
- 页码 - 当前页码(默认第 1 页),
- 大小 - 页面大小(默认为 5 行):
package com.frontbackend.thymeleaf.bootstrap.posts.boundary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.frontbackend.thymeleaf.bootstrap.posts.control.service.PostService;
@Controller
@RequestMapping("/")
public class PostController {
private final PostService postService;
@Autowired
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping
public String posts(@RequestParam(value = "pageNumber", required = false, defaultValue = "1") int pageNumber,
@RequestParam(value = "size", required = false, defaultValue = "5") int size, Model model) {
model.addAttribute("posts", postService.getPage(pageNumber, size));
return "posts";
}
}
Application
是 Java 类,其主要方法是启动Spring Boot应用程序服务器:
package com.frontbackend.thymeleaf.bootstrap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4. FreeMarker模板
表示层包含单个FreeMarker模板posts.ftlh
:
posts.ftlh
视图具有以下结构:
<!doctype html>
<head>
<title>Spring Boot FreeMarker Application - Bootstrap Table</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
<style>
.panel-table .panel-body {
padding: 0;
}
.panel-table .panel-body .table-bordered {
border-style: none;
margin: 0;
}
.panel-table .panel-body .table-bordered > thead > tr > th:first-of-type {
text-align: center;
width: 100px;
}
.panel-table .panel-body .table-bordered > thead > tr > th:last-of-type,
.panel-table .panel-body .table-bordered > tbody > tr > td:last-of-type {
border-right: 0px;
}
.panel-table .panel-body .table-bordered > thead > tr > th:first-of-type,
.panel-table .panel-body .table-bordered > tbody > tr > td:first-of-type {
border-left: 0px;
}
.panel-table .panel-body .table-bordered > tbody > tr:first-of-type > td {
border-bottom: 0px;
}
.panel-table .panel-body .table-bordered > thead > tr:first-of-type > th {
border-top: 0px;
}
.panel-table .panel-footer .pagination {
margin: 0;
}
/*
used to vertically center elements, may need modification if you're not using default sizes.
*/
.panel-table .panel-footer .col {
line-height: 34px;
height: 34px;
}
.panel-table .panel-heading .col h3 {
line-height: 30px;
height: 30px;
}
.panel-table .panel-body .table-bordered > tbody > tr > td {
line-height: 34px;
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
<div class="container">
<a class="navbar-brand" href="/">FreeMarker - Bootstrap Table</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-item active">
<a class="nav-link" href="#">Home
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">About</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
<section class="h-100">
<div class="container h-100">
<div class="row justify-content-md-center">
<div class="card">
<h1>Post CRUD operation with Freemarker Template</h1>
<a href="/create">Create New Post</a>
<table class="table">
<thead>
<tr>
<th scope="col">Id</th>
<th scope="col">Title</th>
<th scope="col">Body</th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<#list posts.page.content as post>
<tr>
<th scope="row">${post.id}</th>
<td>${post.title}</td>
<td>${post.body}</td>
<td><a href="/update/${post.id}">Update</a></td>
<td><a href="/delete/${post.id}">Delete</a></td>
</tr>
</#list>
</tbody>
</table>
</div>
</div>
<div class="panel-footer">
<div class="row">
<div class="col col-xs-2">
Total items: ${posts.page.totalElements}- item ${(posts.page.number )*posts.page.size +1 } -
${(posts.page.number )*posts.page.size +posts.page.numberOfElements }
</div>
<div class="col col-xs-10">
<nav aria-label="Page navigation" class="paging">
<#if (posts.page.totalPages> 1)>
<ul class="pagination">
<li class="page-item ${(posts.paging.isPrevEnabled())?string('' , 'disabled')}">
<a class="page-link" href="/?pageNumber=${posts.paging.pageNumber - 1}"
tabindex="-1">Previous</a>
</li>
<#list posts.paging.getItems() as item>
<#if (item.pageItemType.name() == 'PAGE')>
<li class="page-item ${(item.index == posts.paging.pageNumber)?string('active' , '')}">
<a class="page-link" href="/?pageNumber=${item.index}">${item.index}</a>
</li>
</#if>
<#if (item.pageItemType.name() == 'DOTS')>
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
</#if>
</#list>
<li class="page-item ${(posts.paging.isNextEnabled())?string('' , 'disabled')}">
<a class="page-link" href="/?pageNumber=${posts.paging.pageNumber + 1}"
>Next</a>
</li>
</ul>
</#if>
</nav>
</div>
</div>
</div>
</div>
</body>
</html>
分页组件存储在导航标记中:<nav aria-label="Page navigation" class="paging">...</nav>
。此代码可以很容易地移动到片段中,但为了示例应用程序的清晰度,我们将其与表一起保存在单个文件中。
5. 输出
正在运行的应用程序位于 URL 下,并提供以下功能:http://locahost:8080
6. 结论
在本文中,我们介绍了如何基于春季JPA和引导框架构建FreeMarker分页组件。我们将弹簧启动应用程序与 H2 内存数据库、JPA 和 Liquibase 结合使用来创建数据库结构,并用样本 100 条记录填充它。
像往常一样,本文中使用的代码位于我们的 https://github.com/allwaysoft/spring-boot-jpa-freemarker-bootstrap-pagination下。