在现代Web开发中,将后台数据库中的数据动态地展示给用户,是一项核心且基础的任务,对于使用Java技术栈的开发者而言,JSP(JavaServer Pages)与Servlet的组合是经典的解决方案,理解如何将从数据库中查询到的数据高效、安全地传递到JSP前台页面进行渲染,是掌握Java Web开发的关键一环,本文将详细拆解这一过程,遵循MVC(Model-View-Controller)设计模式,阐述其核心思想与实现步骤。

核心思想:分离关注点,遵循MVC模式
在讨论具体技术之前,我们必须明确其背后的指导思想——MVC模式,这种模式将应用程序清晰地划分为三个部分,以实现高内聚、低耦合:
- Model(模型):负责业务逻辑和数据封装,在我们的场景中,它通常指代一个JavaBean(POJO),用于封装从数据库中取出的一行数据(例如一个用户信息),或者是一个包含多个JavaBean的集合(例如用户列表)。
- View(视图):负责数据显示,它不包含任何业务逻辑,仅负责将模型中的数据以美观的HTML格式呈现给用户,JSP页面就扮演着视图的角色。
- Controller(控制器):作为模型和视图之间的协调者,它接收用户的请求,调用模型处理业务逻辑(如查询数据库),然后选择合适的视图进行响应,并将处理结果(数据)传递给视图,Servlet正是控制器的最佳实现。
遵循MVC模式,数据传递的路径就变得非常清晰:数据库 → Servlet(Controller) → 数据模型 → JSP(View)。
第一步:后端数据准备(Servlet的工作)
Servlet是整个数据流转的起点和指挥中心,它的主要职责包括连接数据库、执行查询、封装数据,并将数据“放置”在一个JSP可以访问到的地方。
1 数据库连接与查询
Servlet需要使用JDBC(Java Database Connectivity)技术来连接数据库并执行SQL查询,这是一个标准流程,通常包括加载驱动、建立连接、创建Statement或PreparedStatement对象、执行查询并获取ResultSet。
// 示例代码片段
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
List<User> userList = new ArrayList<>(); // 创建一个List用于存放用户数据
try {
// 1. 获取数据库连接(实际项目中通常使用连接池)
conn = dataSource.getConnection();
String sql = "SELECT id, name, email FROM users";
// 2. 创建PreparedStatement
pstmt = conn.prepareStatement(sql);
// 3. 执行查询
rs = pstmt.executeQuery();
// 4. 处理结果集
while (rs.next()) {
User user = new User(); // 创建User模型对象
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
userList.add(user); // 将封装好的User对象添加到List中
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5. 关闭资源(非常重要)
// ... 关闭 rs, pstmt, conn
}
2 数据封装
直接将ResultSet对象传递给JSP是一种非常糟糕的做法。ResultSet与数据库连接紧密绑定,如果在JSP页面中处理它,会导致数据库连接长时间占用,并且严重违反MVC原则,使视图层混杂了数据访问逻辑。
正确的做法是,如上例所示,遍历ResultSet,将每一行数据封装成一个独立的JavaBean对象(如User),然后将所有这些对象存入一个List集合中,这个List<User>就是我们的“模型”,它是一个纯粹的、与数据库无关的Java对象集合。
3 设置请求作用域属性
数据封装完毕后,Servlet需要将这个userList传递给JSP,传递的“桥梁”就是HTTP请求对象(HttpServletRequest),Servlet可以通过调用setAttribute方法,将任何Java对象存入请求作用域中。
// 将userList存入request作用域,键名为"userList"
request.setAttribute("userList", userList);
这里,"userList"是一个字符串键,JSP将通过这个键来获取数据;userList则是我们之前创建的包含所有用户数据的List对象。
第二步:请求转发
设置完属性后,Servlet需要将请求转发给指定的JSP页面来处理响应,这里必须使用请求转发,而不是重定向。

// 将请求转发到showUsers.jsp页面
request.getRequestDispatcher("showUsers.jsp").forward(request, response);
forward()方法会将同一个request和response对象传递给showUsers.jsp,这意味着,我们刚刚存入request中的userList属性,在JSP页面中是完全可以访问的,而重定向会告知浏览器发起一个新的请求,原请求中的所有属性都会丢失。
第三步:前端数据渲染(JSP的工作)
JSP页面的任务单一而纯粹:从请求作用域中取出数据,并将其渲染成HTML。
1 使用EL和JSTL(推荐做法)
为了保持JSP页面的整洁,避免在页面中嵌入大量的Java代码(即Scriptlet <% ... %>),强烈推荐使用表达式语言(EL)和JSP标准标签库(JSTL)。
需要在JSP页面顶部引入JSTL核心库:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
2 遍历并展示数据
我们可以使用JSTL的<c:forEach>标签来遍历Servlet传递过来的userList,并使用EL表达式 来访问每个User对象的属性。
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<%-- 使用c:forEach遍历request作用域中的userList --%>
<c:forEach items="${userList}" var="user">
<tr>
<%-- 使用EL表达式访问User对象的属性 --%>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
</c:forEach>
</tbody>
</table>
items="${userList}":items属性指定要遍历的集合。${userList}是EL表达式,它会自动在page、request、session、application作用域中查找名为userList的属性,由于我们是在Servlet中将其存入request作用域的,所以这里能正确找到。var="user":var属性定义了一个临时变量名,在每次循环中,user会引用集合中的当前元素(即一个User对象)。${user.id}:EL表达式会调用user对象的getId()方法(注意:EL会自动将属性名首字母大写并加上get前缀来寻找对应的getter方法),并将其返回值输出到页面。
浏览器会收到一个包含完整用户数据的HTML表格。
将数据库数据传递到JSP前台的完整流程,可以概括为以下几个关键步骤:
| 步骤 | 执行者 | 核心操作 | 目的 |
|---|---|---|---|
| 1 | Servlet (Controller) | JDBC查询数据库 | 获取原始数据 |
| 2 | Servlet (Controller) | 遍历ResultSet,封装为JavaBean集合 | 创建与数据库无关的模型数据 |
| 3 | Servlet (Controller) | request.setAttribute("key", data) |
将模型数据放入请求作用域 |
| 4 | Servlet (Controller) | request.getRequestDispatcher().forward() |
将请求(包含数据)转发给JSP |
| 5 | JSP (View) | 使用JSTL的<c:forEach>和EL |
从请求中获取数据并渲染成HTML |
通过这一套严谨的流程,我们实现了业务逻辑、数据访问和视图展示的彻底分离,使得代码结构清晰、易于维护和扩展,是Java Web开发中值得遵循的最佳实践。
相关问答FAQs
问题1:为什么不应该在JSP页面中直接写JDBC代码来查询数据库?

解答: 直接在JSP中编写JDBC代码是一种严重的反模式,主要弊端如下:
- 违反MVC原则:它将数据访问逻辑(模型)和视图展示逻辑(视图)混杂在一起,导致代码混乱,职责不清。
- 难以维护:一旦数据库连接信息或SQL语句需要变更,就必须修改JSP文件,增加了维护成本和风险,业务逻辑和页面逻辑的耦合使得任何一方的改动都可能影响另一方。
- 资源管理风险:在JSP中管理数据库连接的打开和关闭非常容易出错,稍有不慎就可能导致连接泄漏,最终耗尽数据库资源,使整个应用崩溃。
- 安全性与可测试性差:代码分散且难以进行单元测试,同时也更容易引入SQL注入等安全漏洞。
通过Servlet作为控制器来处理所有后台逻辑,可以完美地解决以上所有问题。
问题2:request.setAttribute(), session.setAttribute(), 和 application.setAttribute() 有什么区别?
解答: 这三个方法都用于在Web应用中存储数据,但它们的作用域(生命周期和可见范围)完全不同,选择哪个取决于数据的使用场景。
-
request.setAttribute():- 作用域:请求作用域。
- 生命周期:仅在单次HTTP请求-响应周期内有效,当服务器将响应发送给客户端后,该请求结束,其中存储的所有数据都会被销毁。
- 用途:最常用于在Servlet和JSP之间传递数据,将查询结果从Servlet传递给用于展示的JSP页面。
-
session.setAttribute():- 作用域:会话作用域。
- 生命周期:在整个用户会话期间有效,会话从用户首次访问网站开始,直到浏览器关闭或会话超时(例如30分钟无操作)。
- 用途:用于存储与特定用户相关的数据,用户的登录信息、购物车内容等,这些数据需要在用户浏览不同页面时持续可用。
-
application.setAttribute():- 作用域:应用作用域。
- 生命周期:在整个Web应用程序的生命周期内有效,从服务器启动应用开始,直到服务器关闭或应用被卸载。
- 用途:用于存储全局共享的数据,网站的配置参数、所有用户共享的缓存数据、在线用户计数器等,此作用域内的数据对所有用户的所有会话都是可见的,因此在使用时需考虑线程安全问题。