Servlet 分页实战指南

分页是指一系列相互连接且包含相似内容的页面顺序。这是一种将包含大量记录的列表拆分为多个子列表的技术。例如,当你在 Google 上使用关键词搜索时,会收到数以万计的结果。值得注意的是,即使页面某部分的内容被分割到了不同的页面上,我们仍然将其定义为分页。

Java 中的分页实现

为了将大量记录分成多个部分,我们可以使用分页技术。这允许用户仅显示一部分记录。如果在单个页面上加载所有记录,可能会耗费较长时间,因此始终建议创建分页功能。在 Java 中,我们可以轻松开发分页示例。在本示例中,我们将使用 MySQL 数据库来获取记录。但在深入代码之前,让我们先理解为什么这在 2026 年依然重要——尽管我们现在拥有了 React、Vue 和 Serverless,但数据分页的核心逻辑依然是后端工程的基础。

实战示例:构建核心逻辑

第 1 步:在 MySQL 中创建一张表

create table employee(empid int(11),empname varchar(20),empsalary int(11),empdept varchar(20));

第 2 步:创建一个 JavaBean 类,用于向数据库设置值以及从数据库获取值。

在现代开发中,我们通常称这种简单的 POJO 为“实体”或“DTO”。虽然 Lombok 等工具可以自动生成样板代码,但理解底层的 Getter 和 Setter 对于掌握 Java 内存模型至关重要。

public class Employee {
    int employeeId;
    String employeeName;
    int salary;
    String deptName;

    // 标准的 Getter 方法
    // 我们在这里封装数据,防止外部直接访问内部状态
    public String getDeptName() { return deptName; }

    public void setDeptName(String deptName) {
        this.deptName = deptName;
    }

    public int getEmployeeId() { return employeeId; }

    public void setEmployeeId(int employeeId) {
        this.employeeId = employeeId;
    }

    public String getEmployeeName() { return employeeName; }

    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }

    public int getSalary() { return salary; }

    public void setSalary(int salary) {
        this.salary = salary;
    }
}

第 3 步:创建一个工厂类,用于从数据库获取连接。
注意:在下面的代码中,我们使用了单例模式。在 2026 年,我们通常依赖连接池(如 HikariCP)和 Spring Boot 的自动配置来管理这些。但作为一个“硬核”的 Java 工程师,理解如何手动建立 JDBC 连接是你调试底层连接泄漏问题的关键。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionFactory {

    // 指向自身的静态引用
    // 这是一个经典的“饿汉式”单例实现,线程安全但缺乏懒加载
    private static ConnectionFactory instance = new ConnectionFactory();

    // 在生产环境中,这些配置应当外部化到配置中心(如 Nacos 或 Consul)
    String url = "jdbc:mysql://localhost:3306/ashok";
    String user = "root";
    String password = "";
    // 注意:com.mysql.jdbc.Driver 在 MySQL 8.0+ 中已被废弃,建议使用 com.mysql.cj.jdbc.Driver
    String driverClass = "com.mysql.cj.jdbc.Driver";

    // 私有构造函数防止外部 new 对象
    private ConnectionFactory() {
        try {
            Class.forName(driverClass);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            // 在生产环境中,这里应该记录到日志系统(如 SLF4J)而不是仅打印堆栈
        }
    }

    public static ConnectionFactory getInstance() {
        return instance;
    }

    // 获取连接
    public Connection getConnection() throws SQLException, ClassNotFoundException {
        Connection connection = DriverManager.getConnection(url, user, password);
        return connection;
    }
}

第 4 步:创建一个 Dao 类,用于创建工厂类对象并在方法中调用。同时创建查询语句,将其结果设置到 JavaBean 对象中,并添加到 ArrayList 对象里。

