JavaEE学习笔记 - 分页

分页技术在Java Web开发中是必须要用到的,虽然它不难,但长时间不接触渐渐就会对其淡忘了,毕竟这种轮子在项目开发中并不会经常被重造。

原理分析

平常,我们就使用到各种搜索引擎、论坛,里面也有分页功能,那大可以模仿它们的来做出属于我们自己的分页功能。

在动手做之前,首先是根据分页功能推理出它内部底层到底是怎么运作起来的。

先不用去想分页条这么复杂的东西,我们先把最基础的东西慢慢开始构思就行了。

最简单地,我们能够首先想到的肯定是每页所显示的实体数据,比如:使用谷歌百度搜索出来显示的是搜索结果列表、论坛贴吧显示的则是对应的帖子列表、淘宝显示的是商品列表。再来,就是每页所显示实体数据的个数,既然是我们自己的分页,所以我们可以根据自己喜好随便限制每页显示多少个实体数据,或者是做成可以让用户动态地修改这个数值。这两个数据是最好理解的。

试想一下,现在数据库商品表中有100条记录,即显示出来的是100个商品数据,我们想要每页都显示5个商品数据。自然而然,我们肯定要向用户显示所有的页码,那在循环的时候肯定要用到总页数这个数据,总页数则需要总记录数才能计算得出。

同样地,在回显数据的时候,肯定是要知道现在显示的是第几页数据,把当前页码高亮一下以表示当前页,而在MySQL数据库底层进行分页查询时,使用的是limit,那必须封装一个数据表示当前页第一条实体数据在数据库中的位置

经过以上的推断,分页功能的大体架构就出来了,但是,我们可以想到,随着数据的增多,那样总页数就会越来越多,这样在显示的时候就把所有的页码都会遍历出来,用户体验会极大地降低,同时也不美观。

所以,这时就像各种搜索引擎、论坛一样,给它增加一个分页条:只能显示指定数量的页码,比如常规的10个。所以,我们首先就能得出的是分页条宽度这个数据,而随着当前页面的变化,分页条就会有“滚动”一样的变化。其实就是要封装分页条起始页码分分页条结束页码

结论

综上所述,要完成一个最简单最容易理解的分页功能,需要在分页类中封装这9个数据:

  • 当前页的实体数据列表 - list
  • 每页实体数据个数 - pageSize
  • 实体数据总记录数 - recordCount
  • 当前页页码 - currentPage
  • 当前页在数据库表中的起始位置 - offset
  • 总页数 - pageCount
  • 分页条宽度 - width
  • 分页条起始页码 - begin
  • 分页条结束页码 - end

小小的分页功能,底层居然要封装这么多数据才能驱动起来,虽然看起来9个数据变量很多,但是需要经过各种判断和计算而产生变化的数据并不多。

红色标注的数据变量都是由外界传递进来的,当前页的实体数据列表和实体数据总记录数这两个都是从数据库中查询出来的,而当前页页码是用户选择传递进来的。

而蓝色标注的数据变量是一开始就应该给默认值的,或者由用户的喜好让用户传值进来修改的。

而其他数据变量,都是要根据其他数据变量计算而得出来的。

1.计算当前页在数据库表中的起始位置(offset)

数据库标示数据记录的索引是从0开始,
所以,假设每页实体数据个数是3,那么,第1页的起始位置是0,第2页的起始位置则是3,第3页的起始位置则是6,其他的如此类推下去。

当有了当前页页码(currentpage)每页实体数据个数(pageSize)这两个值时,就可以计算这个数据变量了。
计算公式:offset = (currentPage - 1) * pageSize

2.计算总页数(pageCount)

总页数的计算是最简单不过了,两个数据相除一下就完事了。

当有了实体数据总记录数(recordCount)每页实体数据个数(pageSize)这两个数值,那就可以计算总页数(pageCount)了。
计算公式:pageCount = recordCount / pageSize + (recordCount % pageSize == 0 ? 0 : 1)


那么,现在剩下要计算的数据变量只剩2个,都是关于分页条的。

首先要明确的是,起始页码和结束页码的变化与当前页页码总页数分页条宽度这3个数据变量有关。

分页条的变化,大体分为2种情况。
第1种情况很简单,实体数据不多,也就是总页数很少并且少于等于分页条宽度的时候,那么分页条的起始页码只能是1,而它的结束页码就是总页数了
反之,第2种情况就是总页数大于分页条宽度的时候,而这种情况里面又细分了几种情况,事情就变得复杂许多了。

我们可以通过观察Google搜索里分页条的变化从而摸索出其中的规律。

从上图也可以非常清楚地看到:当前页页码为7的时候,分页条就开始产生变化了。所以,在这里就可以画图推断了。

这里,假设总页数为15,并且罗列出当前页页码变化时分页条起始页码和结束页码对应变化的数据。现在可以得出分页条的变化分为3种情况:

  • 当前页页码小于等于分页条宽度一半
  • 当前页页码大于总页数减去分页条宽度一半
  • 当前页页码大于分页条宽度一半并且小于等于总页数减去分页条宽度一半

