JSP入门教程:连接数据库并展示查询结果

SEO教程4小时前发布 jdkjadmin
0 00
JSP入门教程:连接数据库并展示查询结果

JSP入门教程:连接数据库并展示查询结果

JSP 可以把 Java Web 应用中的数据呈现为 HTML 页面。入门时常见的做法,是在 JSP 页面里直接写 JDBC 代码、查询数据库,再用循环拼接表格。但这种写法把数据访问、业务逻辑和页面展示混在一起,容易造成难以维护的代码,也更容易引入安全问题。

更适合实际项目的结构是:Servlet 接收请求并组织流程,DAO(数据访问对象)负责查询数据库,JSP 负责展示结果。本文用 Java 17、Tomcat 10.1、Jakarta Servlet、JSP、JSTL 和 PostgreSQL 搭建一个客户列表页面。示例包含可运行的核心代码,也会说明配置方式、适用边界和常见错误。

一、JSP 适合做什么,不适合做什么

JSP 是一种服务端页面技术。服务器收到请求后,会执行 JSP 页面中的标签和表达式,生成 HTML,再把响应发送给浏览器。它适合用于 Java Web 应用中的传统服务端渲染,例如管理后台、内部业务系统、简单的内容展示页。

JSP 本身不是数据库访问技术。它可以调用 Java 代码,但这不代表所有 Java 代码都应该写在 JSP 中。更清晰的分工通常是:

  • Servlet:接收请求、校验参数、调用业务层,并把结果放入请求属性。
  • DAO:使用 JDBC 执行 SQL,将结果转换成 Java 对象。
  • JSP:读取请求属性并生成 HTML,不直接负责数据库连接和 SQL 执行。

如果项目使用前后端分离、需要给多个客户端提供 API,或者已经采用 Spring MVC 等框架,应优先沿用项目现有的架构,而不是为了展示数据库结果单独增加 JSP。JSP 也不适合直接暴露数据库凭据、承载复杂业务逻辑或代替访问控制。

二、示例结构与运行环境

本例查询 PostgreSQL 中的客户记录,并把最多 100 条结果展示在网页上。运行环境为 Java 17、Maven 和 Tomcat 10.1。Tomcat 10.1 使用 Jakarta 命名空间,因此代码导入的是 jakarta.servlet,不能直接照搬面向旧版 Tomcat 的 javax.servlet 代码。

示例项目结构如下:

jsp-customer-demo/
├── pom.xml
└── src/
    └── main/
        ├── java/
        │   └── com/example/
        │       ├── Customer.java
        │       ├── CustomerDao.java
        │       ├── DatabaseContextListener.java
        │       └── CustomerServlet.java
        └── webapp/
            └── WEB-INF/
                └── views/
                    └── customers.jsp

示例使用 HikariCP 管理 JDBC 连接,避免每次请求都新建数据库连接。下面的配置从环境变量读取数据库连接信息;连接池在应用启动时创建,在应用停止时关闭。

三、准备数据库和 Maven 配置

先创建数据库和示例表。以下 SQL 在 PostgreSQL 中执行:

CREATE DATABASE jsp_demo;

连接到 jsp_demo 数据库后,创建表并插入测试数据:

CREATE TABLE customers (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL
);

INSERT INTO customers (name, email) VALUES
    ('林青', '[email protected]'),
    ('陈明', '[email protected]'),
    ('周宁', '[email protected]');

项目的 pom.xml 配置如下。JSTL 3 使用 Jakarta 标签库;Tomcat 提供 Servlet 和 JSP 容器,因此 Servlet API 使用 provided 作用域。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>jsp-customer-demo</artifactId>
    <version>1.0.0</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.5</version>
        </dependency>

        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>6.2.1</version>
        </dependency>

        <dependency>
            <groupId>jakarta.servlet.jsp.jstl</groupId>
            <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
            <version>3.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>jakarta.servlet.jsp.jstl</artifactId>
            <version>3.0.1</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>jsp-customer-demo</finalName>
    </build>
</project>

依赖版本会随时间更新。实际项目应结合所用 Java、Tomcat 和依赖的兼容要求,使用组织认可的版本,并通过依赖扫描和升级流程维护。

