注意
实际上分页并不属于jsp的基础知识范畴,不过分页毕竟是非常常用的一个功能,其中也会涉及到jsp的一些应用,所以还有很有价值一看的。
如果你不满足以下任一条件,请继续阅读,否则请跳过此后的部分,进入下一章: 第 16 章
Never End...。
-
了解如何进行数据库分页。
-
了解如何使用jstl标签替换分页的jsp代码。
所谓的分页,就是要查询的数据太多了,一次性显示出来的话,既不容易查看也影响性能。
就比如我们这里有39条数据,分8页显示出来,每页5条记录。
首先说数据库,里边只有一张表bean,三个字段id, title和add_date。
create table bean(
id bigint,
title varchar(100),
add_date datetime
);
对应的Bean.java咱们就不用写了,只看一看后台去数据库读取数据库的BeanServlet.java。
package anni;
import java.io.*;
import java.sql.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class BeanServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Connection conn = null;
Statement state = null;
ResultSet rs = null;
int pageNo = 1;
try {
pageNo = Integer.parseInt(request.getParameter("pageNo"));
} catch(Exception ex) {
}
if (pageNo < 1) {
pageNo = 1;
}
request.setAttribute("pageNo", pageNo);
List list = new ArrayList();
int count = 0;
try {
conn = DbUtils.getConn();
state = conn.createStatement();
rs = state.executeQuery("select limit " + (pageNo * 5 - 5) + " 5 * from bean");
while (rs.next()) {
Bean bean = new Bean();
bean.setId(rs.getLong(1));
bean.setTitle(rs.getString(2));
bean.setAddDate(rs.getTimestamp(3));
list.add(bean);
}
rs.close();
rs = state.executeQuery("select count(*) from bean");
if (rs.next()) {
count = rs.getInt(1);
}
} catch(Exception ex) {
ex.printStackTrace();
} finally {
DbUtils.close(rs, state, conn);
}
request.setAttribute("count", count);
request.setAttribute("list", list);
request.getRequestDispatcher("/query.jsp").forward(request, response);
}
}
代码长了,分三段来看:
int pageNo = 1;
try {
pageNo = Integer.parseInt(request.getParameter("pageNo"));
} catch(Exception ex) {
}
if (pageNo < 1) {
pageNo = 1;
}
request.setAttribute("pageNo", pageNo);
这里pageNo代表当前的页码,如果没有传递pageNo参数,默认显示第一页,为此我们在解析request中参数时要捕获对应的异常,如果没有输入或者参数不是一个数字时pageNo就还是等于1。
rs = state.executeQuery("select limit " + (pageNo * 5 - 5) + " 5 * from bean");
这里我们拼了一个sql语句,用来从(pageNo * 5 - 5)开始查询五个记录,这条sql语句是hsqldb数据库特有的分页语句。
(pageNo * 5 - 5)用来计算pageNo这页第一条记录的行号,如果是第一页pageNo = 1,pageNo * 5 - 5 = 0。这里的行号跟咱们平常使用的数组索引一样,0代表第一个条记录,这样我们第一页就会显示0,1,2,3,4五条记录。如果是第二页pageNo = 2,pageNo * 5 - 5 = 5,我们就会在第二页看到5,6,7,8,9五条记录。这样正好与我们预期的一致。
rs = state.executeQuery("select count(*) from bean");
最后记得要获得数据库中一共有几条记录,我们要用它来计算一共要分几页。
计算方法是count / 5 + 1,比如现在有39条记录,39 / 5 + 1 = 8,一共分8页。
经过BeanServlet.java的处理,request里有三个变量,list保存当前页面显示的结果,pageNo代表当前页码,count代表数据库中一共有多少条记录,这三个变量会在query.jsp中作为原始数据显示最终的结果。
<tbody>
<%
SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日");
List list = (List) request.getAttribute("list");
if (list != null && list.size() != 0) {
for (int i = 0; i < list.size(); i++) {
Bean bean = (Bean) list.get(i);
%>
<tr class="<%=(i % 2 == 1 ? "odd" : "even")%>">
<td><%=bean.getId()%></td>
<td><%=bean.getTitle()%></td>
<td><%=format.format(bean.getAddDate())%></td>
</tr>
<%
}
} else {
%>
<tr>
<td colspan="3">没有数据</td>
</tr>
<%
}
%>
</tbody>
循环显示list中当前页面的数据并不是新鲜事物,我们只是判断了list是否存在,list里是否有数据,如果这两个条件不符合,就显示“没有数据”作为提示。
下面是重头戏了,仔细研究一下如何使用pageNo(当前页码)和count(数据总数)显示出分页工具栏,虽然只是进行简单的四则运算,但要是平常不注意训练算法,想一次写好也不容易。
分页栏部分的代码如下:
<div>
<%
Integer pageNo = (Integer) request.getAttribute("pageNo");
Integer count = (Integer) request.getAttribute("count");
Integer pageCount = count / 5 + 1;
if (pageNo == 1) {
%>
<span class="unuse">[第一页]</span>
<span class="unuse">[上一页]</span>
<%
} else {
%>
<span><a href="?pageNo=1">[第一页]</a></span>
<span><a href="?pageNo=<%=pageNo - 1%>">[上一页]</a></span>
<%
}
%>
<%
for (int i = 1; i <= pageCount; i++) {
if (i == pageNo) {
%>
<span class="currentPage"><%=i%></span>
<%
} else {
%>
<span><a href="?pageNo=<%=i%>"><%=i%></a></span>
<%
}
}
%>
<%
if (pageNo == pageCount) {
%>
<span class="unuse">[下一页]</span>
<span class="unuse">[最后一页]</span>
<%
} else {
%>
<span><a href="?pageNo=<%=pageNo + 1%>">[下一页]</a></span>
<span><a href="?pageNo=<%=pageCount%>">[最后一页]</a></span>
<%
}
%>
<form style="display:inline;">
<select name="pageNo">
<%
for (int i = 1; i <= pageCount; i++) {
%>
<option value="<%=i%>" <%=(i == pageNo ? "selected" : "")%>><%=i%></option>
<%
}
%>
</select>
<input type="submit" value="go" />
</form>
</div>
首先我们从request中取得pageNo(当前页码)和count(数据总数)两个变量,为了下面计算简便先用它俩计算出总页数 int pageCount = count / 5 + 1; 。
if (pageNo == 1) {
%>
<span class="unuse">[第一页]</span>
<span class="unuse">[上一页]</span>
<%
} else {
%>
<span><a href="?pageNo=1">[第一页]</a></span>
<span><a href="?pageNo=<%=pageNo - 1%>">[上一页]</a></span>
<%
}
%>
这段代码生成“[第一页] [上一页]”,如果pageNo == 1说明当前页已经是第一页了,这时既不需要[第一页]这个链接,也不需要[上一页]了。对应的“[下一页] [最后一页]”处理方法与之类似,只是判断条件要写成pageNo == pageCount,判断是否已经是最后一页了。
<%
for (int i = 1; i <= pageCount; i++) {
if (i == pageNo) {
%>
<span class="currentPage"><%=i%></span>
<%
} else {
%>
<span><a href="?pageNo=<%=i%>"><%=i%></a></span>
<%
}
}
%>
中间页码部分直接使用循环就能输出来,循环从i = 1开始直到i <= 8,最后就是我们看到的1,2,3,4,5,6,7,8。既然pageNo是从1开始算的,我们这里就也从i = 1起步了。如果在i == pageNo的时候,说明这是当前页,不需要链接,再加上一个class="currentPage"突出显示。
<form style="display:inline;">
<select name="pageNo">
<%
for (int i = 1; i <= pageCount; i++) {
%>
<option value="<%=i%>" <%=(i == pageNo ? "selected" : "")%>><%=i%></option>
<%
}
%>
</select>
<input type="submit" value="go" />
</form>
这个select下拉选择框的实现与上面相似,循环一下,遇到i == pageNo的时候就输出一个selected默认显示到select里,在选中想跳转的页码,点击下面的go按钮提交,跳转的效果也前面的超链接相同。
范例在15-01。
jsp的代码让人眼晕,我们现在把分页中所有的jsp代码都换成jstl标签,这些我们要导入三个标签:core,fmt,functions。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
其中core提供循环标签(forEach)和判断标签(if),fmt为我们格式化日期,functions可以判断list的长度。
使用它们,下面显示记录结果的代码最终变成这幅模样。
<tbody>
<c:forEach var="item" items="${list}" varStatus="status">
<c:set var="row" value="${status.index % 2 != 0 ? 'odd' : 'even'}"/>
<tr class="${row}">
<td>${item.id}</td>
<td>${item.title}</td>
<td><fmt:formatDate pattern="yyyy年MM月dd日" value="${item.addDate}"/></td>
</tr>
</c:forEach>
<c:if test="${empty list || fn:length(list) == 0}">
<tr>
<td colspan="3">没有数据</td>
</tr>
</c:if>
</tbody>
forEach用来循环${list}中的内容,把每个元素保存到局部变量item中,供下面使用。
fmt:formatDate是专门用于日期格式化显示的标签,相当于我们刚刚在jsp中使用的SimpleDateFormat,pattern部分设置期望显示的日期格式,最后它就会把value中的${item.addDate}显示成“2008年04-01”的格式。
c:if也是一个非常常用的标签,它用来判断test=""中指定的el表达式的真假,如果是true就执行标签内部的内容,否则就跳过。很难受的是jstl中没有else的部分,想实现else必须写两次if两次test,很麻烦,但真就是jsp的局限。
fn:length(list)是functions标签部分为我们提供的扩展功能,因为el表达式中不能直接调用变量的方法,需要判断list长度的时候无法使用list.size(),只能通过fn:length(list)走一下弯路了,虽然它的功能十分有限,但也为我们提供了一条尽量不写jsp的途径,忍忍吧。
后面分页部分的功能随便帖一下就好,只是循环和判断的组合而已。
<div>
<c:set var="pageCount" value="${count / 5 + 1}"/>
<c:if test="${pageNo == 1}">
<span class="unuse">[第一页]</span>
<span class="unuse">[上一页]</span>
</c:if>
<c:if test="${pageNo != 1}">
<span><a href="?pageNo=1">[第一页]</a></span>
<span><a href="?pageNo=${pageNo - 1}">[上一页]</a></span>
</c:if>
<c:forEach begin="1" end="${pageCount}" varStatus="status">
<c:if test="${status.index == pageNo}">
<span class="currentPage">${status.index}</span>
</c:if>
<c:if test="${status.index != pageNo}">
<span><a href="?pageNo=${status.index}">${status.index}</a></span>
</c:if>
</c:forEach>
<c:if test="${pageNo == pageCount}">
<span class="unuse">[下一页]</span>
<span class="unuse">[最后一页]</span>
</c:if>
<c:if test="${pageNo != pageCount}">
<span><a href="?pageNo=${pageNo + 1}">[下一页]</a></span>
<span><a href="?pageNo=${pageCount}">[最后一页]</a></span>
</c:if>
<form style="display:inline;">
<select name="pageNo">
<c:forEach begin="1" end="${pageCount}" varStatus="status">
<option value="${status.index}" ${status.index == pageNo ? 'selected' : ''}>${status.index}</option>
</c:forEach>
</select>
<input type="submit" value="go" />
</form>
</div>
这里可以看到forEach的另一种用法,begin="1"从i = 1开始循环,end="${pageCount}"会一直循环到i <= pageCount结束,这帮助我们实现简单循环,以后还可以使用varStatus="status"获得当前行号信息,这些变量最后都可以在循环体中使用到。