根据这3种情况,计算公式也随之而推导出来了。

不过现阶段还没完,如果客户要求自定义分页条宽度呢?这些计算公式肯定不能满足这种需求的,图中的计算公式只能满足分页条宽度为偶数的情况。

所以说只要把分页条宽度为奇数这种情况的计算公式也推导出来,这样无论分页条宽度如何变化,分页条都能正常工作了。

其实分页条宽度为奇数相比之前分页条宽度为偶数所得出的计算公式变化并不大,下面是假设分页条宽度为5时所画的图。

代码示例

这里,就用普通的Web程序来演示分页功能。

从底层开始开发,那样,先把数据库以及对应的表构建起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE DATABASE paging
CHARACTER SET utf8
COLLATE utf8_general_ci;

USE paging;

CREATE TABLE book(
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(60) NOT NULL UNIQUE,
author VARCHAR(20) NOT NULL,
price DECIMAL(8, 2),
publisher VARCHAR(255),
type VARCHAR(30)
);

然后,可以根据表来创建对应的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Book {
private String id;
private String name;
private String author;
private double price;
private String publisher;
private String type;

public Book() {

}

public Book(String id, String name, String author, double price,
String publisher, String type) {
this.id = id;
this.name = name;
this.author = author;
this.price = price;
this.publisher = publisher;
this.type = type;
}

@Override
public String toString() {
return "Book [id=" + id + ", name=" + name + ", author=" + author
+ ", price=" + price + ", publisher=" + publisher + ", type="
+ type + "]";
}

// 省略get/set方法
}

接着,则要来构建并实现封装分页数据的分页类,它是整个分页功能最核心的敌方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
public class Page<T> {
// 一页的数据
private List<T> list = null;
// 一页数据的起始位置
private int offset = 0;
// 一页数据的大小
private int pageSize = 2;
// 总记录数
private int recordCount = 0;
// 总页数
private int pageCount = 0;
//当前页索引
private int currentPage = 0;

// 分页条宽度
private int width = 10;
// 分页条起始位置
private int begin;
// 分页条结束位置
private int end;

public Page(int rCount, int currPage) {
recordCount = rCount;
currentPage = currPage;

pageCount = recordCount / pageSize + (recordCount % pageSize == 0 ? 0 : 1);

offset = (currentPage - 1) * pageSize;

// 计算分页条起始和结束的位置
// 具体分为两种情况:
// 1.总页数小于等于分页条宽度
// 2.总页数大于分页条宽度
if (pageCount <= width) {
begin = 1;
end = pageCount;
} else {
// 当总页数多于分页条宽度时, 又分3种情况
// 1.当前页索引小于等于分页条一半宽度
// 2.当前页索引大于(总页数-分页条一半宽度)
// 3.当前页索引介于条件1, 2之间
int pivot = width / 2;
if (currentPage <= pivot) {
begin = 1;
end = width;
} else if (currentPage > pageCount - pivot) {
begin = pageCount - width + 1;
end = pageCount;
} else {
begin = currentPage - pivot;
// width & 0x1 ^ 0x1的作用相当于width % 2 == 0 : 1 : 0
end = currentPage + pivot - (width & 0x1 ^ 0x1);
}
}
}

public List<T> getList() {
return list;
}

public void setList(List<T> list) {
this.list = list;
}

public int getOffset() {
return offset;
}

public int getPageSize() {
return pageSize;
}

public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}

public int getRecordCount() {
return recordCount;
}

public void setRecordCount(int recordCount) {
this.recordCount = recordCount;
}

public int getPageCount() {
return pageCount;
}

public void setPageCount(int pageCount) {
this.pageCount = pageCount;
}

public int getCurrentPage() {
return currentPage;
}

public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}

public int getWidth() {
return width;
}

public void setWidth(int width) {
this.width = width;
}

public int getBegin() {
return begin;
}

public void setBegin(int begin) {
this.begin = begin;
}

public int getEnd() {
return end;
}

public void setEnd(int end) {
this.end = end;
}
}

Page类的构造函数里运用了之前推导的计算公式,将每个分页要用到的数据变量都计算出来。

那接下来的事情就很简单了,我们只要随便往数据库表里面插入一些数据,在查询出来显示到页面上即可。