四、创建连接池并管理生命周期

把数据库连接池放在 ServletContext 中,应用内的 DAO 可以通过它获取连接。注意,连接池不是单个数据库连接:DAO 每次借用连接,用完后关闭它,连接会归还给连接池,而不是直接关闭底层网络连接。

package com.example;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class DatabaseContextListener implements ServletContextListener {
    private HikariDataSource dataSource;

    @Override
    public void contextInitialized(ServletContextEvent event) {
        String url = env("DB_URL",
                "jdbc:postgresql://localhost:5432/jsp_demo");
        String user = env("DB_USER", "postgres");
        String password = env("DB_PASSWORD", "");

        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(user);
        config.setPassword(password);
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(2);
        config.setPoolName("customer-pool");

        dataSource = new HikariDataSource(config);
        event.getServletContext().setAttribute("dataSource", dataSource);
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        if (dataSource != null) {
            dataSource.close();
        }
    }

    private static String env(String name, String defaultValue) {
        String value = System.getenv(name);
        return value == null ? defaultValue : value;
    }
}

本地开发时可以设置环境变量 DB_URL、DB_USER 和 DB_PASSWORD。默认 URL 指向本机 PostgreSQL。真实部署不要把生产密码写入源码、提交到版本库或放进公开的配置文件;应使用受控的环境变量、密钥管理服务或容器平台的 Secret 配置,并限制数据库账号权限。

连接池大小并不是越大越好。上例的 10 是演示配置,不是适用于所有系统的推荐值。应结合数据库连接上限、应用实例数量、请求并发和连接占用时间进行设置。多个应用实例各自拥有连接池时,数据库看到的连接总数是各实例连接池连接数的合计。

五、定义查询结果对象和 DAO

客户对象只保存展示需要的字段。使用 JavaBean 风格的 getter,便于 JSP EL 通过 customer.id、customer.name 读取属性。

package com.example;

public class Customer {
    private final long id;
    private final String name;
    private final String email;

