1. 概述

本文将带你快速实现一个基于 Spring 与 Thymeleaf 的分页列表展示功能

我们不会从零讲起 Spring 和 Thymeleaf 的集成细节(这属于基础操作),如果你还不熟悉,建议先查阅我们之前的文章:Spring MVC 中使用 Thymeleaf。本文聚焦于 分页逻辑与前后端协同展示,属于实战型内容,踩坑点也会一并指出。

2. Maven 依赖

除了常规的 Spring Web 和 Spring Boot 相关依赖外,我们需要引入以下两个关键依赖:

Thymeleaf Spring5 集成包:用于模板渲染
Spring Data Commons:提供 PagePageable 等分页核心接口

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
    <version>3.0.11.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-commons</artifactId>
    <version>2.3.2.RELEASE</version>
</dependency>

💡 提示:最新版本可前往 Maven Central 搜索 thymeleaf-spring5spring-data-commons 获取。

⚠️ 注意:如果你用的是 Spring Boot,推荐直接使用 spring-boot-starter-thymeleaf,它已内置了这些依赖,无需手动添加。

3. 模型定义

我们以一个图书列表为例,演示分页功能。

定义一个简单的 Book 类,包含 ID 和书名:

public class Book {
    private int id;
    private String name;

    // 全参构造、getter、setter 省略
    // 实际项目中建议使用 Lombok @Data
}

数据源我们暂时用静态工具类生成(BookUtils.buildBooks()),模拟数据库返回的列表。

4. 分页服务实现

核心逻辑在 BookService 中完成。这里我们不依赖 JPA,而是手动实现内存分页,便于理解原理。

@Service
public class BookService {

    final private List<Book> books = BookUtils.buildBooks();

    public Page<Book> findPaginated(Pageable pageable) {
        int pageSize = pageable.getPageSize();
        int currentPage = pageable.getPageNumber();
        int startItem = currentPage * pageSize;
        List<Book> list;

        if (books.size() < startItem) {
            list = Collections.emptyList();
        } else {
            int toIndex = Math.min(startItem + pageSize, books.size());
            list = books.subList(startItem, toIndex);
        }

        Page<Book> bookPage
          = new PageImpl<Book>(list, PageRequest.of(currentPage, pageSize), books.size());

        return bookPage;
    }
}

关键点解析:

  • Pageable 是 Spring Data 提供的分页参数接口,封装了页码和页大小
  • PageImplPage 接口的实现类,用于构造分页结果,需传入:
    • 当前页数据列表
    • 分页请求对象(PageRequest
    • 总记录数(用于计算总页数)
  • ⚠️ 注意:Pageable.getPageNumber() 从 0 开始,前端通常从 1 开始,所以 controller 中要 -1

这个实现虽然简单粗暴,但足够清晰,适合理解分页机制。真实项目中若使用 JPA,JpaRepository 直接支持 Page<T> findAll(Pageable),一行代码搞定。

5. Spring 控制器

控制器负责接收分页参数,调用服务层,并将数据交给视图渲染。

@Controller
public class BookController {

    @Autowired
    private BookService bookService;

    @RequestMapping(value = "/listBooks", method = RequestMethod.GET)
    public String listBooks(
      Model model, 
      @RequestParam("page") Optional<Integer> page, 
      @RequestParam("size") Optional<Integer> size) {
        int currentPage = page.orElse(1);
        int pageSize = size.orElse(5);

        Page<Book> bookPage = bookService.findPaginated(PageRequest.of(currentPage - 1, pageSize));

        model.addAttribute("bookPage", bookPage);

        int totalPages = bookPage.getTotalPages();
        if (totalPages > 0) {
            List<Integer> pageNumbers = IntStream.rangeClosed(1, totalPages)
                .boxed()
                .collect(Collectors.toList());
            model.addAttribute("pageNumbers", pageNumbers);
        }

        return "listBooks.html";
    }
}

参数说明:

  • /listBooks:默认访问第 1 页,每页 5 条
  • /listBooks?page=2&size=6:访问第 2 页,每页 6 条

数据准备要点:

  • bookPage:Thymeleaf 模板中用于遍历当前页数据和获取分页元信息(如总页数、当前页码等)
  • pageNumbers:生成页码列表 [1, 2, 3, ...],用于前端生成页码链接

⚠️ 踩坑提醒:PageRequest.of(currentPage - 1, pageSize) 这里必须减 1,因为 Spring Data 的页码从 0 开始!

6. Thymeleaf 模板

创建 templates/listBooks.html,使用 Thymeleaf 渲染分页列表。

<table border="1">
    <thead>
        <tr>
            <th th:text="#{msg.id}" />
            <th th:text="#{msg.name}" />
        </tr>
    </thead>
    <tbody>
        <tr th:each="book, iStat : ${bookPage.content}"
            th:style="${iStat.odd}? 'font-weight: bold;'"
            th:alt-title="${iStat.even}? 'even' : 'odd'">
            <td th:text="${book.id}" />
            <td th:text="${book.name}" />
        </tr>
    </tbody>
</table>
<div th:if="${bookPage.totalPages > 0}" class="pagination"
    th:each="pageNumber : ${pageNumbers}">
    <a th:href="@{/listBooks(size=${bookPage.size}, page=${pageNumber})}"
        th:text=${pageNumber}
        th:class="${pageNumber==bookPage.number + 1} ? active"></a>
</div>

模板解析:

表格部分:

  • th:each="book, iStat : ${bookPage.content}"
    遍历当前页的书籍列表,iStat 是状态变量,可用于判断奇偶行
  • th:style="${iStat.odd}? 'font-weight: bold;'
    奇数行加粗,简单实现隔行高亮

分页部分:

  • th:if="${bookPage.totalPages > 0}"
    只有总页数大于 0 才显示分页栏
  • th:each="pageNumber : ${pageNumbers}"
    遍历页码列表生成链接
  • th:href="@{/listBooks(size=${bookPage.size}, page=${pageNumber})}"
    使用 Thymeleaf URL 表达式构造带参数的请求链接
  • th:class="${pageNumber==bookPage.number + 1} ? active"
    当前页高亮显示(注意 bookPage.number 从 0 开始,所以要 +1

✅ 效果:点击页码跳转,当前页链接样式为 active,便于 CSS 定制。

7. 总结

本文通过一个简单示例,完整展示了 Spring + Thymeleaf 实现分页列表的全流程

  1. ✅ 使用 Pageable 接收分页参数
  2. ✅ 手动实现 PageImpl 构建分页结果(或使用 JPA 自动支持)
  3. ✅ Controller 传递 Page 和页码列表到视图
  4. ✅ Thymeleaf 模板渲染数据与分页链接

虽然示例基于内存数据,但结构完全适用于真实项目。后续可结合 Spring Data JPA 实现数据库分页,性能更优。

所有代码示例均可在 GitHub 获取:
👉 https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-thymeleaf-5


原始标题:Spring with Thymeleaf Pagination for a List