JSP与Servlet整合数据库开发:构建Java Web应用的全栈指南
概述
在Java Web开发领域,JSP(JavaServer Pages)与Servlet是构建动态Web应用的核心技术组合。Servlet作为Java EE的基础组件,负责处理客户端请求、执行业务逻辑并协调数据交互;JSP则专注于视图渲染,将动态数据与静态页面模板结合生成HTML响应;而关系型数据库则提供了数据的持久化存储能力。
三者的整合本质上是MVC(Model-View-Controller)设计模式在Java Web中的经典实践:Servlet扮演“控制器”(Controller),JSP扮演“视图”(View),数据模型(Model)与DAO(Data Access Object)层则负责数据的封装与数据库交互。这种分层架构能有效分离“业务逻辑”“数据处理”与“页面展示”,大幅提升代码的可维护性、可扩展性和可测试性。
本文将以一个用户管理系统为实战案例,从环境搭建到代码实现,完整讲解如何基于JSP+Servlet+JDBC构建可运行的Java Web应用,并融入安全、性能等最佳实践。
技术栈架构
要理解三者的协同机制,首先需要明确各组件的角色与交互流程。以下架构图清晰展示了请求从客户端发起至响应返回的全链路:
┌─────────┐ HTTP请求/响应 ┌─────────┐ JDBC调用 ┌─────────┐
│ 客户端 │ ◄────────────────► │ Servlet │ ◄──────────────► │ 数据库 │
│(浏览器) │ │ (控制器) │ │ (MySQL/ │
└─────────┘ └─────────┘ │ Oracle等)△ └─────────┘│ 设置属性/转发请求│┌─────────┐│ JSP ││ (视图) │└─────────┘
各组件核心作用解析
- 客户端(浏览器):用户交互入口,负责发送HTTP请求(如GET/POST),并接收服务器返回的HTML/CSS/JS响应进行渲染。
- Servlet(控制器):
- 接收客户端请求,解析请求参数(如表单数据、URL参数);
- 调用DAO层执行数据库操作(如查询用户、新增数据);
- 将处理结果封装为“请求属性”(Request Attribute),转发至JSP视图;
- 或通过重定向(Redirect)引导客户端跳转至其他页面(如新增用户后跳转至列表页)。
- JSP(视图):
- 以HTML为基础,通过EL表达式(${}) 读取Servlet传递的请求属性;
- 借助JSTL标签库(如
<c:forEach>
)实现循环、条件判断等动态逻辑; - 最终生成完整的HTML页面,通过Servlet返回给客户端。
- 数据库:持久化存储应用数据(如用户信息),通过JDBC与Servlet层交互。
- JDBC(Java Database Connectivity):Java访问数据库的标准API,提供了连接数据库、执行SQL语句、处理结果集的统一接口,屏蔽了不同数据库的底层差异。
环境准备
在开始编码前,需完成开发环境与项目结构的搭建。以下是详细的准备步骤:
1. 所需技术与版本建议
选择稳定且主流的版本组合,避免因版本兼容问题导致开发受阻:
技术组件 | 推荐版本 | 核心作用 |
---|---|---|
Java SE | JDK 11+ | 基础开发语言,Servlet/JSP的运行依赖 |
Servlet容器 | Tomcat 10+ | 运行Servlet/JSP的服务器(兼容Servlet 5.0+) |
JSP | 2.3+ | 动态视图生成 |
JDBC | 4.3+ | 数据库连接API |
数据库 | MySQL 8.0+ | 关系型数据库,存储应用数据 |
构建工具(可选) | Maven 3.6+ | 依赖管理与项目构建 |
开发工具 | IntelliJ IDEA/Eclipse | 代码编写与调试 |
2. 环境搭建关键步骤
(1)安装JDK并配置环境变量
- 下载JDK 11+(如Oracle JDK或OpenJDK);
- 配置
JAVA_HOME
(指向JDK安装目录)、PATH
(添加%JAVA_HOME%\bin
); - 验证:命令行执行
java -version
,显示版本信息即配置成功。
(2)安装Tomcat并测试
- 下载Tomcat 10+(Apache官网),解压至无空格目录;
- 启动:运行
tomcat/bin/startup.bat
(Windows)或startup.sh
(Linux); - 验证:浏览器访问
http://localhost:8080
,显示Tomcat默认页面即启动成功。
(3)安装MySQL并创建数据库
- 安装MySQL 8.0+,配置root用户密码;
- 通过MySQL命令行或Navicat等工具创建应用数据库(如
user_management
):CREATE DATABASE user_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
3. 项目结构设计
遵循MVC与分层思想的项目结构能让代码逻辑更清晰,以下是标准的Maven项目结构(非Maven项目可参考此目录划分):
/project-root # 项目根目录├── /src│ └── /main│ ├── /java # Java源代码目录(核心业务逻辑)│ │ └── /com│ │ └── /yourapp│ │ ├── /servlets # 控制器:Servlet类│ │ ├── /models # 模型:POJO类(封装数据)│ │ ├── /daos # 数据访问层:DAO接口与实现│ │ ├── /services # 业务逻辑层:处理复杂业务(可选)│ │ └── /utils # 工具类:数据库连接、加密等│ ├── /webapp # Web资源目录(视图、静态资源、配置)│ │ ├── /WEB-INF # 受保护目录(客户端无法直接访问)│ │ │ ├── web.xml # Web应用配置文件(Servlet映射等)│ │ │ └── /views # 视图:JSP文件│ │ ├── /css # 静态资源:样式表│ │ ├── /js # 静态资源:JavaScript│ │ └── /images # 静态资源:图片│ └── /resources # 配置资源:数据库连接参数等└── pom.xml # Maven配置:依赖坐标
关键目录说明
- /WEB-INF:核心配置目录,包含
web.xml
(Servlet 3.0前需手动配置Servlet映射)和JSP视图。由于客户端无法直接访问此目录下的资源,需通过Servlet转发才能访问JSP,保证了视图的安全性。 - /java/com/yourapp/daos:DAO层负责与数据库直接交互,封装了所有SQL操作,使Servlet层无需关心数据访问细节。
- /java/com/yourapp/services:当业务逻辑复杂时(如“注册用户前验证邮箱唯一性”),可新增Service层介于Servlet与DAO之间,避免Servlet代码臃肿。
数据库设计
数据是应用的核心,合理的数据库设计是系统稳定运行的基础。以“用户管理系统”为例,我们设计users
表存储用户基本信息,遵循以下设计原则:
- 主键唯一:使用自增ID作为主键;
- 约束明确:非空(NOT NULL)、唯一(UNIQUE)约束保证数据完整性;
- 字符集适配:使用
utf8mb4
支持中文及特殊字符; - 时间追踪:添加
created_at
记录用户注册时间。
1. 创建users
表SQL语句
USE user_management; -- 切换至应用数据库CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID(主键,自增)',username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名(唯一,非空)',email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱(唯一,非空)',password VARCHAR(255) NOT NULL COMMENT '密码(哈希存储,非空)',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间(默认当前时间)'
) COMMENT '用户表';
2. 设计补充说明
- password字段:此处定义为
VARCHAR(255)
,因为实际开发中严禁存储明文密码,需通过BCrypt等算法哈希后存储(哈希结果通常为60+字符); - email字段:添加
UNIQUE
约束避免重复注册,同时便于后续实现“忘记密码”等功能; - TIMESTAMP类型:
DEFAULT CURRENT_TIMESTAMP
表示插入数据时若未指定created_at
,自动填充当前时间。
模型层(Model):封装数据
模型层由POJO(Plain Old Java Object,简单Java对象) 构成,其核心作用是“封装数据”——将数据库表中的一条记录映射为一个Java对象(如users
表的一条记录对应一个User
对象),便于在各层之间传递数据。
POJO类需满足以下规范:
- 私有属性(与数据库表字段对应);
- 无参构造方法(便于反射实例化,如JDBC结果集映射);
- 有参构造方法(便于快速创建对象);
- Getter/Setter方法(用于访问和修改属性);
- 可选:
toString()
方法(便于调试时打印对象信息)。
1. User类实现代码
package com.yourapp.models;import java.sql.Timestamp;/*** 用户模型类:映射users表的一条记录*/
public class User {// 私有属性:与users表字段一一对应private int id;private String username;private String email;private String password;private Timestamp createdAt;// 1. 无参构造方法(必须,JDBC映射时需要)public User() {}// 2. 有参构造方法(新增用户时使用,无需id和createdAt)public User(String username, String email, String password) {this.username = username;this.email = email;this.password = password;}// 3. Getter方法:获取属性值public int getId() {return id;}public String getUsername() {return username;}public String getEmail() {return email;}public String getPassword() {return password;}public Timestamp getCreatedAt() {return createdAt;}// 4. Setter方法:修改属性值(id和createdAt由数据库生成,仅需setter)public void setId(int id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setEmail(String email) {this.email = email;}public void setPassword(String password) {this.password = password;}public void setCreatedAt(Timestamp createdAt) {this.createdAt = createdAt;}// 可选:toString()方法,便于调试时打印用户信息@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", email='" + email + '\'' +", createdAt=" + createdAt +'}';}
}
2. 模型层设计意义
- 数据封装:将用户的多个属性(id、username等)封装为一个对象,避免方法参数过多(如新增用户时无需传递5个独立参数,只需传递一个
User
对象); - 解耦依赖:Servlet层、DAO层通过
User
对象交互,无需关心数据的具体存储格式(如数据库字段类型); - 扩展性强:若后续
users
表新增字段(如phone
),只需在User
类中添加对应属性和Getter/Setter即可,无需大面积修改代码。
数据访问层(DAO):操作数据库
DAO层是“模型层”与“数据库”之间的桥梁,负责所有数据库交互逻辑(如新增、查询、修改、删除用户)。其设计遵循DAO模式,核心思想是“抽象数据访问细节”,使上层(Servlet/Service)无需编写JDBC代码即可操作数据库。
DAO层通常包含两部分:
- DAO接口:定义数据访问方法(如
createUser(User user)
); - DAO实现类:实现接口,编写具体的JDBC代码(连接数据库、执行SQL、处理结果集)。
此外,需创建一个数据库连接工具类,统一管理数据库连接的创建与关闭,避免代码冗余。
1. 工具类:数据库连接管理(DBUtils)
JDBC连接数据库的步骤固定(加载驱动→创建连接),但频繁创建/关闭连接会严重影响性能。此处先实现基础连接工具类,后续会优化为“连接池”。
package com.yourapp.utils;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;/*** 数据库连接工具类:提供获取连接和关闭连接的静态方法*/
public class DBUtils {// 数据库连接参数(建议从配置文件读取,此处为演示硬编码)private static final String DB_URL = "jdbc:mysql://localhost:3306/user_management?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";private static final String DB_USER = "root"; // 你的MySQL用户名private static final String DB_PASSWORD = "123456"; // 你的MySQL密码private static final String DRIVER_CLASS = "com.mysql.cj.jdbc.Driver"; // MySQL 8.0+驱动类static {// 静态代码块:加载数据库驱动(仅执行一次)try {Class.forName(DRIVER_CLASS);System.out.println("数据库驱动加载成功!");} catch (ClassNotFoundException e) {// 驱动加载失败时抛出运行时异常,终止程序启动throw new RuntimeException("数据库驱动加载失败,请检查依赖是否正确!", e);}}/*** 获取数据库连接* @return Connection 数据库连接对象* @throws SQLException 连接失败时抛出异常*/public static Connection getConnection() throws SQLException {return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);}/*** 关闭数据库资源(ResultSet、PreparedStatement、Connection)* 注意关闭顺序:先ResultSet,再PreparedStatement,最后Connection*/public static void closeResources(AutoCloseable... resources) {for (AutoCloseable resource : resources) {if (resource != null) {try {resource.close(); // 关闭资源} catch (Exception e) {e.printStackTrace();}}}}
}
关键代码解释
- 连接参数:
useSSL=false
:禁用SSL连接(开发环境简化配置);serverTimezone=UTC
:设置时区,避免MySQL 8.0+的时区警告;allowPublicKeyRetrieval=true
:允许获取服务器公钥,解决连接时的权限问题。
- 静态代码块:类加载时自动执行,仅加载一次驱动,提高效率。
- closeResources方法:使用
AutoCloseable
接口接收所有可关闭资源(ResultSet、PreparedStatement、Connection均实现此接口),简化关闭逻辑。
2. DAO接口:定义数据访问方法(UserDAO)
接口定义了“做什么”,不关心“怎么做”,便于后续替换实现(如从MySQL切换到Oracle时,只需新增Oracle的DAO实现类)。
package com.yourapp.daos;import com.yourapp.models.User;
import java.util.List;/*** 用户DAO接口:定义用户数据的访问方法*/
public interface UserDAO {/*** 新增用户* @param user 待新增的用户对象(包含username、email、password)* @return boolean 新增成功返回true,失败返回false*/boolean createUser(User user);/*** 查询所有用户* @return List<User> 所有用户的列表(无数据时返回空列表,非null)*/List<User> getAllUsers();/*** 根据ID查询用户* @param id 用户ID* @return User 找到返回用户对象,未找到返回null*/User getUserById(int id);/*** 根据用户名查询用户(用于验证用户名是否已存在)* @param username 用户名* @return User 找到返回用户对象,未找到返回null*/User getUserByUsername(String username);/*** 更新用户信息* @param user 待更新的用户对象(包含id及需更新的字段)* @return boolean 更新成功返回true,失败返回false*/boolean updateUser(User user);/*** 根据ID删除用户* @param id 用户ID* @return boolean 删除成功返回true,失败返回false*/boolean deleteUserById(int id);
}
3. DAO实现类:编写JDBC逻辑(UserDAOImpl)
实现类负责“怎么做”,通过JDBC完成具体的数据库操作。核心要点:
- 使用
PreparedStatement
代替Statement
,防止SQL注入; - 采用
try-with-resources
语法自动关闭资源(无需手动调用close()
); - 处理
SQLException
,并向上层传递或返回状态标识。
package com.yourapp.daos.impl;import com.yourapp.daos.UserDAO;
import com.yourapp.models.User;
import com.yourapp.utils.DBUtils;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;/*** 用户DAO实现类:基于MySQL的JDBC实现*/
public class UserDAOImpl implements UserDAO {// 1. 新增用户@Overridepublic boolean createUser(User user) {// SQL语句:使用?作为占位符,避免拼接SQL(防止注入)String sql = "INSERT INTO users (username, email, password) VALUES (?, ?, ?)";Connection conn = null;PreparedStatement pstmt = null;try {// 获取连接conn = DBUtils.getConnection();// 创建PreparedStatement(预编译SQL)pstmt = conn.prepareStatement(sql);// 绑定参数:按占位符顺序设置值(1表示第一个?,依次类推)pstmt.setString(1, user.getUsername());pstmt.setString(2, user.getEmail());pstmt.setString(3, user.getPassword()); // 注意:实际开发需先哈希密码// 执行更新操作:executeUpdate()返回受影响的行数int rowsAffected = pstmt.executeUpdate();// 受影响行数>0表示新增成功return rowsAffected > 0;} catch (SQLException e) {e.printStackTrace();// 若用户名/邮箱重复(UNIQUE约束冲突),可在此处特殊处理if (e.getErrorCode() == 1062) {System.out.println("用户名或邮箱已存在!");}return false;} finally {// 关闭资源(顺序:pstmt → conn)DBUtils.closeResources(pstmt, conn);}}// 2. 查询所有用户@Overridepublic List<User> getAllUsers() {String sql = "SELECT id, username, email, password, created_at FROM users ORDER BY created_at DESC";List<User> userList = new ArrayList<>(); // 初始化空列表,避免返回nullConnection conn = null;PreparedStatement pstmt = null;ResultSet rs = null;try {conn = DBUtils.getConnection();pstmt = conn.prepareStatement(sql);// 执行查询操作:executeQuery()返回ResultSet结果集rs = pstmt.executeQuery();// 遍历结果集:rs.next()判断是否有下一条记录while (rs.next()) {// 创建User对象,映射结果集字段User user = new User();user.setId(rs.getInt("id")); // 按字段名获取int值user.setUsername(rs.getString("username")); // 按字段名获取String值user.setEmail(rs.getString("email"));user.setPassword(rs.getString("password"));user.setCreatedAt(rs.getTimestamp("created_at")); // 按字段名获取Timestamp值// 添加到列表userList.add(user);}} catch (SQLException e) {e.printStackTrace();} finally {// 关闭资源(顺序:rs → pstmt → conn)DBUtils.closeResources(rs, pstmt, conn);}return userList;}// 3. 根据ID查询用户@Overridepublic User getUserById(int id) {String sql = "SELECT id, username, email, password, created_at FROM users WHERE id = ?";User user = null;Connection conn = null;PreparedStatement pstmt = null;ResultSet rs = null;try {conn = DBUtils.getConnection();pstmt = conn.prepareStatement(sql);pstmt.setInt(1, id); // 绑定ID参数rs = pstmt.executeQuery();// 若找到记录,映射为User对象if (rs.next()) {user = new User();user.setId(rs.getInt("id"));user.setUsername(rs.getString("username"));user.setEmail(rs.getString("email"));user.setPassword(rs.getString("password"));user.setCreatedAt(rs.getTimestamp("created_at"));}} catch (SQLException e) {e.printStackTrace();} finally {DBUtils.closeResources(rs, pstmt, conn);}return user;}// 4. 根据用户名查询用户@Overridepublic User getUserByUsername(String username) {String sql = "SELECT id, username, email, password, created_at FROM users WHERE username = ?";User user = null;Connection conn = null;PreparedStatement pstmt = null;ResultSet rs = null;try {conn = DBUtils.getConnection();pstmt = conn.prepareStatement(sql);pstmt.setString(1, username);rs = pstmt.executeQuery();if (rs.next()) {user = new User();user.setId(rs.getInt("id"));user.setUsername(rs.getString("username"));user.setEmail(rs.getString("email"));user.setPassword(rs.getString("password"));user.setCreatedAt(rs.getTimestamp("created_at"));}} catch (SQLException e) {e.printStackTrace();} finally {DBUtils.closeResources(rs, pstmt, conn);}return user;}// 5. 更新用户信息@Overridepublic boolean updateUser(User user) {// 只更新username、email、password字段(id为条件,created_at不更新)String sql = "UPDATE users SET username = ?, email = ?, password = ? WHERE id = ?";Connection conn = null;PreparedStatement pstmt = null;try {conn = DBUtils.getConnection();pstmt = conn.prepareStatement(sql);pstmt.setString(1, user.getUsername());pstmt.setString(2, user.getEmail());pstmt.setString(3, user.getPassword());pstmt.setInt(4, user.getId()); // 按ID定位待更新记录int rowsAffected = pstmt.executeUpdate();return rowsAffected > 0;} catch (SQLException e) {e.printStackTrace();return false;} finally {DBUtils.closeResources(pstmt, conn);}}// 6. 根据ID删除用户@Overridepublic boolean deleteUserById(int id) {String sql = "DELETE FROM users WHERE id = ?";Connection conn = null;PreparedStatement pstmt = null;try {conn = DBUtils.getConnection();pstmt = conn.prepareStatement(sql);pstmt.setInt(1, id);int rowsAffected = pstmt.executeUpdate();return rowsAffected > 0;} catch (SQLException e) {e.printStackTrace();return false;} finally {DBUtils.closeResources(pstmt, conn);}}
}
核心JDBC操作解析
- PreparedStatement的优势:
- 防SQL注入:通过占位符(?)绑定参数,而非拼接SQL字符串(如
"INSERT INTO users VALUES ('" + username + "')"
),避免恶意用户输入' OR 1=1 --
等注入语句; - 预编译优化:SQL语句仅编译一次,多次执行时可复用,提高效率。
- 防SQL注入:通过占位符(?)绑定参数,而非拼接SQL字符串(如
- ResultSet处理:
rs.next()
移动游标至下一条记录,rs.getInt("id")
按字段名获取值(比按索引rs.getInt(1)
更易维护,字段顺序变化不影响)。 - 异常处理:捕获
SQLException
后打印堆栈信息便于调试,同时针对常见错误(如1062:唯一约束冲突)可添加特殊处理逻辑。
控制器层(Servlet):处理请求与协调逻辑
Servlet是Java Web的“控制器”核心,负责接收客户端请求、调用DAO/Service层处理业务、并将结果转发至JSP视图。其生命周期由Servlet容器(如Tomcat)管理,分为三个阶段:
- 初始化(init()):Servlet首次被访问时执行,用于初始化资源(如创建DAO实例);
- 服务(service()):每次接收请求时执行,根据请求方式(GET/POST)调用
doGet()
或doPost()
; - 销毁(destroy()):Servlet容器关闭时执行,用于释放资源(如关闭连接池)。
1. 核心Servlet开发:UserServlet
UserServlet
负责处理所有用户相关的请求(如“查看用户列表”“新增用户”“删除用户”),通过action
参数区分不同操作。
package com.yourapp.servlets;import com.yourapp.daos.UserDAO;
import com.yourapp.daos.impl.UserDAOImpl;
import com.yourapp.models.User;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;/*** 用户控制器Servlet:处理所有用户相关的HTTP请求* @WebServlet("/users"):映射URL路径,客户端通过http://localhost:8080/项目名/users访问*/
@WebServlet("/users")
public class UserServlet extends HttpServlet {// 依赖DAO层:通过接口编程,降低耦合(后续可替换为其他实现)private UserDAO userDAO;/*** 初始化方法:Servlet创建时执行,仅一次* 用于初始化DAO实例等资源*/@Overridepublic void init() throws ServletException {super.init();// 实例化DAO实现类(实际开发中可使用依赖注入框架如Spring管理)userDAO = new UserDAOImpl();}/*** 处理GET请求:通常用于查询操作(如查看列表、查看详情)*/@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 1. 设置请求/响应的字符编码,避免中文乱码request.setCharacterEncoding("UTF-8");response.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=UTF-8");// 2. 获取action参数,区分不同操作(如list、new、edit、delete)// 若未指定action,默认执行list(查看用户列表)String action = request.getParameter("action");if (action == null) {action = "list";}// 3. 根据action执行对应逻辑try {switch (action) {case "new": // 跳转至新增用户表单页showNewUserForm(request, response);break;case "edit": // 跳转至编辑用户表单页showEditUserForm(request, response);break;case "delete": // 删除用户deleteUser(request, response);break;case "list": // 查看用户列表(默认操作)default:listAllUsers(request, response);break;}} catch (Exception e) {// 捕获所有异常,转发至错误页面request.setAttribute("errorMsg", "操作失败:" + e.getMessage());request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response);}}/*** 处理POST请求:通常用于提交数据的操作(如新增、更新)* 此处直接调用doGet(),统一处理逻辑(也可单独实现)*/@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {doGet(request, response);}// ------------------------------ 具体操作方法 ------------------------------/*** 1. 查看所有用户列表* 流程:调用DAO查询所有用户 → 将用户列表设置为请求属性 → 转发至user-list.jsp*/private void listAllUsers(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 调用DAO查询所有用户List<User> userList = userDAO.getAllUsers();// 将用户列表设置为请求属性(key="userList",JSP中通过${userList}获取)request.setAttribute("userList", userList);// 转发请求至JSP视图(路径为/WEB-INF/views/user-list.jsp)// 转发:服务器内部跳转,URL不变,可共享request属性request.getRequestDispatcher("/WEB-INF/views/user-list.jsp").forward(request, response);}/*** 2. 跳转至新增用户表单页* 流程:直接转发至user-form.jsp(无需查询数据)*/private void showNewUserForm(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 转发至新增表单页request.getRequestDispatcher("/WEB-INF/views/user-form.jsp").forward(request, response);}/*** 3. 跳转至编辑用户表单页* 流程:获取用户ID → 调用DAO查询用户 → 将用户对象设置为请求属性 → 转发至user-form.jsp*/private void showEditUserForm(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 获取URL中的id参数(如/users?action=edit&id=1)String idStr = request.getParameter("id");if (idStr == null || idStr.isEmpty()) {throw new IllegalArgumentException("用户ID不能为空!");}int userId = Integer.parseInt(idStr); // 转换为int类型// 调用DAO根据ID查询用户User user = userDAO.getUserById(userId);if (user == null) {throw new IllegalArgumentException("未找到ID为" + userId + "的用户!");}// 将用户对象设置为请求属性(JSP中用于回显表单数据)request.setAttribute("user", user);// 转发至表单页(与新增共用一个JSP,通过是否存在user对象区分新增/编辑)request.getRequestDispatcher("/WEB-INF/views/user-form.jsp").forward(request, response);}/*** 4. 新增/更新用户(根据是否有id参数区分)* 流程:获取表单参数 → 封装为User对象 → 调用DAO执行新增/更新 → 重定向至用户列表*/private void saveOrUpdateUser(HttpServletRequest request, HttpServletResponse response)throws IOException {// 获取表单参数(name属性对应的值)String idStr = request.getParameter("id"); // 编辑时存在,新增时为nullString username = request.getParameter("username");String email = request.getParameter("email");String password = request.getParameter("password");// 验证参数合法性(简单示例,实际开发需更完善)if (username == null || username.isEmpty() || email == null || email.isEmpty() || password == null || password.isEmpty()) {throw new IllegalArgumentException("用户名、邮箱、密码均不能为空!");}// 验证用户名是否已存在(新增时)if (idStr == null || idStr.isEmpty()) {User existingUser = userDAO.getUserByUsername(username);if (existingUser != null) {throw new IllegalArgumentException("用户名" + username + "已存在!");}}// 封装为User对象User user = new User();if (idStr != null && !idStr.isEmpty()) {// 编辑操作:设置iduser.setId(Integer.parseInt(idStr));}user.setUsername(username);user.setEmail(email);// 实际开发:此处需对密码进行哈希处理(如使用BCrypt),而非直接存储明文user.setPassword(password);// 调用DAO执行新增或更新boolean success;if (user.getId() == 0) {// 新增:id为0表示未设置(User默认id为0)success = userDAO.createUser(user);} else {// 更新:id不为0表示编辑success = userDAO.updateUser(user);}if (!success) {throw new RuntimeException("保存用户失败!");}// 重定向至用户列表页(http://localhost:8080/项目名/users?action=list)// 重定向:客户端跳转,URL改变,避免表单重复提交(刷新页面时不会重新执行POST)response.sendRedirect(request.getContextPath() + "/users?action=list");}/*** 5. 删除用户* 流程:获取用户ID → 调用DAO删除 → 重定向至用户列表*/private void deleteUser(HttpServletRequest request, HttpServletResponse response)throws IOException {// 获取id参数String idStr = request.getParameter("id");if (idStr == null || idStr.isEmpty()) {throw new IllegalArgumentException("用户ID不能为空!");}int userId = Integer.parseInt(idStr);// 调用DAO删除用户boolean success = userDAO.deleteUserById(userId);if (!success) {throw new RuntimeException("删除用户失败!");}// 重定向至用户列表response.sendRedirect(request.getContextPath() + "/users?action=list");}
}
2. Servlet核心知识点解析
(1)URL映射:@WebServlet注解
Servlet 3.0+支持注解配置,无需在web.xml
中手动映射。@WebServlet("/users")
表示:
- 客户端可通过
http://localhost:8080/项目上下文路径/users
访问此Servlet; - 上下文路径(Context Path):即项目部署时的名称(如Tomcat中部署为
user-management.war
,则上下文路径为/user-management
)。
(2)请求处理:doGet()与doPost()
- doGet():用于“查询型”操作(如查看列表、编辑页跳转),参数通过URL传递(如
/users?action=edit&id=1
),数据暴露在URL中,有长度限制; - doPost():用于“提交型”操作(如新增、更新),参数通过请求体传递,数据不暴露,无长度限制,更安全。
- 此处将
doPost()
转发至doGet()
,统一处理逻辑,简化代码(也可根据需要单独实现)。
(3)请求转发与重定向的区别
这是Servlet开发中的核心概念,选择错误可能导致功能异常(如表单重复提交):
特性 | 请求转发(Forward) | 重定向(Redirect) |
---|---|---|
跳转位置 | 服务器内部跳转 | 客户端跳转(服务器返回302状态码,客户端重新发起请求) |
URL地址 | 不变 | 改变 |
请求对象 | 共享同一个HttpServletRequest对象 | 不同请求,不共享 |
适用场景 | 跳转至视图(JSP)、传递请求属性 | 表单提交后跳转、避免重复提交 |
代码示例 | request.getRequestDispatcher(path).forward(...) | response.sendRedirect(url) |
示例场景:
- 查看用户列表后跳转至
user-list.jsp
:用转发(需传递userList
属性); - 新增用户后跳转至列表页:用重定向(避免刷新页面时重新提交表单)。
(4)中文乱码处理
客户端提交的中文参数若不处理,会出现乱码。解决方案:
- 在
doGet()
/doPost()
开头设置请求编码:request.setCharacterEncoding("UTF-8")
; - 设置响应编码:
response.setCharacterEncoding("UTF-8")
; - 设置响应内容类型:
response.setContentType("text/html;charset=UTF-8")
(告诉浏览器用UTF-8解析)。
视图层(JSP):渲染动态页面
JSP是Servlet的“模板化”扩展,允许在HTML中嵌入Java代码、EL表达式和JSTL标签,最终由Servlet容器翻译为Servlet并执行,生成动态HTML响应。
视图层的核心目标是“展示数据”,应避免包含复杂业务逻辑(逻辑应放在Servlet/Service层)。我们将开发两个核心JSP页面:user-list.jsp
(用户列表)和user-form.jsp
(新增/编辑表单)。
1. 依赖准备:引入JSTL标签库
JSTL(JavaServer Pages Standard Tag Library)提供了一套标准标签,用于替代JSP中的Java脚本(如<% %>
),使页面更简洁易维护。需在pom.xml
中添加依赖(非Maven项目需手动下载JAR包放入/WEB-INF/lib
):
<!-- JSTL核心标签库 -->
<dependency><groupId>javax.servlet.jsp.jstl</groupId><artifactId>jstl-api</artifactId><version>1.2</version>
</dependency>
<dependency><groupId>org.glassfish.web</groupId><artifactId>jstl-impl</artifactId><version>1.2</version><scope>runtime</scope>
</dependency>
2. 用户列表页:user-list.jsp
展示所有用户信息,提供“新增”“编辑”“删除”操作入口。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%-- 引入JSTL核心标签库(prefix="c"表示使用<c:xxx>标签) --%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>用户管理系统 - 用户列表</title><!-- 引入CSS样式(${pageContext.request.contextPath}获取上下文路径) --><link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">
</head>
<body><div class="container"><h1>用户管理</h1><!-- 新增用户按钮:跳转至新增表单页(/users?action=new) --><a href="${pageContext.request.contextPath}/users?action=new" class="btn-add">添加新用户</a><!-- 错误提示:若Servlet传递了errorMsg属性,显示错误信息 --><c:if test="${not empty errorMsg}"><div class="error">${errorMsg}</div></c:if><!-- 用户列表表格 --><table class="user-table"><thead><tr><th>ID</th><th>用户名</th><th>邮箱</th><th>注册时间</th><th>操作</th></tr></thead><tbody><%-- 循环遍历userList(Servlet传递的请求属性) --%><c:forEach var="user" items="${userList}"><tr><!-- EL表达式:${user.id}获取User对象的id属性(调用getter方法) --><td>${user.id}</td><td>${user.username}</td><td>${user.email}</td><td>${user.createdAt}</td><td class="action-buttons"><!-- 编辑按钮:传递id参数(/users?action=edit&id=${user.id}) --><a href="${pageContext.request.contextPath}/users?action=edit&id=${user.id}" class="btn-edit">编辑</a><!-- 删除按钮:点击时弹出确认框,传递id参数 --><a href="${pageContext.request.contextPath}/users?action=delete&id=${user.id}" class="btn-delete" onclick="return confirm('确定要删除用户【${user.username}】吗?')">删除</a></td></tr></c:forEach><%-- 若用户列表为空,显示提示信息 --%><c:if test="${empty userList}"><tr><td colspan="5" class="empty">暂无用户数据</td></tr></c:if></tbody></table></div>
</body>
</html>
3. 新增/编辑表单页:user-form.jsp
共用一个表单页,通过是否存在user
对象区分“新增”和“编辑”(新增时user
为null,编辑时user
为待编辑的用户对象)。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title><%-- 动态标题:编辑时显示“编辑用户”,新增时显示“添加新用户” --%><c:if test="${not empty user}">编辑用户</c:if><c:if test="${empty user}">添加新用户</c:if></title><link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">
</head>
<body><div class="container"><h1><c:if test="${not empty user}">编辑用户</c:if><c:if test="${empty user}">添加新用户</c:if></h1><!-- 错误提示 --><c:if test="${not empty errorMsg}"><div class="error">${errorMsg}</div></c:if><%-- 表单:新增时提交至action=create,编辑时提交至action=update --%><form action="${pageContext.request.contextPath}/users?action=${empty user ? 'create' : 'update'}" method="post" class="user-form"><%-- 编辑时需要传递id参数(隐藏字段) --%><c:if test="${not empty user}"><input type="hidden" name="id" value="${user.id}"></c:if><div class="form-group"><label for="username">用户名:</label><%-- 表单回显:编辑时设置value为user.username,新增时为空 --%><input type="text" id="username" name="username" value="${not empty user ? user.username : ''}" required maxlength="50"></div><div class="form-group"><label for="email">邮箱:</label><input type="email" id="email" name="email" value="${not empty user ? user.email : ''}" required maxlength="100"></div><div class="form-group"><label for="password">密码:</label><input type="password" id="password" name="password" value="${not empty user ? user.password : ''}" required maxlength="255"><small>提示:密码请至少包含6个字符</small></div><div class="form-actions"><button type="submit" class="btn-submit">提交</button><%-- 取消按钮:返回用户列表页 --%><a href="${pageContext.request.contextPath}/users?action=list" class="btn-cancel">取消</a></div></form></div>
</body>
</html>
4. JSP核心知识点解析
(1)EL表达式(${})
EL(Expression Language)用于简化JSP中数据的访问,核心语法:
${user.id}
:等价于((User)request.getAttribute("user")).getId()
,自动从pageContext
、request
、session
、application
作用域中查找属性;${empty userList}
:判断userList
是否为null或空集合;${not empty user ? user.username : ''}
:三元运算符,编辑时回显用户名,新增时为空。
(2)JSTL标签
<c:forEach>
:循环遍历集合(如userList
),var="user"
表示当前循环的元素,items="${userList}"
表示要遍历的集合;<c:if>
:条件判断,test="${not empty user}"
表示当user
不为空时执行标签内的内容。
(3)路径处理:${pageContext.request.contextPath}
获取项目的上下文路径(如/user-management
),避免硬编码路径导致部署后无法访问。例如:
- 正确:
href="${pageContext.request.contextPath}/css/style.css"
; - 错误:
href="/css/style.css"
(部署上下文路径变化时失效)。
(4)表单回显
编辑用户时,需将数据库中的用户信息填充到表单中(回显),通过value="${user.username}"
实现,简化了代码(无需在JSP中写Java脚本)。
数据流完整过程
理解请求从发起至响应的完整流程,是掌握JSP+Servlet+数据库整合的关键。以下以“新增用户”为例,通过序列图和步骤说明展示数据流转:
1. 数据流序列图
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 浏览器 │ │Servlet │ │ DAO │ │ 数据库 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘│ │ │ ││ 1. 提交表单 │ │ ││ POST /users?action=create │ ││───────────────────────────>│ ││ │ │ ││ │ 2. 解析参数 │ ││ │ (username/email/password) ││ │ │ ││ │ 3. 封装User对象 ││ │ │ ││ │ 4. 调用DAO.createUser() ││ │─────────────>│ ││ │ │ ││ │ │ 5. 执行SQL插入 ││ │ │─────────────>││ │ │ ││ │ │ 6. 返回受影响行数││ │ │<─────────────││ │ │ ││ │ 7. 重定向至列表页 ││<───────────────────────────│ ││ │ │ ││ 8. 发起列表请求 │ ││ GET /users?action=list │ ││───────────────────────────>│ ││ │ │ ││ │ 9. 调用DAO.getAllUsers() ││ │─────────────>│ ││ │ │ ││ │ │ 10. 执行SQL查询││ │ │─────────────>││ │ │ ││ │ │ 11. 返回结果集 ││ │ │<─────────────││ │ │ ││ │ 12. 封装userList属性 ││ │ │ ││ │ 13. 转发至user-list.jsp ││ │─────────────>│ ││ │ │ ││ │ 14. 生成HTML响应 ││ │<─────────────│ ││ │ │ ││ 15. 返回HTML页面 │ ││<───────────────────────────│ ││ │ │ │
2. 新增用户核心步骤详解
- 用户提交表单:在
user-form.jsp
中填写用户名、邮箱、密码,点击“提交”,浏览器发送POST /users?action=create
请求; - Servlet接收请求:
UserServlet.doPost()
被调用,解析表单参数(username
、email
、password
); - 参数验证与封装:验证参数非空,检查用户名是否已存在,封装为
User
对象; - DAO执行插入:
UserServlet
调用UserDAO.createUser()
,DAO通过JDBC执行INSERT
语句; - 重定向至列表页:插入成功后,通过
response.sendRedirect()
引导浏览器发起GET /users?action=list
请求; - 查询并展示列表:
UserServlet
调用UserDAO.getAllUsers()
获取所有用户,将userList
设置为请求属性,转发至user-list.jsp
; - JSP渲染页面:
user-list.jsp
通过JSTL和EL表达式遍历userList
,生成包含所有用户的HTML页面,返回给浏览器。
最佳实践与进阶优化
基础实现完成后,需从安全、性能、可维护性三个维度进行优化,使应用达到生产级标准。
1. 安全优化(重中之重)
(1)密码哈希存储(避免明文)
明文存储密码是严重的安全隐患,需使用BCrypt等算法对密码进行哈希处理(哈希是单向过程,无法反向破解)。
实现步骤:
- 添加BCrypt依赖:
<dependency><groupId>org.mindrot</groupId><artifactId>jbcrypt</artifactId><version>0.4</version> </dependency>
- 新增/更新用户时哈希密码:
// 替换UserServlet.saveOrUpdateUser()中的密码设置 // 生成盐值(自动包含在哈希结果中,无需单独存储) String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt()); user.setPassword(hashedPassword);
- 登录验证时匹配密码(扩展功能):
// 从数据库查询用户 User user = userDAO.getUserByUsername(username); // 验证密码:BCrypt.checkpw(明文密码, 哈希密码) if (user != null && BCrypt.checkpw(inputPassword, user.getPassword())) {// 登录成功 }
(2)防止SQL注入
已通过PreparedStatement
占位符实现基本防护,还需注意:
- 避免使用动态SQL拼接(如
"SELECT * FROM users WHERE username = '" + username + "'"
); - 对用户输入进行过滤(如限制用户名只能包含字母、数字、下划线)。
(3)防止CSRF攻击
CSRF(跨站请求伪造)攻击者诱导用户在已登录状态下执行非本意的操作(如删除用户)。防护方案:
- 在Session中生成随机CSRF Token;
- 在表单中添加隐藏字段
<input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}">
; - 在Servlet中验证请求中的Token与Session中的Token是否一致。
2. 性能优化
(1)使用数据库连接池
频繁创建/关闭数据库连接会消耗大量资源,连接池通过预先创建一定数量的连接并复用,大幅提升性能。推荐使用HikariCP(目前性能最优的连接池)。
实现步骤:
- 添加HikariCP依赖:
<dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId><version>5.0.1</version> </dependency>
- 修改
DBUtils
为连接池实现:public class DBUtils {// 连接池实例(单例)private static final HikariDataSource DATA_SOURCE;static {// 配置连接池HikariConfig config = new HikariConfig();config.setJdbcUrl("jdbc:mysql://localhost:3306/user_management?useSSL=false&serverTimezone=UTC");config.setUsername("root");config.setPassword("123456");config.setDriverClassName("com.mysql.cj.jdbc.Driver");config.setMaximumPoolSize(10); // 最大连接数config.setMinimumIdle(2); // 最小空闲连接数config.setConnectionTimeout(30000); // 连接超时时间(30秒)// 初始化连接池DATA_SOURCE = new HikariDataSource(config);}// 获取连接(从连接池获取,而非新建)public static Connection getConnection() throws SQLException {return DATA_SOURCE.getConnection();} }
(2)分页查询(避免大量数据加载)
当用户数量过多时,getAllUsers()
会加载所有数据,导致内存溢出和页面卡顿。需实现分页查询:
- 修改
UserDAO
接口:// 分页查询用户 List<User> getUsersByPage(int pageNum, int pageSize); // 查询总用户数(用于计算总页数) int getTotalUserCount();
- 实现DAO方法:
@Override public List<User> getUsersByPage(int pageNum, int pageSize) {// MySQL分页SQL:LIMIT 起始索引, 每页条数(起始索引 = (页码-1)*每页条数)String sql = "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC LIMIT ?, ?";List<User> userList = new ArrayList<>();Connection conn = null;PreparedStatement pstmt = null;ResultSet rs = null;try {conn = DBUtils.getConnection();pstmt = conn.prepareStatement(sql);pstmt.setInt(1, (pageNum - 1) * pageSize); // 起始索引pstmt.setInt(2, pageSize); // 每页条数rs = pstmt.executeQuery();while (rs.next()) {User user = new User();user.setId(rs.getInt("id"));user.setUsername(rs.getString("username"));user.setEmail(rs.getString("email"));user.setCreatedAt(rs.getTimestamp("created_at"));userList.add(user);}} catch (SQLException e) {e.printStackTrace();} finally {DBUtils.closeResources(rs, pstmt, conn);}return userList; }@Override public int getTotalUserCount() {String sql = "SELECT COUNT(*) FROM users";try (Connection conn = DBUtils.getConnection();PreparedStatement pstmt = conn.prepareStatement(sql);ResultSet rs = pstmt.executeQuery()) {if (rs.next()) {return rs.getInt(1); // 返回总条数}} catch (SQLException e) {e.printStackTrace();}return 0; }
- 在Servlet和JSP中添加分页控件(如“上一页”“下一页”按钮)。
3. 代码可维护性优化
(1)引入Service层
当业务逻辑复杂时(如“注册用户需发送验证邮件”“更新用户时同步修改关联表”),可新增Service层介于Servlet与DAO之间,集中处理业务逻辑:
- 定义Service接口(
UserService
):public interface UserService {boolean registerUser(User user); // 注册用户(包含用户名验证、密码哈希、邮件发送)List<User> getUsersByPage(int pageNum, int pageSize);// 其他业务方法... }
- 实现Service类(
UserServiceImpl
):public class UserServiceImpl implements UserService {private UserDAO userDAO = new UserDAOImpl();@Overridepublic boolean registerUser(User user) {// 1. 验证用户名是否存在if (userDAO.getUserByUsername(user.getUsername()) != null) {return false;}// 2. 哈希密码String hashedPassword = BCrypt.hashpw(user.getPassword(), BCrypt.gensalt());user.setPassword(hashedPassword);// 3. 调用DAO新增用户return userDAO.createUser(user);// 4. 发送验证邮件(扩展功能)// sendVerificationEmail(user.getEmail());}// 其他方法实现... }
- Servlet调用Service层:
// UserServlet中替换DAO为Service private UserService userService = new UserServiceImpl();private void saveOrUpdateUser(HttpServletRequest request, HttpServletResponse response) throws IOException {// ... 参数解析 ...// 调用Service注册用户boolean success = userService.registerUser(user);// ... 后续逻辑 ... }
(2)配置文件外部化
将数据库连接参数、邮件配置等硬编码信息提取到配置文件(如/resources/db.properties
),便于修改:
- 创建
db.properties
:db.url=jdbc:mysql://localhost:3306/user_management?useSSL=false&serverTimezone=UTC db.username=root db.password=123456 db.driver=com.mysql.cj.jdbc.Driver
- 工具类读取配置文件:
public class PropertiesUtils {private static Properties props;static {props = new Properties();try {// 读取配置文件InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("db.properties");props.load(in);} catch (IOException e) {throw new RuntimeException("加载配置文件失败!", e);}}public static String getProperty(String key) {return props.getProperty(key);} }
- DBUtils使用配置文件参数:
config.setJdbcUrl(PropertiesUtils.getProperty("db.url")); config.setUsername(PropertiesUtils.getProperty("db.username")); config.setPassword(PropertiesUtils.getProperty("db.password"));
总结
本文通过“用户管理系统”实战案例,完整讲解了JSP+Servlet+数据库的整合开发流程,核心要点可归纳为:
1. 架构分层清晰
- 视图层(JSP):专注于页面渲染,通过EL和JSTL减少Java代码;
- 控制器层(Servlet):接收请求、协调逻辑、转发/重定向,不处理具体业务;
- 业务层(Service,可选):封装复杂业务逻辑,解耦Servlet与DAO;
- 数据访问层(DAO):抽象数据库操作,屏蔽JDBC细节;
- 模型层(POJO):封装数据,作为各层间的传递载体。
2. 核心技术点
- Servlet:URL映射、请求处理(doGet/doPost)、转发与重定向;
- JSP:EL表达式、JSTL标签、表单回显;
- JDBC:PreparedStatement防注入、连接池优化;
- 安全:密码哈希、CSRF防护;
- 性能:分页查询、连接池复用。
3. 进阶方向
掌握基础后,可进一步学习更成熟的框架:
- Spring MVC:替代Servlet的更强大的控制器框架,支持依赖注入、拦截器等;
- MyBatis/Hibernate:替代JDBC的ORM框架,简化数据库操作;
- Spring Boot:整合Spring MVC、MyBatis等框架,实现零配置快速开发。
JSP与Servlet是Java Web开发的基础,理解其核心思想和整合逻辑,能为学习更高阶的框架打下坚实基础。实际开发中,需结合业务场景灵活运用分层架构,并始终将安全和性能作为核心考量。