Java 分页实战:从原理到代码,3 步实现高效数据分页

在 JavaWeb 开发中,当需要展示大量数据(如 10 万条用户记录、百万级订单列表)时,“一次性加载所有数据” 会导致两个严重问题:服务器查询耗时过长(数据库压力大)、浏览器渲染卡顿(前端加载缓慢)。而分页技术正是解决这个问题的 “标准方案”——将数据分成多页,用户每次只加载当前页数据,平衡性能与体验。

本文专为 JavaWeb 开发者设计,从底层原理到完整代码实现,手把手教你用 3 步实现分页功能,涵盖 MySQL 分页查询、后端参数计算、前端页码渲染全流程。

一、先搞懂:分页的核心原理是什么?

分页本质是 “分批次查询 + 按需展示”,核心由 3 部分组成:前端参数传递、后端逻辑处理、数据库分页查询,三者协作流程如下:

  1. 前端传递参数:用户点击 “第 2 页” 时,前端向服务器传递当前页码(pageNum)每页条数(pageSize)
  2. 后端计算关键参数:服务器根据这两个参数,计算跳过的条数(start)总页数(totalPage)
  3. 数据库分页查询:用startpageSize查询当前页数据,同时查询总条数(totalCount)
  4. 返回结果渲染:后端将 “当前页数据 + 分页参数” 返回给前端,前端渲染数据和页码控件。

核心参数公式(新手必记)

参数名称含义计算公式 / 说明
pageNum当前页码前端传递,默认值为 1(避免空指针)
pageSize每页显示条数前端传递或后端固定,常见值:10、20、50
start跳过的条数(分页起点)(pageNum - 1) * pageSize
totalCount总数据条数数据库查询(SELECT COUNT(*) FROM 表
totalPage总页数totalCount % pageSize == 0 ? totalCount/pageSize : totalCount/pageSize + 1

例如:总条数 123,每页 10 条 → 总页数 13(123=12*10+3);当前页 2 → 跳过 10 条(从第 11 条开始查)。

二、实战:3 步实现分页功能(Servlet+JSP+MySQL)

以 “用户列表分页” 为例,完整实现从后端查询到前端展示的全流程,新手可直接复用代码。

步骤 1:准备工作(数据库表 + JavaBean)

(1)创建用户表(MySQL)
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `age` int(3) DEFAULT NULL,
  `address` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入测试数据(100条,模拟大量数据)
INSERT INTO `user` (username, age, address) VALUES
('张三', 22, '北京'), ('李四', 25, '上海'),
-- ... 省略其他98条数据 ...
(2)创建 User 实体类(JavaBean)
public class User {
    private Integer id;
    private String username;
    private Integer age;
    private String address;

    // 构造器、getter、setter(必须有,否则JSP无法获取属性)
    public User() {}
    public User(Integer id, String username, Integer age, String address) {
        this.id = id;
        this.username = username;
        this.age = age;
        this.address = address;
    }
    // getter和setter省略(IDEA可自动生成)
}
(3)创建分页工具类(封装分页参数)

为避免参数零散,用一个类封装所有分页相关数据:

public class PageBean<T> {
    private int pageNum;      // 当前页码
    private int pageSize;     // 每页条数
    private int totalCount;   // 总条数
    private int totalPage;    // 总页数
    private List<T> dataList; // 当前页数据

    // 计算总页数(setTotalCount时自动计算)
    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
        this.totalPage = (totalCount % pageSize == 0) ? 
                        totalCount / pageSize : totalCount / pageSize + 1;
    }

    // getter和setter省略
}

步骤 2:后端处理(Servlet + 数据库查询)

(1)编写 DAO 层(数据库操作)

用 JDBC 实现分页查询和总条数查询(实际项目可用 MyBatis 简化):

public class UserDao {
    // 分页查询当前页数据
    public List<User> findByPage(int start, int pageSize) {
        List<User> userList = new ArrayList<>();
        String sql = "SELECT id, username, age, address FROM user LIMIT ?, ?";
        try (Connection conn = JDBCUtils.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, start);
            pstmt.setInt(2, pageSize);
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                User user = new User(
                    rs.getInt("id"),
                    rs.getString("username"),
                    rs.getInt("age"),
                    rs.getString("address")
                );
                userList.add(user);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return userList;
    }

