JSP分页显示数据

2022-05-05 19:41:21 浏览数 (1)

最近在做一个小程序,用到了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,只需要在页面中添加如下一段代码。

代码语言:javascript复制
<nav>
  <ul class="pagination">
    <li><a href="#">&laquo;</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="#">&raquo;</a></li>
  </ul>
</nav>

当然,这段代码是静态的,我们要让它产生动态的行为,就需要放到JSP中进行处理。

JSP代码

下面是我的JSP代码。我用了JSTL来做JSP的扩展,因此在项目中还需要添加JSTL的包。为了简洁,我将一些不相关的代码写在了其它JSP中,然后包含进来。_header.jsp是引入Bootstrap的一些代码。_navbar.jsp_footer.jsp则是可选的导航条和页脚,没有也罢。

然后是一堆<c:set>,设置了我们分页要使用的一些变量。currentPageUsers这个变量做了实际的分页工作。

然后,我用了一个表格来显示当前页的数据。用到了JSTL的<c:forEach>标签。

代码语言: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">

    <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}"/>">&laquo;</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}"/>">&raquo;</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),在取出数据的时候就进行了分页。所以效率等各方面都优于前面的一次性获取所有数据,然后在前端执行分页的方式。

代码语言:javascript复制
@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}"/>">&laquo;</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}"/>">&raquo;</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通用性更好一点,推荐大家学习一下。

0 人点赞