    public Customer(long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

DAO 负责执行 SQL 并把结果集转换为对象。此处使用 PreparedStatement,虽然当前 SQL 没有外部输入参数,仍保留参数化查询的写法,便于后续扩展时避免将用户输入拼接进 SQL。

package com.example;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class CustomerDao {
    private static final String SQL =
            "SELECT id, name, email FROM customers ORDER BY id LIMIT ?";

    private final DataSource dataSource;

    public CustomerDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public List<Customer> findFirstPage(int limit) throws SQLException {
        if (limit < 1 || limit > 100) {
            throw new IllegalArgumentException("limit 必须在 1 到 100 之间");
        }

        List<Customer> customers = new ArrayList<>();

        try (Connection connection = dataSource.getConnection();
             PreparedStatement statement = connection.prepareStatement(SQL)) {
            statement.setInt(1, limit);

            try (ResultSet resultSet = statement.executeQuery()) {
                while (resultSet.next()) {
                    customers.add(new Customer(
                            resultSet.getLong("id"),
                            resultSet.getString("name"),
                            resultSet.getString("email")
                    ));
                }
            }
        }

        return customers;
    }
}

这里的 LIMIT 是 PostgreSQL 支持的分页语法。其他数据库对分页的写法可能不同。参数化语句的占位符用于绑定数据值,不能用来替换表名、列名或 SQL 关键字;如果列名或排序方向需要动态选择,应通过白名单映射到固定 SQL 片段,而不是把请求参数直接拼接进去。

六、Servlet 调用 DAO,并转发到 JSP

Servlet 从应用上下文取出连接池,调用 DAO 查询固定上限的记录,然后把结果放入请求属性。JSP 放在 WEB-INF 目录下,浏览器不能直接通过 URL 请求它,只能由应用内部转发访问。

package com.example;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;

@WebServlet("/customers")
public class CustomerServlet extends HttpServlet {
    private CustomerDao customerDao;

    @Override
    public void init() throws ServletException {
        DataSource dataSource = (DataSource) getServletContext()
                .getAttribute("dataSource");

        if (dataSource == null) {
            throw new ServletException("数据库连接池未初始化");
        }

        customerDao = new CustomerDao(dataSource);
    }

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {
        response.setCharacterEncoding("UTF-8");

        try {
            List<Customer> customers = customerDao.findFirstPage(100);
            request.setAttribute("customers", customers);
            request.getRequestDispatcher("/WEB-INF/views/customers.jsp")
                    .forward(request, response);
        } catch (SQLException e) {
            log("读取客户列表失败", e);
            response.sendError(
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "暂时无法读取客户列表"
            );
        }
    }
}

异常详情写入服务器日志,响应给浏览器的错误信息保持简洁。不要把 SQL、数据库连接地址、账号、密码或堆栈跟踪直接展示给普通用户。生产环境还应配置统一的异常处理、日志访问权限和监控告警。

七、用 JSP 和 JSTL 展示数据

JSP 页面只负责呈现结果。下面使用 JSTL 的 c:forEach 遍历集合,使用 c:out 输出动态文本。c:out 默认会转义 XML/HTML 特殊字符,降低用户数据被当作 HTML 标签或脚本执行的风险。

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>客户列表</title>
    <style>
        body { font-family: sans-serif; margin: 2rem; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ccc; padding: .6rem; text-align: left; }
        th { background: #f3f3f3; }
    </style>
</head>
<body>
    <h1>客户列表</h1>

    <c:choose>
        <c:when test="${empty customers}">
            <p>目前没有客户记录。</p>
        </c:when>
        <c:otherwise>
            <table>
                <thead>
                    <tr>
                        <th>编号</th>
                        <th>姓名</th>
                        <th>邮箱</th>
                    </tr>
                </thead>
                <tbody>
                    <c:forEach var="customer" items="${customers}">
                        <tr>
                            <td><c:out value="${customer.id}"/></td>
                            <td><c:out value="${customer.name}"/></td>
                            <td><c:out value="${customer.email}"/></td>
                        </tr>
                    </c:forEach>
                </tbody>
            </table>
            <p>本页最多显示 100 条记录。</p>
        </c:otherwise>
    </c:choose>
</body>
</html>

在 JSP 中,${customer.name} 会通过 EL 读取对象的属性。不要把用户输入或数据库内容直接用 JSP 表达式输出到 HTML 中,也不要用字符串拼接手工生成页面。HTML 转义能帮助防止 HTML 文本位置上的跨站脚本(XSS),但不是所有输出场景的通用安全方案:JavaScript、CSS、URL 和 HTML 属性等上下文有不同的编码要求。需要在这些上下文输出动态值时,应选择适用于对应上下文的安全编码方式。

八、构建、部署与访问

确认 PostgreSQL 已启动,并且数据库、表和测试数据已经创建。设置连接所需的环境变量后,在项目目录运行:

mvn clean package

构建成功后,将生成的 target/jsp-customer-demo.war 部署到 Tomcat 10.1 的 webapps 目录,启动 Tomcat。然后访问:

http://localhost:8080/jsp-customer-demo/customers

如果应用部署时使用了不同的上下文路径,应相应调整 URL。浏览器显示客户表格,说明 Servlet、DAO、连接池、JDBC 查询和 JSP 展示流程已经连通。

九、为什么不在 JSP 里直接连接数据库

入门示例中常出现以下形式:在 JSP 中通过脚本片段创建连接、执行 SQL,再用循环输出 HTML。这种代码虽然可能运行,但不建议用于新的项目:

  • 页面同时处理请求、数据库连接和 HTML,修改任一部分都容易影响其他部分。
  • 页面异常时数据库资源可能没有正确释放;数据库查询和页面渲染也难以分别测试。
  • 把 SQL 和业务逻辑放进视图,使权限校验、事务和错误处理更难统一。
  • 如果在 JSP 中直接输出数据库字段,未经适当转义的内容可能造成 XSS。

把数据库访问移到 DAO、请求流程放到 Servlet,并不意味着 JSP 永远不能包含表达式或标签。关键是把 JSP 限制在展示职责内,避免在视图中建立连接、执行更新或放置复杂业务判断。

十、参数限制和安全边界

  • 查询条数上限:示例固定调用 findFirstPage(100),DAO 还校验 limit 必须在 1 到 100 之间。限制条数可以控制单次响应的数据规模,但不等同于完整分页;数据超过 100 条时,后续记录不会显示。实际系统通常需要经过校验的页码、页大小和稳定的排序条件。
  • SQL 参数化:PreparedStatement 能把绑定值作为数据处理,降低这些值被解释为 SQL 语法的风险。它不会自动验证业务规则,也不能替代权限检查;它也不能安全地把表名或列名当作普通参数绑定。
  • HTML 转义:c:out 适用于本例中输出到 HTML 文本节点的动态值。若把值放进脚本、样式、URL 或特定属性,需要考虑相应上下文,不能把“开了转义”当成覆盖所有场景的保证。
  • 访问权限:示例没有实现登录和授权,因此适合本地演示,不应未经访问控制就公开部署。正式应用应在 Servlet 或统一的安全层中检查用户身份和数据访问权限。
  • 数据库凭据与权限:使用环境变量只是演示配置方式,并不自动保证部署环境安全。数据库账号应遵循最小权限原则;展示列表的账号通常不应拥有不必要的表结构修改或管理权限。
  • 连接池:连接池减少频繁建连的开销,但不会消除数据库容量限制。生产配置要结合连接超时、空闲回收、最大连接数和应用实例数验证。

十一、常见错误排查

1. 出现 Jakarta 类找不到或类型不匹配

检查 Tomcat 版本、依赖和 import 是否一致。Tomcat 10.1 使用 jakarta.servlet;面向旧版 Java EE 容器的代码可能使用 javax.servlet。两套命名空间不是可以随意混用的同一类型。

2. 页面报错,提示找不到 JSTL 标签库

确认项目包含 JSTL API 和实现依赖,且 JSTL URI 使用 jakarta.tags.core。部分旧教程仍使用旧标签库 URI;照搬旧配置到 JSTL 3 项目中可能导致标签无法解析。修改依赖后重新构建并重新部署。

3. 数据库连接失败

检查 PostgreSQL 是否运行、数据库名和端口是否正确,以及运行 Tomcat 的进程是否读取到了 DB_URL、DB_USER、DB_PASSWORD。注意,终端中设置的环境变量不一定会传递给以服务方式启动的 Tomcat。查看服务器日志中的根因,区分认证失败、网络不可达和数据库不存在。

4. 浏览器直接访问 JSP 返回 404

本例 JSP 位于 WEB-INF 下,浏览器不能直接请求它,这是预期行为。应访问 Servlet 映射地址 /customers,由 Servlet 使用服务器端转发显示 JSP。

5. 页面内容乱码

检查 JSP 的 pageEncoding 和响应的 contentType 是否为 UTF-8,源码文件也应以 UTF-8 保存。数据库、数据库连接和字段编码同样需要兼容;仅设置页面字符集不能修复所有编码不一致问题。

6. 查询结果为空或只显示部分数据

确认 SQL 在数据库客户端中能够返回数据,应用连接的是同一个数据库,并检查测试数据是否已提交。当前示例最多展示 100 条,不会读取完整结果集;数据量更大时应增加分页,而不是简单移除上限后一次加载全部记录。

7. JSP 中的 EL 表达式显示为空

检查 Servlet 是否在转发前调用了 request.setAttribute("customers", customers),并确认属性名与 JSP 中的 ${customers} 一致。还要确认使用的是服务器端 forward:重定向会发起新的 HTTP 请求,原请求中的请求属性不会自动保留。

总结

用 JSP 展示数据库查询结果时,页面不必直接连接数据库。把请求处理交给 Servlet、把 JDBC 查询放入 DAO、把 HTML 呈现交给 JSP,可以让代码更清晰,也更容易测试和维护。本文示例通过连接池复用数据库连接,使用 PreparedStatement 执行查询,限制单次返回条数,并通过 JSTL 输出动态内容。

这个结构适合学习 JSP 和构建简单的服务端渲染页面,但示例没有包含完整分页、登录授权、事务管理和生产级配置。将它用于真实业务前,应根据数据量、访问权限、部署环境和数据库负载补齐这些能力,并持续维护依赖与安全配置。

© 版权声明

相关文章