JSP与Servlet整合数据库开发:构建Java Web应用的全栈指南

发布于:2025-09-05 ⋅ 阅读:(28) ⋅ 点赞:(0)

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   │
                               │ (视图)  │
                               └─────────┘

各组件核心作用解析

  1. 客户端(浏览器):用户交互入口,负责发送HTTP请求(如GET/POST),并接收服务器返回的HTML/CSS/JS响应进行渲染。
  2. Servlet(控制器)
    • 接收客户端请求,解析请求参数(如表单数据、URL参数);
    • 调用DAO层执行数据库操作(如查询用户、新增数据);
    • 将处理结果封装为“请求属性”(Request Attribute),转发至JSP视图;
    • 或通过重定向(Redirect)引导客户端跳转至其他页面(如新增用户后跳转至列表页)。
  3. JSP(视图)
    • 以HTML为基础,通过EL表达式(${}) 读取Servlet传递的请求属性;
    • 借助JSTL标签库(如<c:forEach>)实现循环、条件判断等动态逻辑;
    • 最终生成完整的HTML页面,通过Servlet返回给客户端。
  4. 数据库:持久化存储应用数据(如用户信息),通过JDBC与Servlet层交互。
  5. 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 JDKOpenJDK);
  • 配置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()方法,便于调试时打印用户信息
    @Override
    public 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. 新增用户
    @Override
    public 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. 查询所有用户
    @Override
    public List<User> getAllUsers() {
        String sql = "SELECT id, username, email, password, created_at FROM users ORDER BY created_at DESC";
        List<User> userList = new ArrayList<>(); // 初始化空列表,避免返回null
        Connection 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查询用户
    @Override
    public 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. 根据用户名查询用户
    @Override
    public 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. 更新用户信息
    @Override
    public 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删除用户
    @Override
    public 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的优势
    1. 防SQL注入:通过占位符(?)绑定参数,而非拼接SQL字符串(如"INSERT INTO users VALUES ('" + username + "')"),避免恶意用户输入' OR 1=1 --等注入语句;
    2. 预编译优化:SQL语句仅编译一次,多次执行时可复用,提高效率。
  • ResultSet处理rs.next()移动游标至下一条记录,rs.getInt("id")按字段名获取值(比按索引rs.getInt(1)更易维护,字段顺序变化不影响)。
  • 异常处理:捕获SQLException后打印堆栈信息便于调试,同时针对常见错误(如1062:唯一约束冲突)可添加特殊处理逻辑。

控制器层(Servlet):处理请求与协调逻辑

Servlet是Java Web的“控制器”核心,负责接收客户端请求、调用DAO/Service层处理业务、并将结果转发至JSP视图。其生命周期由Servlet容器(如Tomcat)管理,分为三个阶段:

  1. 初始化(init()):Servlet首次被访问时执行,用于初始化资源(如创建DAO实例);
  2. 服务(service()):每次接收请求时执行,根据请求方式(GET/POST)调用doGet()doPost()
  3. 销毁(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实例等资源
     */
    @Override
    public void init() throws ServletException {
        super.init();
        // 实例化DAO实现类(实际开发中可使用依赖注入框架如Spring管理)
        userDAO = new UserDAOImpl();
    }

    /**
     * 处理GET请求:通常用于查询操作(如查看列表、查看详情)
     */
    @Override
    protected 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(),统一处理逻辑(也可单独实现)
     */
    @Override
    protected 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"); // 编辑时存在,新增时为null
        String 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()) {
            // 编辑操作:设置id
            user.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(),自动从pageContextrequestsessionapplication作用域中查找属性;
  • ${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. 新增用户核心步骤详解

  1. 用户提交表单:在user-form.jsp中填写用户名、邮箱、密码,点击“提交”,浏览器发送POST /users?action=create请求;
  2. Servlet接收请求UserServlet.doPost()被调用,解析表单参数(usernameemailpassword);
  3. 参数验证与封装:验证参数非空,检查用户名是否已存在,封装为User对象;
  4. DAO执行插入UserServlet调用UserDAO.createUser(),DAO通过JDBC执行INSERT语句;
  5. 重定向至列表页:插入成功后,通过response.sendRedirect()引导浏览器发起GET /users?action=list请求;
  6. 查询并展示列表UserServlet调用UserDAO.getAllUsers()获取所有用户,将userList设置为请求属性,转发至user-list.jsp
  7. JSP渲染页面user-list.jsp通过JSTL和EL表达式遍历userList,生成包含所有用户的HTML页面,返回给浏览器。

最佳实践与进阶优化

基础实现完成后,需从安全性能可维护性三个维度进行优化,使应用达到生产级标准。

1. 安全优化(重中之重)

(1)密码哈希存储(避免明文)

明文存储密码是严重的安全隐患,需使用BCrypt等算法对密码进行哈希处理(哈希是单向过程,无法反向破解)。

实现步骤

  1. 添加BCrypt依赖:
    <dependency>
        <groupId>org.mindrot</groupId>
        <artifactId>jbcrypt</artifactId>
        <version>0.4</version>
    </dependency>
    
  2. 新增/更新用户时哈希密码:
    // 替换UserServlet.saveOrUpdateUser()中的密码设置
    // 生成盐值(自动包含在哈希结果中,无需单独存储)
    String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt());
    user.setPassword(hashedPassword);
    
  3. 登录验证时匹配密码(扩展功能):
    // 从数据库查询用户
    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(跨站请求伪造)攻击者诱导用户在已登录状态下执行非本意的操作(如删除用户)。防护方案:

  1. 在Session中生成随机CSRF Token;
  2. 在表单中添加隐藏字段<input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}">
  3. 在Servlet中验证请求中的Token与Session中的Token是否一致。

2. 性能优化

(1)使用数据库连接池

频繁创建/关闭数据库连接会消耗大量资源,连接池通过预先创建一定数量的连接并复用,大幅提升性能。推荐使用HikariCP(目前性能最优的连接池)。

实现步骤

  1. 添加HikariCP依赖:
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version>
    </dependency>
    
  2. 修改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()会加载所有数据,导致内存溢出和页面卡顿。需实现分页查询:

  1. 修改UserDAO接口:
    // 分页查询用户
    List<User> getUsersByPage(int pageNum, int pageSize);
    // 查询总用户数(用于计算总页数)
    int getTotalUserCount();
    
  2. 实现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;
    }
    
  3. 在Servlet和JSP中添加分页控件(如“上一页”“下一页”按钮)。

3. 代码可维护性优化

(1)引入Service层

当业务逻辑复杂时(如“注册用户需发送验证邮件”“更新用户时同步修改关联表”),可新增Service层介于Servlet与DAO之间,集中处理业务逻辑:

  1. 定义Service接口(UserService):
    public interface UserService {
        boolean registerUser(User user); // 注册用户(包含用户名验证、密码哈希、邮件发送)
        List<User> getUsersByPage(int pageNum, int pageSize);
        // 其他业务方法...
    }
    
  2. 实现Service类(UserServiceImpl):
    public class UserServiceImpl implements UserService {
        private UserDAO userDAO = new UserDAOImpl();
    
        @Override
        public 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());
        }
    
        // 其他方法实现...
    }
    
  3. 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),便于修改:

  1. 创建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
    
  2. 工具类读取配置文件:
    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);
        }
    }
    
  3. 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开发的基础,理解其核心思想和整合逻辑,能为学习更高阶的框架打下坚实基础。实际开发中,需结合业务场景灵活运用分层架构,并始终将安全和性能作为核心考量。


网站公告

今日签到

点亮在社区的每一天
去签到