分頁是 Web 應用最基礎的功能之一。本文採用經典三層架構 (Model/DAO/Service) + Servlet 控制器 + JSP 視圖,構建一個完整、可復用的分頁組件,涵蓋 PageBean 封裝、邊界處理、前端交互 (首頁/末頁/上下頁/下拉跳轉/輸入框跳轉) 及 JSTL 改造。
整體架構
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ JSP │◀───▶│ Servlet │◀───▶│ Service │◀───▶│ DAO │
│ 視圖層 │ │ 控制層 │ │ 業務層 │ │ 數據層 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
▲ │
│ ┌──────────────┐ │
└───────────────────▶│ PageBean │◀──────────┘
│ 分頁數據封裝 │
└──────────────┘
領域模型
// Blog.java 實體類
public class Blog {
private int id;
private int categoryId;
private String title;
private String content;
private Date createdTime;
// getter/setter/toString 省略
}
分頁核心:PageBean 封裝所有分頁元數據
public class BlogPage {
private List pageRecords; // 當前頁記錄
private int pageNo; // 當前頁碼 (1-based)
private int pageNoStart; // 當前頁起始索引 (0-based, 用於 LIMIT)
private int pageSize = 5; // 每頁條數
private int totalRecord; // 總記錄數
private int totalPage; // 總頁數
/** 單一構造器:由 Service 傳入 pageNo、totalRecord 自動推算其餘 */
public BlogPage(int pageNo, int totalRecord) {
this.totalRecord = totalRecord;
this.totalPage = (totalRecord % pageSize == 0)
? totalRecord / pageSize
: totalRecord / pageSize + 1;
// 邊界修正
if (pageNo <= 1) this.pageNo = 1;
else if (pageNo >= totalPage) this.pageNo = totalPage;
else this.pageNo = pageNo;
this.pageNoStart = (this.pageNo - 1) * pageSize;
}
// getter/setter 省略
}
DAO 層:MySQL LIMIT 分頁查詢
public class BlogDao {
// 分頁查詢:LIMIT offset, size
public List getPageRecord(int offset, int pageSize) {
String sql = "SELECT * FROM blog LIMIT ?, ?";
try (Connection conn = JDBCUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, offset);
ps.setInt(2, pageSize);
ResultSet rs = ps.executeQuery();
List list = new ArrayList<>();
while (rs.next()) {
Blog b = new Blog();
b.setId(rs.getInt("id"));
b.setCategoryId(rs.getInt("category_id"));
b.setTitle(rs.getString("title"));
b.setContent(rs.getString("content"));
b.setCreatedTime(rs.getDate("created_time"));
list.add(b);
}
return list;
} catch (SQLException e) { throw new RuntimeException(e); }
}
// 總記錄數
public int getTotal() {
String sql = "SELECT COUNT(*) FROM blog";
try (Connection conn = JDBCUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getInt(1) : 0;
} catch (SQLException e) { throw new RuntimeException(e); }
}
}
Service 層:組裝 PageBean
public class BlogService {
private BlogDao dao = new BlogDao();
public BlogPage findPageRecord(int pageNo) {
int total = dao.getTotal();
BlogPage page = new BlogPage(pageNo, total);
List records = dao.getPageRecord(page.getPageNoStart(), page.getPageSize());
page.setPageRecords(records);
return page;
}
}
Servlet 控制器:處理請求參數、轉發
@WebServlet("/BlogSplitServlet")
public class BlogSplitServlet extends HttpServlet {
private BlogService service = new BlogService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
// 1. 獲取頁碼參數,預設第 1 頁
String pageNoStr = req.getParameter("pageno");
int pageNo = 1;
if (pageNoStr != null) {
try { pageNo = Integer.parseInt(pageNoStr); }
catch (NumberFormatException ignored) {}
}
// 2. 業務處理
BlogPage page = service.findPageRecord(pageNo);
// 3. 尾頁補白:保證表格行數一致 (可選,視 UI 需求)
List list = page.getPageRecords();
if (list.size() < page.getPageSize()) {
for (int i = list.size(); i < page.getPageSize(); i++) list.add(null);
}
// 4. 轉發到 JSP
req.setAttribute("blogPage", page);
req.getRequestDispatcher("/blogPage.jsp").forward(req, resp);
}
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException { doGet(req, resp); }
}
JSP 視圖:原始版 (Scriptlet) 與 JSTL 版對比
版本 A:Scriptlet 版 (教學/舊專案)
<%@ page import="java.util.*,model.Blog,model.BlogPage" %>
<%
BlogPage page = (BlogPage) request.getAttribute("blogPage");
List list = page.getPageRecords();
%>
| ID | 標題 | 內容 | 創建時間 |
<% for (Blog b : list) {
if (b != null) { %>
| <%=b.getId()%> | <%=b.getTitle()%> |
<%=b.getContent()%> | <%=b.getCreatedTime()%> |
<% } else { %>
| | | |
<% } } %>
版本 B:JSTL 版 (推薦,關閉 Scriptlet)
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
| ID | 標題 | 內容 | 創建時間 |
| ${b.id} | ${b.title} | ${b.content} | ${b.createdTime} |
| | | |
前端交互邏輯說明
| 控件 | 觸發事件 | 跳轉邏輯 |
| 下拉選單 | onchange | 直接跳轉到選中頁碼 |
| 輸入框 | onblur | 失焦時跳轉輸入頁碼 (需前端校驗數字範圍) |
| 首頁/尾頁 | click | 固定 pageno=1 / pageno=totalPage |
| 上一頁/下一頁 | click | 邊界保護:最小 1、最大 totalPage |
關鍵優化與擴展點
- 計數查詢優化:大數據量時 `COUNT(*)` 慢,可用觸發器維護計數表、或估算值 (EXPLAIN)。
- 游標分頁:`WHERE id > lastId ORDER BY id LIMIT size` 避免深分頁 OFFSET 掃描慢。
- PageBean 泛型化:`PageBean` 復用於所有實體。
- 前端組件化:提取分頁片段為 `pagination.jspf` 或 Vue/React 組件。
- RESTful 風格:`/blogs?page=2&size=10` 替代 `pageno` 參數。
- 緩存總數:Redis 緩存 `totalRecord`,定時/事件驅動刷新。
常見坑點檢查清單
- ✅ `pageNo` 參數校驗:非數字、負數、超過總頁數 → 修正為邊界值。
- ✅ `LIMIT` 參數順序:MySQL `LIMIT offset, size`,PostgreSQL `LIMIT size OFFSET offset`。
- ✅ 尾頁補白邏輯放在 Servlet (控制層) 而非 JSP,保持視圖純粹。
- ✅ JSTL 表達式語言 (EL) 需啟用:`<%@ page isELIgnored="false" %>` 或 web.xml 配置。
- ✅ 編碼統一:`request.setCharacterEncoding("UTF-8")`、頁面 `pageEncoding="UTF-8"`、資料庫連接 `characterEncoding=utf8`。