操作图书数据的Dao类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
public class BookDao {
/**
* 获取一页图书数据
*
* @param offset
* @param size
* @return
*/
public List<Book> getPageData(int offset, int size) {
List<Book> list = new ArrayList<Book>();

Connection con = null;
PreparedStatement pStmt = null;
ResultSet rs = null;

try {
con = DBUtils.getConnection();
String sql = "select * from book limit ?, ?";
pStmt = con.prepareStatement(sql);

// 设置参数
pStmt.setInt(1, offset);
pStmt.setInt(2, size);
// 执行查询
rs = pStmt.executeQuery();
while (rs.next()) {
String id = rs.getString(1);
String name = rs.getString(2);
String author = rs.getString(3);
double price = rs.getDouble(4);
String publisher = rs.getString(5);
String type = rs.getString(6);

Book book = new Book(id, name, author, price, publisher, type);
list.add(book);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, pStmt, rs);
}

return list;
}

/**
* 获取统计的图书记录数
*
* @return
*/
public int getCount() {
int count = 0;

Connection con = null;
Statement stmt = null;
ResultSet rs = null;

try {
con = DBUtils.getConnection();
stmt = con.createStatement();
String sql = "select count(*) from book";
rs = stmt.executeQuery(sql);
rs.next();
count = rs.getInt(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, stmt, rs);
}

return count;
}

/**
* 插入图书数据
*
* @param book
*/
public void insert(Book book) {
Connection con = null;
PreparedStatement pStmt = null;

try {
con = DBUtils.getConnection();
String sql = "insert into book values(?, ?, ?, ?, ?, ?)";
pStmt = con.prepareStatement(sql);

// 设置参数
pStmt.setString(1, book.getId());
pStmt.setString(2, book.getName());
pStmt.setString(3, book.getAuthor());
pStmt.setDouble(4, book.getPrice());
pStmt.setString(5, book.getPublisher());
pStmt.setString(6, book.getType());
// 执行
pStmt.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, pStmt, null);
}
}

/**
* 删除所有图书数据
*/
public void deleteAll() {
Connection con = null;
Statement stmt = null;

try {
con = DBUtils.getConnection();
stmt = con.createStatement();
String sql = "delete from book";
stmt.execute(sql);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, stmt, null);
}
}
}

写一个测试方法,随便插入一些图书数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testInsert() throws Exception {
BookDao bookDao = new BookDao();
for (int i = 0; i < 50; i++) {
Book book = new Book(UUID.randomUUID().toString(),
"Thinking In Java" + i,
"makwan" + i,
60.0 + i,
"机械工业出版社" + i,
"Java编程语言");
bookDao.insert(book);
}
}

处理图书数据的服务层也是比较重要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BookService {
private static BookDao bookDao = new BookDao();

public Page<Book> getPageData(String currentPage) {
Page<Book> page = null;
try {
// 获取总记录数
int count = bookDao.getCount();
if (count > 0) {
int cp = StringUtils.isBlank(currentPage) ? 1
: Integer.parseInt(currentPage);
page = new Page<Book>(count, cp);
// 获取指定页的数据
List<Book> list = bookDao.getPageData(page.getOffset(), page.getPageSize());
page.setList(list);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return page;
}
}

我们可以看到这就像之前分析的一样,用户提供当前页页码传递进来,并且总记录数也查询出来,这2个数据是构成Page对象的关键所在。

其他无关要紧的层就不再一一列举了。现在再来编写一个页面分页显示数据即可。这是用于显示图书分页数据的jsp页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>图书信息展示页面</title>

<style type="text/css">
.pagingbar {
text-align: center;
}

a {
text-decoration: none;
}
</style>
</head>
<body>
<!-- 数据列表 -->
<table align="center" border="1">
<thead>
<tr>
<th>编号</th>
<th>书名</th>
<th>作者</th>
<th>价格</th>
<th>出版社</th>
<th>类型</th>
</tr>
</thead>
<tbody>
<c:choose>
<c:when test="${not empty requestScope.page}">
<c:forEach items="${requestScope.page.list}" var="book">
<tr>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.author}</td>
<td>${book.price}</td>
<td>${book.publisher}</td>
<td>${book.type}</td>
</tr>
</c:forEach>
</c:when>
<c:otherwise>
<tr>
<td colspan="9">
<font color="red">当前没有数据</font>
</td>
</tr>
</c:otherwise>
</c:choose>
</tbody>
</table>

<!-- 分页条 -->
<div class="pagingbar">
<c:if test="${not empty requestScope.page}">
<c:if test="${requestScope.page.currentPage > 1}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=1">首页</a>
</c:if>
<c:if test="${requestScope.page.currentPage > 1}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${requestScope.page.currentPage - 1}">
上一页
</a>
</c:if>
<c:forEach begin="${requestScope.page.begin}" end="${requestScope.page.end}" step="1" var="pageNum">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${pageNum}"
${pageNum == requestScope.page.currentPage ? "style='font-size:20px;color:red;'" : "style='color:blue;'"}>
${pageNum}
</a>
</c:forEach>
<c:if test="${requestScope.page.currentPage < requestScope.page.pageCount}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${requestScope.page.currentPage + 1}">
下一页
</a>
</c:if>
<c:if test="${requestScope.page.currentPage < requestScope.page.pageCount}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${requestScope.page.pageCount}">
尾页
</a>
</c:if>
</c:if>
<form action="${pageContext.request.contextPath}/bookservlet?method=getPageData" method="post" style="display: inline;">
<input type="text" name="currentPage" style="width: 20px"/>
<input type="submit" value="Go">
</form>
当前 ${requestScope.page.currentPage}页
共 ${requestScope.page.pageCount}页
共 ${requestScope.page.recordCount}条记录
</div>
</body>
</html>

分页功能正如上面所演示的一样,效果如下图。