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