这里我们引入了 SQL_CALC_FOUND_ROWS。这是一个很有用的技巧,它允许我们在一次数据库交互中获取数据和总数。然而,在现代高并发场景下,我们可能会思考:这真的是最高效的方式吗?让我们先看看代码,稍后我会进行深度剖析。

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class EmployeeDAO {

    Connection connection;
    Statement stmt;
  
    // 用于保存总记录数,以便前端计算页码
    private int noOfRecords;
    
    public EmployeeDAO() {}

    private static Connection getConnection() throws SQLException, ClassNotFoundException {
        Connection con = ConnectionFactory.getInstance().getConnection();
        return con;
    }

    // 核心分页逻辑
    // offset: 起始位置 (例如第2页,每页5条,offset就是5)
    // noOfRecords: 每页显示的记录数
    public List viewAllEmployees(int offset, int noOfRecords) {
        // 使用 SQL_CALC_FOUND_ROWS 是一种优化,避免发送两次 Count 查询
        // 但在某些 MySQL 版本中,这可能会引发性能锁竞争
        String query = "select SQL_CALC_FOUND_ROWS * from employee limit " + offset + ", " + noOfRecords;
        List list = new ArrayList();
        Employee employee = null;
        try {
            connection = getConnection();
            stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            
            // 遍历结果集,将数据库行映射为 Java 对象 (ORM 的原始形态)
            while (rs.next()) {
                employee = new Employee();
                employee.setEmployeeId(rs.getInt(1));
                employee.setEmployeeName(rs.getString(2));
                employee.setSalary(rs.getInt(3));
                employee.setDeptName(rs.getString(4));
                list.add(employee);
            }

            rs.close();
            // 这是一个 MySQL 特有的函数,用于获取上一条查询不考虑 Limit 时的总行数
            rs = stmt.executeQuery("SELECT FOUND_ROWS()");

            if (rs.next())
               this.noOfRecords = rs.getInt(1);
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 资源释放是防止内存泄漏的关键
            try {
                if (stmt != null) stmt.close();
                if (connection != null) connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return list;
    }
    
    // 提供给 Servlet 获取总记录数的方法
    public int getNoOfRecords() { return noOfRecords; }
}

深入剖析:2026 年视角的分页技术重构

作为一名经历过多个技术周期迭代的开发者,我们深知上述代码虽然能够工作,但在 2026 年的云原生与 AI 时代,我们需要用更先进的工程思维来审视它。在我们的最近的一个企业级项目中,我们将这种传统的 Servlet 分页逻辑重构为了更符合现代微服务架构的模式。让我们探讨几个关键点。

#### 1. 从 Statement 到 PreparedStatement:不仅是语法糖,更是安全防线

在之前的示例中,为了代码简洁,我们使用了 INLINECODEa4c1c3e8 并拼接 SQL 字符串。这在 2026 年是绝对不被允许的,也是我们作为工程师必须第一时间识别出的安全漏洞。SQL 注入至今仍是 OWASP Top 10 的常客。如果你现在使用 Cursor 或 GitHub Copilot 等辅助编程工具,它们可能会建议你直接使用 INLINECODE758ae527,这正是 AI 辅助我们在基础安全层面不犯错的一个例子。

改进后的 DAO 片段 (使用 PreparedStatement):

public List viewAllEmployeesSafe(int offset, int limit) {
    String query = "SELECT * FROM employee LIMIT ?, ?";
    List list = new ArrayList();
    
    try (Connection con = ConnectionFactory.getInstance().getConnection();
         PreparedStatement pstmt = con.prepareStatement(query)) {
        
        // 参数化查询,数据库驱动会自动处理转义,彻底杜绝 SQL 注入
        pstmt.setInt(1, offset);
        pstmt.setInt(2, limit);
        
        try (ResultSet rs = pstmt.executeQuery()) {
            while (rs.next()) {
                Employee emp = new Employee();
                // ... 映射逻辑 ...
                list.add(emp);
            }
        }
        // 注意:这里我们不再使用 SQL_CALC_FOUND_ROWS,因为某些现代云数据库(如 Aurora)
        // 推荐使用 COUNT(*) OVER() 或单独的 Count 查询来优化资源利用。
    } catch (SQLException | ClassNotFoundException e) {
        throw new RuntimeException("分页查询失败", e); // 异常链保留
    }
    return list;
}

#### 2. 性能与“Keyset Pagination” (游标分页)

传统的 INLINECODE8f18635a 在数据量达到百万级时,性能会断崖式下跌。为什么?因为数据库必须扫描并跳过 INLINECODE6467d677 之前的所有行。在我们的生产实践中,当数据量激增时,我们发现第 10000 页的加载时间远高于第 1 页。

为了解决这个问题,我们引入了 Keyset Pagination (也称为 Cursor-based Pagination 或 Seek Method)。这种方法不使用偏移量,而是记录上一页最后一条数据的 ID(或唯一索引)。

场景对比:

  • 传统分页SELECT * FROM user LIMIT 10000, 10; (数据库需读取 10010 行)
  • Keyset 分页SELECT * FROM user WHERE id > last_seen_id ORDER BY id LIMIT 10; (数据库直接通过索引定位,极快)

这种“无状态”的分页方式在 2026 年的移动应用和无限滚动场景中已成为标准。

#### 3. 缓存策略:Redis 与本地缓存的艺术

想象一下,如果首页被每秒数万次访问,每次都去击打 MySQL 数据库是不明智的。我们可以在 Servlet 层引入多级缓存。

  • L1 本地缓存: 存放元数据或极高热点的数据,但由于 Servlet 容器可能分布式部署,本地缓存容易不一致,需谨慎使用。
  • L2 分布式缓存: 这里的王者无疑是 Redis。

在 2026 年,我们不仅缓存数据,更缓存“计算”。例如,我们可以直接缓存 INLINECODEe7248888 的序列化 JSON 结果,甚至可以使用 Redis 的 INLINECODE976277a0 结构来存储每一页的数据,设置合理的过期时间(TTL)。

// 伪代码:带有缓存逻辑的 Service 层
public List getEmployeesByPage(int page) {
    String cacheKey = "employee_page_" + page;
    
    // 1. 尝试从 Redis 获取
    String cachedData = redis.get(cacheKey);
    if (cachedData != null) {
        return deserialize(cachedData); // 命中缓存,直接返回
    }
    
    // 2. 缓存未命中,查询数据库
    List result = employeeDAO.viewAllEmployees(...);
    
    // 3. 写入缓存,设置 5 分钟过期
    redis.setex(cacheKey, 300, serialize(result));
    
    return result;
}

现代前端与 Servlet 的交互:AJAX 还是 Server-Side Rendering?

在 GeeksforGeeks 的传统示例中,我们通常使用 JSP 进行服务端渲染。但在 2026 年,前后端分离已成主流。我们不再返回 HTML 视图,而是返回 JSON

这意味着我们的 Servlet 代码会有所变化,我们将使用 INLINECODEec2e3f9b 来输出 JSON 字符串,而不是 INLINECODE8751b71a。

JSON 响应示例:

// 在 Servlet 中
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    int page = Integer.parseInt(request.getParameter("page"));
    int limit = 10;
    
    List employees = dao.viewAllEmployees((page-1)*limit, limit);
    int totalRecords = dao.getNoOfRecords();
    
    // 构建一个简单的 Map 并转为 JSON (生产环境使用 Jackson 或 Gson)
    Map data = new HashMap();
    data.put("employees", employees);
    data.put("currentPage", page);
    data.put("totalPages", (int) Math.ceil((double) totalRecords / limit));
    
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    response.getWriter().write(new Gson().toJson(data));
}

总结:从代码编写者的视角

回顾这篇文章,我们从最基本的 JDBC 分页开始,一步步深入到安全性、性能优化以及现代化的缓存策略。我们不仅复制了代码,更重要的是,我们一起探讨了“为什么”“怎么做”。在 2026 年,虽然框架会变得越来越智能(也许 AI 可以直接帮我们写出这些 DAO),但理解底层的原理——无论是 SQL 执行计划、网络开销,还是缓存一致性——依然是区分“码农”和“架构师”的试金石。希望你在实际项目中,不仅能让代码运行起来,更能让它优雅、安全且高效地运行。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/45206.html
点赞
0.00 平均评分 (0% 分数) - 0