    // 查询总条数
    public int findTotalCount() {
        String sql = "SELECT COUNT(*) FROM user";
        try (Connection conn = JDBCUtils.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql);
             ResultSet rs = pstmt.executeQuery()) {
            if (rs.next()) {
                return rs.getInt(1); // 返回总条数
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return 0;
    }
}
(2)编写 Servlet(处理请求,计算参数)
@WebServlet("/user/pageList")
public class UserPageServlet extends HttpServlet {
    private UserDao userDao = new UserDao();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 获取前端参数(pageNum和pageSize),设置默认值
        String pageNumStr = request.getParameter("pageNum");
        String pageSizeStr = request.getParameter("pageSize");
        int pageNum = (pageNumStr == null || "".equals(pageNumStr)) ? 1 : Integer.parseInt(pageNumStr);
        int pageSize = (pageSizeStr == null || "".equals(pageSizeStr)) ? 10 : Integer.parseInt(pageSizeStr);

        // 2. 校验参数(防止pageNum <=0 或过大)
        if (pageNum < 1) pageNum = 1;

        // 3. 计算分页参数
        int start = (pageNum - 1) * pageSize;
        int totalCount = userDao.findTotalCount(); // 查总条数
        int totalPage = (totalCount % pageSize == 0) ? totalCount/pageSize : totalCount/pageSize + 1;
        // 再次校验pageNum(如果超过总页数,设为最后一页)
        if (pageNum > totalPage && totalPage > 0) pageNum = totalPage;

        // 4. 查询当前页数据
        List<User> userList = userDao.findByPage(start, pageSize);

        // 5. 封装到PageBean
        PageBean<User> pageBean = new PageBean<>();
        pageBean.setPageNum(pageNum);
        pageBean.setPageSize(pageSize);
        pageBean.setTotalCount(totalCount);
        pageBean.setTotalPage(totalPage);
        pageBean.setDataList(userList);

        // 6. 传递到JSP
        request.setAttribute("pageBean", pageBean);
        request.getRequestDispatcher("/user/pageList.jsp").forward(request, response);
    }
}

步骤 3:前端渲染(JSP 展示数据和页码)

