最近在做一个小程序,用到了JSP的分页。虽然只是最简单的分页,但是还是花了我不少时间。这看似简单的功能,实现起来还是稍微有点麻烦。实现分页功能,需要知道数据的总个数,每页应该有多少条数据,以及当前页码。假如总共有300条数据,每页20条,那么应该就有15页;假设有301条数据,每页20条,这时候就需要16页。因此,总页数可以这样计算:总页数=数据总数%每页条数==0?数据总数/每页条数:数据总数/每页条数 1
。为了能显示当前页的数据,我们需要知道当前页码,然后根据当前页码计算应该显示哪些数据。因此,我们还需要一个参数来跟踪当前页码。
知道了这些,就可以开始分页的实现了。
简单分页
首先来看看最简单的分页。我们先不考虑数据库如何分页,假设现在我们直接获取到了所有数据,只考虑如何将这些数据分页。
后端代码
首先我们需要一个实体类,其他方法已省略。
代码语言:javascript复制public class User {
private int id;
private String name;
private String password;
private LocalDate birthday;
}
然后需要一个数据访问层的接口:
代码语言:javascript复制public interface UserRepository {
List<User> listAll();
}
然后我们来实现这个接口,作为我们的数据源。
代码语言:javascript复制public class MemoryUserRepository implements UserRepository {
public static final int COUNTS = 302;
@Override
public List<User> listAll() {
List<User> users = new ArrayList<>();
for (int i = 0; i < COUNTS; i) {
User user = new User();
user.setId(i 1);
user.setName("用户" i 1);
user.setPassword("12345" i);
user.setBirthday(LocalDate.now());
users.add(user);
}
return users;
}
}
然后我们需要一个Servlet,来计算总页数等这些分页相关的变量,然后将分页信息传递给JSP。这个分页非常简单,实际上是利用了List接口的subList方法来切分数据,而这个方法需要接受子列的起始索引和结束索引组成的闭开区间,所以我们需要计算本页起始用户序号和本页末尾用户序号的下一个。如果数据有零头,不够一整页,那么我们就需要判断一下末尾序号是否超过了列表的大小。
代码语言:javascript复制@WebServlet(name = "ListAllServlet", urlPatterns = {"/list"})
public class ListAllServlet extends HttpServlet {
private List<User> users;
private UserRepository repository;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String p = req.getParameter("page");
int page;
try {
//当前页数
page = Integer.valueOf(p);
} catch (NumberFormatException e) {
page = 1;
}
//用户总数
int totalUsers = users.size();
//每页用户数
int usersPerPage = 20;
//总页数
int totalPages = totalUsers % usersPerPage == 0 ? totalUsers / usersPerPage : totalUsers / usersPerPage 1;
//本页起始用户序号
int beginIndex = (page - 1) * usersPerPage;
//本页末尾用户序号的下一个
int endIndex = beginIndex usersPerPage;
if (endIndex > totalUsers)
endIndex = totalUsers;
req.setAttribute("totalUsers", totalUsers);
req.setAttribute("usersPerPage", usersPerPage);
req.setAttribute("totalPages", totalPages);
req.setAttribute("beginIndex", beginIndex);
req.setAttribute("endIndex", endIndex);
req.setAttribute("page", page);
req.setAttribute("users", users);
req.getRequestDispatcher("list.jsp").forward(req, resp);
}
@Override
public void init() throws ServletException {
repository = new MemoryUserRepository();
users = repository.listAll();
}
}
上面这个Servlet中的list.jsp
就是我们具体显示的页面了。下面我们要做的就是处理前端了。
前端代码
分页组件
首先来看看前端如何分页。我在这里用的前端框架是Bootstrap,它也提供了一个分页组件pagination
,只需要在页面中添加如下一段代码。
<nav>
<ul class="pagination">
<li><a href="#">«</a></li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li><a href="#">»</a></li>
</ul>
</nav>
当然,这段代码是静态的,我们要让它产生动态的行为,就需要放到JSP中进行处理。
JSP代码
下面是我的JSP代码。我用了JSTL来做JSP的扩展,因此在项目中还需要添加JSTL的包。为了简洁,我将一些不相关的代码写在了其它JSP中,然后包含进来。_header.jsp
是引入Bootstrap的一些代码。_navbar.jsp
和_footer.jsp
则是可选的导航条和页脚,没有也罢。
然后是一堆<c:set>
,设置了我们分页要使用的一些变量。currentPageUsers
这个变量做了实际的分页工作。
然后,我用了一个表格来显示当前页的数据。用到了JSTL的<c:forEach>
标签。
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>简单分页</title>
<%@include file="_header.jsp" %>
</head>
<body>
<%@include file="_navbar.jsp" %>
<div class="container">
<c:set var="totalUsers" value="${requestScope.totalUsers}"/>
<c:set var="usersPerPage" value="${requestScope.usersPerPage}"/>
<c:set var="totalPages" value="${requestScope.totalPages}"/>
<c:set var="beginIndex" value="${requestScope.beginIndex}"/>
<c:set var="endIndex" value="${requestScope.endIndex}"/>
<c:set var="page" value="${requestScope.page}"/>
<c:set var="currentPageUsers" value="${requestScope.users.subList(beginIndex,endIndex)}"/>
<p>用户总数:${totalUsers}</p>
<p>每页用户数:${usersPerPage}</p>
<p>总页数:${totalPages}</p>
<p>当前页:${page}</p>
<table class="table table-hover table-responsive table-striped table-bordered">
<thead>
<tr>
<td>用户编号</td>
<td>姓名</td>
<td>密码</td>
<td>生日</td>
</tr>
</thead>
<tbody>
<c:forEach var="user" items="${currentPageUsers}">
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.password}</td>
<td>${user.birthday}</td>
</tr>
</c:forEach>
</tbody>
</table>
<hr>
<div class="text-center">
<nav>
<ul class="pagination">
<li><a href="<c:url value="/list?page=1"/>">首页</a></li>
<li><a href="<c:url value="/list?page=${page-1>1?page-1:1}"/>">«</a></li>
<c:forEach begin="1" end="${totalPages}" varStatus="loop">
<c:set var="active" value="${loop.index==page?'active':''}"/>
<li class="${active}"><a
href="<c:url value="/list?page=${loop.index}"/>">${loop.index}</a>
</li>
</c:forEach>
<li>
<a href="<c:url value="/list?page=${page 1<totalPages?page 1:totalPages}"/>">»</a>
</li>
<li><a href="<c:url value="/list?page=${totalPages}"/>">尾页</a></li>
</ul>
</nav>
</div>
</div>
<%@include file="_footer.jsp" %>
</body>
</html>
表格最后就是我们动态化的Bootstrap分页。首页、尾页、上一页、下一页都是固定的,不管有多少页都必须显示的。然后又用了一个<c:forEach>
标签循环列出所有页。如果某页和当前页页码相同,还为这页添加了active
类,让其高亮显示。这些分页链接最后需要跟一个page参数,表明要查看的是哪一页。
最后的显示效果如下:
数据库分页
上面仅仅使用一个列表简单演示了最基本的分页。下面来看看数据库分页。大部分数据库都支持结果的分页。这里我用MySQL数据库,它支持如下的分页语句:SELECT * FROM 表名 LIMIT m, n
,m是起始数据,n是偏移量。假如我们要前20条数据,就需要SELECT * FROM 表名 LIMIT 0, 20
,如果我们需要第二页(21-40条),就需要SELECT * FROM 表名 LIMIT 20, 20
。
建立数据库
确定数据库分页方式之后,我们就可以实现数据库分页了。首先需要一个数据库表。我还定义了两个存储过程,一个存储过程用于添加初始数据,另一个存储过程用于获取用户总数。
代码语言:javascript复制DROP DATABASE IF EXISTS page;
CREATE DATABASE page;
USE page;
CREATE TABLE user (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
birthday DATETIME
);
DELIMITER //
CREATE PROCEDURE init_user(IN count INT)
BEGIN
DECLARE i INT;
SET i = 1;
WHILE i <= count DO
INSERT INTO user (name, password, birthday) VALUES (concat('用户', i), concat('123456', i), now());
SET i = i 1;
END WHILE;
END//
DELIMITER ;
CALL init_user(202);
DELIMITER //
CREATE PROCEDURE user_counts(OUT count INT)
BEGIN
SELECT count(id)
INTO count
FROM user;
END//
DELIMITER ;
后端代码
我们需要一个支持分页的接口:
代码语言:javascript复制public interface PageableUserRepository extends UserRepository {
List<User> listAllOf(int startIndex, int offset);
int counts();
}
相应的需要一个数据访问层的实现。由于用到了存储过程,这里还使用了JDBC的CallableStatement来调用存储过程。
代码语言:javascript复制public class DataBaseUserRepository implements PageableUserRepository {
private Connection connection;
private static final String url = "jdbc:mysql://localhost:3306/page";
private static final String username = "root";
private static final String password = "12345678";
public DataBaseUserRepository() {
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(url, username, password);
} catch (SQLException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public List<User> listAll() {
List<User> users = new ArrayList<>();
try (PreparedStatement statement = connection.prepareStatement("SELECT id,name,password,birthday FROM user ")) {
ResultSet rs = statement.executeQuery();
while (rs.next()) {
User user = new User();
user.setId(rs.getInt(1));
user.setName(rs.getString(2));
user.setPassword(rs.getString(3));
user.setBirthday(rs.getObject(4, LocalDate.class));
users.add(user);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return users;
}
@Override
public List<User> listAllOf(int startIndex, int offset) {
List<User> users = new ArrayList<>();
try (PreparedStatement statement = connection.prepareStatement("SELECT id,name,password,birthday FROM user LIMIT ?,?")) {
statement.setInt(1, startIndex);
statement.setInt(2, offset);
ResultSet rs = statement.executeQuery();
while (rs.next()) {
User user = new User();
user.setId(rs.getInt(1));
user.setName(rs.getString(2));
user.setPassword(rs.getString(3));
user.setBirthday(rs.getObject(4, LocalDate.class));
users.add(user);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return users;
}
@Override
public int counts() {
try (CallableStatement statement = connection.prepareCall("CALL user_counts(?)")) {
statement.registerOutParameter(1, Types.INTEGER);
statement.execute();
return statement.getInt(1);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
最后我们需要一个Servlet,计算相应的变量并执行分页。和前面的例子相比,这里计算出分页范围之后,直接调用List<User> users = repository.listAllOf(beginIndex, usersPerPage)
,在取出数据的时候就进行了分页。所以效率等各方面都优于前面的一次性获取所有数据,然后在前端执行分页的方式。
@WebServlet(name = "PageableListServlet", urlPatterns = {"/pageableList"})
public class PageableListServlet extends HttpServlet {
private PageableUserRepository repository;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String p = req.getParameter("page");
int page;
try {
//当前页数
page = Integer.valueOf(p);
} catch (NumberFormatException e) {
page = 1;
}
//用户总数
int totalUsers = repository.counts();
//每页用户数
int usersPerPage = 20;
//总页数
int totalPages = totalUsers % usersPerPage == 0 ? totalUsers / usersPerPage : totalUsers / usersPerPage 1;
//本页起始用户序号
int beginIndex = (page - 1) * usersPerPage;
List<User> users = repository.listAllOf(beginIndex, usersPerPage);
req.setAttribute("totalPages", totalPages);
req.setAttribute("page", page);
req.setAttribute("users", users);
req.getRequestDispatcher("pageableList.jsp").forward(req, resp);
}
@Override
public void init() throws ServletException {
repository = new DataBaseUserRepository();
}
}
分页组件
本来这篇文章老早以前就写完不管了,没想到有同学还关注了这个问题,并在评论区提问能不能封装一下。所以我们来分析一下分页组件应该是什么样的,首先总页数和每页有多少数据应该是预先提供的,然后就可以计算出有多少页,在给出一个当前页码,就可以得出当前页应该显示的数据了。
辅助类
把上面的逻辑综合一下,在稍加整理,就有了下面的简单的分页辅助类。这个类用到了lombok帮忙自动生成Getter和Setter,没用过的同学可以研究一下。这个辅助类的用法很简单,首先实例化一个对象,然后用setter设置总页数和每页用户数,然后剩余信息都可以通过Getter获得。
代码语言:javascript复制@Data
@NoArgsConstructor
public class PaginationHelper {
// 数据总数
private int totalCount;
// 每页数据数
private int countPerPage;
public int getPageCount() {
return totalCount % countPerPage == 0 ? totalCount / countPerPage : totalCount / countPerPage 1;
}
/**
* 页数以1开始
*
* @param currentPage
* @return
*/
public int getCurrentPageStart(int currentPage) {
if (currentPage < 1 || currentPage > getTotalCount()) {
throw new RuntimeException("页数错误");
}
return (currentPage - 1) * countPerPage;
}
public int getCurrentPageEnd(int currentPage) {
if (currentPage < 1 || currentPage > getTotalCount()) {
throw new RuntimeException("页数错误");
}
// 三元运算符
return getCurrentPageStart(currentPage) countPerPage > totalCount ?
totalCount : getCurrentPageStart(currentPage) countPerPage;
}
}
有了辅助类,Servlet代码就简单多了。这个Servlet功能和前面的差不多,只不过增加了一个修改每页用户数的功能。
代码语言:javascript复制@WebServlet(name = "pageServlet", urlPatterns = {"/page"})
public class PageServlet extends HttpServlet {
private List<User> users;
private UserRepository repository;
private PaginationHelper pagination;
private int countPerPage;
@Override
public void init() throws ServletException {
repository = new MemoryUserRepository();
users = repository.listAll();
pagination = new PaginationHelper();
countPerPage = 20;
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 当前页数
int page;
try {
page = Integer.valueOf(req.getParameter("page"));
} catch (NumberFormatException e) {
page = 1;
}
try {
if (req.getParameter("countPerPage") != null)
countPerPage = Integer.valueOf(req.getParameter("countPerPage"));
} catch (NumberFormatException e) {
countPerPage = 20;
}
// 设置用户总数
pagination.setTotalCount(users.size());
// 设置每页用户数
pagination.setCountPerPage(countPerPage);
req.setAttribute("pagination", pagination);
req.setAttribute("users", users.subList(pagination.getCurrentPageStart(page), pagination.getCurrentPageEnd(page)));
req.setAttribute("page", page);
req.getRequestDispatcher("page.jsp").forward(req, resp);
}
}
前端代码
两年以后,Bootstrap 4也出来了,另外一些类库也更新了,因此这里我也一并做了更新,前面的文章内容没啥变动, 但是代码已经改成Bootstrap 4的风格了,主要看代码就好。
代码语言:javascript复制<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>分页</title>
<%@include file="_header.jsp" %>
</head>
<body>
<%@include file="_navbar.jsp" %>
<div class="container" role="main">
<c:set var="users" value="${requestScope.users}"/>
<c:set var="pagination" value="${requestScope.pagination}"/>
<c:set var="page" value="${requestScope.page}"/>
<c:set var="totalPages" value="${pagination.getPageCount()}"/>
<div class="row justify-content-center">
<div class="col-md-8">
<form action="<c:url value="/page"/>" method="get">
<p>用户总数:${pagination.getTotalCount()},每页用户数:<input name="countPerPage" type="text"
value="${pagination.getCountPerPage()}"/>(<input
type="submit" value="修改"/>),总页数:${pagination.getPageCount()},当前页:${page}</p>
</form>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-8 table-responsive">
<table class="table table-hover table-striped table-bordered table-sm">
<thead>
<tr>
<td>用户编号</td>
<td>姓名</td>
<td>密码</td>
<td>生日</td>
</tr>
</thead>
<tbody>
<c:forEach var="user" items="${users}">
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.password}</td>
<td>${user.birthday}</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
<div class="row justify-content-center">
<div>
<nav>
<ul class="pagination">
<li class="page-item"><a class="page-link" href="<c:url value="/page?page=1"/>">首页</a></li>
<li class="page-item"><a class="page-link" href="<c:url value="/page?page=${page-1>1?page-1:1}"/>">«</a>
</li>
<c:forEach begin="1" end="${totalPages}" varStatus="loop">
<c:set var="active" value="${loop.index==page?'active':''}"/>
<li class="page-item ${active}">
<a class="page-link" href="<c:url value="/page?page=${loop.index}"/>">${loop.index}</a>
</li>
</c:forEach>
<li class="page-item">
<a class="page-link" href="<c:url value="/page?page=${page 1<totalPages?page 1:totalPages}"/>">»</a>
</li>
<li class="page-item">
<a class="page-link" href="<c:url value="/page?page=${totalPages}"/>">尾页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<%@include file="_footer.jsp" %>
</body>
</html>
网页截图如下。输入不同的每页用户数,就可以看到不同数量的分页效果了。
隐藏多余页数
最后一个问题就是隐藏多余的页数了,数据量太多的话,底下几百页的页码没法看。当然这个问题没有什么具体的解决方案,因为仔细观察的话,你会发现基本上很多网站的分页都还不一样。而且这也是一个前端的问题,在JSP里面就能处理。
我这里使用的方案是:显示首页和尾页,显示上一页和下一页,显示当前页,其余页使用省略号代替。具体逻辑如下:
- 判断当前页和首页的距离,如果是0,则不显示首页;
- 判断当前页和首页的距离,如果是1,则显示首页,不显示前一个省略号;
- 判断当前页和首页的距离,如果大于2,则显示首页和前一个省略号;
- 显示当前页;
- 逻辑类似前一段,判断当前页和末页的距离,决定是否显示末页和后一个省略号。
JSP代码如下,需要对上面5种情况都判断一下:
代码语言:javascript复制 <div class="row justify-content-center">
<div>
<nav>
<ul class="pagination">
<li class="page-item ${page==1?'disabled':''}"><a class="page-link"
href="<c:url value="/page?page=${page-1>1?page-1:1}"/>">上一页</a>
</li>
<c:if test="${page!=1}">
<li class="page-item">
<a class="page-link" href="<c:url value="/page?page=1"/>">1</a>
</li>
</c:if>
<c:if test="${page>2}">
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
</c:if>
<li class="page-item active">
<a class="page-link" href="<c:url value="/page?page=${page}"/>">${page}</a>
</li>
<c:if test="${totalPages-page>1}">
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
</c:if>
<c:if test="${page!=totalPages}">
<li class="page-item">
<a class="page-link" href="<c:url value="/page?page=${totalPages}"/>">${totalPages}</a>
</li>
</c:if>
<li class="page-item ${page==totalPages?'disabled':''}">
<a class="page-link" href="<c:url value="/page?page=${page 1<totalPages?page 1:totalPages}"/>">下一页</a>
</li>
</ul>
</nav>
</div>
</div>
下面是实际页面效果,大家可以自行调整每页显示数来查看分页效果,当然如果有需要的话也可以自己额外处理。
以上就是JSP分页的简单例子。第一个例子显示了最基本的分页。第二个例子利用了数据库的分页功能,在取出数据的时候就对数据进行分页。第三个例子增加了每页显示数和隐藏多余分页的代码。前端框架用的是Bootstrap 4。
代码我上传到了码云,有兴趣的同学们可以看看。项目有两个分支,主分支是用gradle整理的代码,推荐会用gradle的同学;webapp分支是普通的Java Web项目格式,可用Intellij IDEA打开,如果要用Eclipse的话可能需要在Eclipse中添加一个项目然后将几个部分代码复制进去。代码中由于使用了Lombok,所以还需要额外安装IDE插件,并修改设置支持Lombok。
另外原来User类用的是java.util.Date
,现在改为Java 8的java.time.LocalDate
,如果没有Java 8的话需要修改成原来的样子,可以查看最前几次提交来修改。总的来说,还是Gradle通用性更好一点,推荐大家学习一下。