MyBatisPlus 与 Thymeleaf 全栈分页实战
在现代 Web 开发中,分页是平衡用户体验与系统性能的关键。通过 MyBatisPlus 处理后端数据查询,结合 Thymeleaf 进行前端渲染,可以实现高效的全栈分页方案。本文将直接切入实战,从环境搭建到前后端交互,完整展示这一流程。
技术选型与目标
MyBatisPlus 作为 MyBatis 的增强工具,内置了强大的分页插件,能大幅减少手动编写 SQL 的工作量。Thymeleaf 则提供了优雅的模板引擎能力,支持静态预览和动态绑定。我们的目标是构建一个完整的分页模块,涵盖实体定义、Service 层逻辑、Controller 接口以及前端的 Ajax 请求与页面渲染。
环境搭建与表结构
1. Maven 依赖配置
在 pom.xml 中引入必要的依赖。这里以 PostgreSQL 数据库为例,同时集成 Lombok 简化代码。
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- lombok 代码自动生成组件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- PostgreSql 驱动包 -->
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>2.5.0</version>
</dependency>
2. 示例表结构
我们以城市停水信息数据为例,主要字段包括停水区域、时间、原因等。表结构如下:

对应的实例数据如下:

Java 后台分页实现
1. 实体类定义
使用 Lombok 注解简化 Getter/Setter,配合 MyBatisPlus 注解映射数据库字段。
package org.yelang.pcwater.domain;
import java.io.Serializable;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@TableName(value = "biz_stop_water_info")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class StopWaterInfo implements Serializable {
private static final long serialVersionUID = -1582687729349525826L;
@TableId(value = "pk_id")
private Long pkId;
@TableField(value = "affect_user")
private String affectUser; // 停水客户
@TableField(value = "affected_range")
private String affectedRange; // 停水范围
@TableField(value = "affect_region")
private String affectRegion; // 停水区域
@TableField(value = "created_on")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createdOn; // 原创建时间
@TableField(value = "inform_emergency_plan")
private String informEmergencyPlan; // 应急计划
@TableField(value = "inform_id")
private Integer informId; // 信息标识
@TableField(value = "inform_remark")
private String informRemark; // 备注信息
@TableField(value = "main_lead_path")
private String mainLeadPath; // 停水主管径
private String position; // 停水地点
private String reason; // 停水原因
@TableField(value = "service_phone")
private String servicePhone; // 服务热线
@TableField(value = "show_end_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@SerializedName("showenddate")
private Date showEndDate; // 显示结束时间
@TableField(value = "stop_end_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date stopEndTime; // 停水结束时间
@TableField(value = "stop_start_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date stopStartTime; // 停水开始时间
@TableField(value = "stop_time")
@SerializedName("stoptime")
private String stopTime; // 停水时间
@TableField(value = "stopwater_type")
private String stopwaterType; // 停水类型
@TableField(value = "supply_emergency_plan")
private String supplyEmergencyPlan; // 应急供应计划
@TableField(value = "supply_remark")
private String supplyRemark; // 供应备注
private String title; // 停水标题
private String type; // 类型
@TableField(value = "regin_name")
private String reginName; // 区域名称
@TableField(value = "sync_sno")
private Long syncSno; // 同步批次号
}
2. 业务层分页逻辑
核心在于使用 IPage 对象封装分页信息。我们在 Service 接口中定义分页方法,并在实现类中利用 QueryWrapper 构建查询条件。
/**
* 根据区域查询分页停水信息列表
*/
IPage<StopWaterInfo> page(Integer pageNum, Integer pageSize, String regionName);
具体实现如下:
@Override
public IPage<StopWaterInfo> page(Integer pageNum, Integer pageSize, String regionName) {
QueryWrapper<StopWaterInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.like("regin_name", regionName);
queryWrapper.orderByDesc("created_on");
Page<StopWaterInfo> page = new Page<>(pageNum, pageSize);
return this.baseMapper.selectPage(page, queryWrapper);
}
这里构造 Page 对象传入当前页码和每页条数,配合 QueryWrapper 传递给 Mapper 执行查询。
3. 控制层接口
Controller 负责接收前端参数并返回结果。注意设置默认值以避免空指针异常。
@PostMapping("/list")
@ResponseBody
public AjaxResult list(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "5") Integer pageSize,
@RequestParam(defaultValue = "") String regionName) {
IPage<StopWaterInfo> page = waterInfoService.page(pageNum, pageSize, regionName);
AjaxResult result = AjaxResult.success();
result.put("data", page);
return result;
}
Thymeleaf 分页集成
1. 表格数据渲染
前端通过 Ajax 请求后端接口,获取 JSON 数据后动态填充表格。以下是 HTML 结构和 JS 逻辑。
HTML 结构:
<div>
<table>
<thead>
<tr>
<th>创建时间</th>
<th>停水地点</th>
<th>原因</th>
</tr>
</thead>
<tbody id="table-body">
<!-- 数据行将通过 JS 插入 -->
</tbody>
</table>
</div>
JS 加载逻辑:
function renderTablePage(page) {
currentPage = page;
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
$.ajax({
type: "post",
url: ctx + "datasync/list",
dataType: "json",
cache: false,
processData: true,
data: { "pageNum": page, "pageSize": EVENTS_PER_PAGE, "regionName": "" },
success: function(result) {
const pageData = result.data.records;
const totalPages = result.data.pages;
if (pageData.length === 0) {
tableBody.innerHTML = `<tr><td colspan="5">未找到符合条件的停水事件。</td></tr>`;
} else {
pageData.forEach(event => {
const row = tableBody.insertRow();
row.innerHTML = `
<td>${event.createdOn}</td>
<td>${event.position}</td>
<td>${event.reason}</td>
`;
});
}
renderPagination(totalPages);
},
error: function() {
tableBody.innerHTML = `<tr><td colspan="5">未找到符合条件的停水事件。</td></tr>`;
}
});
}
2. 分页条组件
分页条需要独立容器,并根据总页数动态生成上一页/下一页按钮。
HTML 容器:
<div>
<nav aria-label="Table Pagination">
<ul id="table-pagination"></ul>
</nav>
</div>
JS 渲染逻辑(修复了原始片段中的语法错误):
function renderPagination(totalPages) {
const paginationContainer = document.getElementById('table-pagination');
paginationContainer.innerHTML = '';
if (totalPages <= 1) return;
// 上一页
const prevDisabled = currentPage <= 1 ? 'disabled' : '';
paginationContainer.innerHTML += `
<li ${prevDisabled}>
<a href="#" onclick="changePage(${totalPages},${currentPage - 1})" aria-label="Previous">前一页</a>
</li>
`;
// 下一页
const nextDisabled = currentPage >= totalPages ? 'disabled' : '';
paginationContainer.innerHTML += `
<li ${nextDisabled}>
<a href="#" onclick="changePage(${totalPages},${currentPage + 1})" aria-label="Next">后一页</a>
</li>
`;
}
最终效果如下:

常见问题排查
分页不生效
如果页面展示了数据但下方没有分页条,或者所有数据一次性加载,通常是分页插件未正确配置导致的。

解决方案
需要在 Spring Boot 配置类中注册 MybatisPlusInterceptor 并添加 PaginationInnerInterceptor。
package org.yelang.pcwater.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor i = new MybatisPlusInterceptor();
i.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
return i;
}
}
配置完成后重启服务,分页功能即可正常运作。
总结
本文详细演示了基于 MyBatisPlus 和 Thymeleaf 的全栈分页实现路径。从 Maven 依赖引入、实体映射、Service 层分页查询,到 Controller 接口暴露及前端 Ajax 交互,每一步都提供了可运行的代码参考。重点解决了分页插件配置缺失这一常见坑点。掌握这套方案,能有效提升开发效率并确保系统的稳定性。