web/user目录下创建pageList.jsp,实现数据表格和分页控件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.example.pojo.PageBean,com.example.pojo.User,java.util.List" %>
<html>
<head>
    <title>用户列表分页</title>
    <style>
        table { border-collapse: collapse; width: 80%; margin: 20px auto; }
        th, td { border: 1px solid #333; padding: 12px; text-align: center; }
        .pagination { text-align: center; margin: 20px; }
        .pagination a { 
            display: inline-block; 
            padding: 6px 12px; 
            margin: 0 5px; 
            border: 1px solid #2f54eb; 
            color: #2f54eb; 
            text-decoration: none; 
        }
        .pagination a.active { 
            background: #2f54eb; 
            color: white; 
            border-color: #2f54eb; 
        }
        .pagination a:hover { border-color: #1d39c4; color: #1d39c4; }
    </style>
</head>
<body>
    <h1 style="text-align: center;">用户列表(分页展示)</h1>
    <table>
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>年龄</th>
            <th>地址</th>
        </tr>

        <% 
            PageBean<User> pageBean = (PageBean<User>) request.getAttribute("pageBean");
            List<User> userList = pageBean.getDataList();
            if (userList != null && !userList.isEmpty()) {
                for (User user : userList) {
        %>
        <tr>
            <td><%= user.getId() %></td>
            <td><%= user.getUsername() %></td>
            <td><%= user.getAge() %></td>
            <td><%= user.getAddress() %></td>
        </tr>
        <% 
                }
            } else { 
        %>
        <tr>
            <td colspan="4">暂无数据</td>
        </tr>
        <% } %>
    </table>

    <%-- 分页控件 --%>
    <div class="pagination">
        <% 
            int pageNum = pageBean.getPageNum();
            int totalPage = pageBean.getTotalPage();
            int pageSize = pageBean.getPageSize();
            int totalCount = pageBean.getTotalCount();
        %>
        <p>共<%= totalCount %>条数据,每页<%= pageSize %>条,共<%= totalPage %>页</p>
        
        <%-- 首页和上一页(如果是第1页,不显示) --%>
        <% if (pageNum > 1) { %>
            <a href="/JavaWebDemo/user/pageList?pageNum=1&pageSize=<%= pageSize %>">首页</a>
            <a href="/JavaWebDemo/user/pageList?pageNum=<%= pageNum-1 %>&pageSize=<%= pageSize %>">上一页</a>
        <% } %>

        <%-- 页码列表(显示当前页前后2页,避免页码过多) --%>
        <% 
            // 计算起始页码(避免小于1)
            int startPage = Math.max(1, pageNum - 2);
            // 计算结束页码(避免大于总页数)
            int endPage = Math.min(totalPage, pageNum + 2);
            for (int i = startPage; i <= endPage; i++) {
        %>
            <a href="/JavaWebDemo/user/pageList?pageNum=<%= i %>&pageSize=<%= pageSize %>" 
               class="<%= i == pageNum ? "active" : "" %>">
                <%= i %>
            </a>
        <% } %>

        <%-- 下一页和末页(如果是最后一页,不显示) --%>
        <% if (pageNum < totalPage) { %>
            <a href="/JavaWebDemo/user/pageList?pageNum=<%= pageNum+1 %>&pageSize=<%= pageSize %>">下一页</a>
            <a href="/JavaWebDemo/user/pageList?pageNum=<%= totalPage %>&pageSize=<%= pageSize %>">末页</a>
        <% } %>
    </div>
</body>
</html>

三、关键优化:让分页更高效、体验更好

基础分页实现后,还需注意 3 个优化点,避免性能问题和用户体验缺陷:

1. 性能优化:减少不必要的总条数查询

总条数查询(SELECT COUNT(*))在数据量大时可能耗时,可做以下优化:

  • 缓存总条数:非实时性场景(如商品列表),将总条数缓存到 Redis,定时更新;
  • 延迟查询:首次加载时只查当前页数据,用户点击 “下一页” 再查总条数(适合数据量极大的场景);
  • 优化 COUNT 语句:MySQL 中COUNT(*)COUNT(1)COUNT(id)更高效(MySQL 对COUNT(*)有优化)。

2. 体验优化:页码控件动态适配

  • 限制页码显示数量:当总页数超过 10 页时,只显示当前页前后 2-3 页(如示例中startPageendPage的计算),避免页码过多导致控件过长;
  • 支持自定义每页条数:前端增加下拉框(10 条 / 页、20 条 / 页、50 条 / 页),让用户按需选择;
  • 添加加载状态:用户点击页码时,显示 “加载中” 提示,避免重复点击。

3. 安全优化:防止参数篡改和 SQL 注入

  • 参数校验:后端必须校验pageNumpageSize(如pageNum不能小于 1,pageSize不能超过 1000,防止恶意请求大量数据);
  • 用预编译语句:数据库查询时必须用PreparedStatement(如示例中LIMIT ?, ?),避免直接拼接 SQL 字符串导致注入攻击。

四、新手避坑:5 个高频问题及解决方案

  1. 坑 1:分页参数为 null 导致空指针异常
    原因:前端未传递pageNumpageSize,后端直接Integer.parseInt()
    ✅ 解决方案:设置默认值,如int pageNum = (pageNumStr == null) ? 1 : Integer.parseInt(pageNumStr)

  2. 坑 2:页码超出总页数导致无数据
    原因:用户手动输入pageNum=1000(超过总页数),查询结果为空。
    ✅ 解决方案:后端校验,若pageNum > totalPage,强制设为totalPage(如示例中代码)。

  3. 坑 3:LIMIT 参数错误导致查询失败
    原因:start计算错误(如pageNum=1start=1*pageSize,导致跳过第 1 页数据)。
    ✅ 解决方案:牢记start = (pageNum - 1) * pageSizepageNum=1start=0,从第 1 条开始查)。

  4. 坑 4:总页数计算错误(少一页或多一页)
    原因:用totalCount / pageSize直接计算,未处理余数。
    ✅ 解决方案:用三目运算符:totalPage = totalCount % pageSize == 0 ? totalCount/pageSize : totalCount/pageSize + 1

  5. 坑 5:前端页码点击后样式不变
    原因:未给当前页添加高亮样式,用户分不清当前在第几页。
    ✅ 解决方案:在 JSP 中判断i == pageNum时,添加active类(如示例中class="<%= i == pageNum ? "active" : "" %>")。

总结

分页的核心是 “分批次查询 + 参数计算”,掌握(pageNum-1)*pageSize的分页起点计算、LIMIT语句的使用、前端页码控件的渲染,就能实现基础分页功能。在此基础上,通过缓存总条数、优化页码显示、校验参数等方式,可进一步提升性能和体验。

如果这篇文章帮你解决了分页开发的困惑,欢迎点赞、收藏,评论区可以分享你的实现技巧或遇到的问题~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Pretend________

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值