当前位置: 首页 > news >正文

JavaWeb用户管理系统实战:从Servlet到JSP的MVC架构全解析

1. 项目概述与核心价值

最近在后台和社群里,不少刚学完Java基础、准备上手Web开发的朋友都在问同一个问题:有没有一个能串起所有核心知识点的实战项目?既能巩固Servlet、JSP、JDBC,又能提前接触点MVC分层的思想,最好还能看到真实的业务逻辑流转。说实话,我当年自学的时候也卡在这个阶段,理论学了一堆,一打开IDE就不知道从哪下手。今天要拆解的这个“JavaWeb用户管理系统”,就是专门为这个阶段设计的“毕业设计级”练手项目。它麻雀虽小,五脏俱全,几乎涵盖了JavaWeb初级阶段你需要掌握的所有技术栈。

这个系统的核心目标非常明确:对用户信息进行增删改查(CRUD)。别小看这四个操作,一个完整的Web应用,无论前台多么花哨,后台都离不开对数据的基本管理。通过实现它,你能亲手把Servlet如何接收请求、JSP如何展示页面、JavaBean如何封装数据、JDBC如何操作数据库这一整套流程跑通。更重要的是,我会带着你一行行读源码,不仅告诉你怎么写,更重点讲清楚为什么这么写——比如为什么要把数据库连接信息单独放到配置文件里?为什么查询用户列表后要先把结果集转成List再传给JSP?这些设计背后的考量,才是从“会敲代码”到“懂开发”的关键跨越。

项目采用经典的JSP + Servlet + JavaBean模式,数据库选用最普遍的MySQL。这套组合是理解后续任何高级框架(如Spring MVC)的基石。我会假设你已经有Java基础和简单的SQL知识,但即使你刚入门,跟着详细的步骤和源码讲解,也能一步步搭起来。接下来,我们就从最核心的设计思路开始,把这个项目彻底拆开揉碎讲明白。

2. 系统整体设计与架构拆解

2.1 技术选型与架构模式解析

为什么选择JSP+Servlet+JavaBean,而不是直接用Spring Boot?对于初学者而言,过早接触高度封装的框架,就像还没学会走路就先坐上了汽车,虽然快,但会错过对底层HTTP协议、请求-响应生命周期、数据库连接池等核心机制的理解。JSP+Servlet+JavaBean模式,通常被称为Model 1向Model 2的过渡形态,是理解MVC(Model-View-Controller)思想的绝佳起点。

在这个项目中,我们清晰地划分了三层:

  1. 视图层(View):由JSP页面担任。负责数据的展示和用户交互表单的呈现。例如,userList.jsp用来展示所有用户的表格,addUser.jsp是添加用户的表单页。JSP中应尽量避免写入大量的Java代码(Scriptlet),我们通过JSTL标签和EL表达式来简化展示逻辑。
  2. 控制层(Controller):由Servlet担任。它是系统的“交通枢纽”,所有来自浏览器的请求(如/addUser,/deleteUser)都由对应的Servlet接收。Servlet负责解析请求参数、调用业务逻辑、并将处理结果(数据或跳转指令)传递给视图层。例如,AddUserServlet会获取表单提交的用户名、年龄等信息,然后调用服务层的方法进行插入操作。
  3. 模型层(Model):由JavaBean和数据库操作类(DAO)共同构成。JavaBean(如User.java)是一个纯粹的实体类,它的属性(id, name, age等)与数据库表中的字段一一对应,并提供了标准的getter/setter方法。DAO(Data Access Object,数据访问对象,如UserDao.java)封装了所有对数据库的操作细节(增删改查),为Servlet提供干净的接口。

这种分层的好处是职责分离。JSP只关心怎么显示,Servlet只关心流程调度,JavaBean和DAO只关心数据和业务逻辑。当需要修改页面样式时,你几乎不用动Java代码;当数据库从MySQL换成Oracle时,你也只需要修改DAO层的实现。这种可维护性和可扩展性,是软件工程的核心追求。

2.2 数据库设计与核心表结构

任何管理系统,设计好数据库是第一步。我们设计一张user表,结构力求简单但典型:

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID,主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(50) NOT NULL COMMENT '密码(实际项目中应加密存储)', `email` varchar(100) DEFAULT NULL COMMENT '电子邮箱', `gender` varchar(10) DEFAULT NULL COMMENT '性别', `birthday` date DEFAULT NULL COMMENT '出生日期', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) COMMENT '用户名唯一' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

设计思路与细节考量:

  • 主键id:使用自增INT类型,这是最简单高效的主键策略。AUTO_INCREMENT让数据库自动生成唯一ID,避免程序层处理并发带来的重复问题。
  • 字段类型与长度usernamepassword用了varchar(50),这是一个折中的长度。对于邮箱,varchar(100)更保险。gendervarchar(10)是为了兼容“男”、“女”、“未知”等中文存储。
  • 唯一索引uk_username:在username上建立唯一约束,这是业务逻辑的强要求——系统不允许存在两个同名的用户。数据库层面的约束是最可靠、性能最好的防重复手段。
  • 注释:养成给表和字段写COMMENT的习惯。一个月后回头看,或者交接给同事时,这些注释价值千金。
  • 存储引擎与字符集InnoDB支持事务和外键(虽然本项目未用到),是MySQL的默认和推荐引擎。utf8mb4字符集能完整支持存储Emoji表情,避免未来出现乱码问题。

注意:在实际生产环境中,password字段绝不应该明文存储。这里为了简化,直接存了明文。正确的做法是使用BCrypt、PBKDF2等不可逆的哈希算法加盐(Salt)后存储。你可以在DAO层的插入和验证逻辑中加入加密步骤,这是一个非常重要的安全实践扩展点。

3. 核心模块源码逐行精讲

3.1 模型层(Model):User实体与UserDao数据访问对象

1. User.java (JavaBean/实体类)这个类的对象,就是系统中“用户”这个概念的载体。它严格遵循JavaBean规范。

package com.yourdomain.usermgr.model; import java.util.Date; public class User { private Integer id; private String username; private String password; private String email; private String gender; private Date birthday; private Date createTime; // 无参构造器是JavaBean的规范要求,很多框架(如反射、EL表达式)依赖它来创建对象。 public User() { } // 带参构造器,方便测试和快速创建对象。 public User(String username, String password, String email) { this.username = username; this.password = password; this.email = email; } // 以下是所有属性的getter和setter方法。 // IDE可以一键生成,但你要理解每个方法的作用。 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } // ... 其他getter/setter省略 // 重写toString()方法,方便调试时打印对象信息。 @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", email='" + email + '\'' + '}'; } }

关键点Date类型对应数据库的datetime/datecreateTime的命名采用了驼峰式,而数据库字段是create_time,这需要在JDBC操作时进行转换,我们稍后在DAO层会看到如何处理。

2. UserDao.java (数据访问对象)DAO层是连接Java对象和数据库表的桥梁。它封装了所有SQL语句,对外提供面向对象的接口。

package com.yourdomain.usermgr.dao; import com.yourdomain.usermgr.model.User; import com.yourdomain.usermgr.util.DBUtil; // 假设有一个封装好的数据库工具类 import java.sql.*; import java.util.ArrayList; import java.util.List; public class UserDao { // 1. 新增用户 public boolean addUser(User user) { Connection conn = null; PreparedStatement pstmt = null; String sql = "INSERT INTO user(username, password, email, gender, birthday) VALUES(?,?,?,?,?)"; try { conn = DBUtil.getConnection(); // 从工具类获取连接 pstmt = conn.prepareStatement(sql); // 设置参数,注意序号从1开始 pstmt.setString(1, user.getUsername()); pstmt.setString(2, user.getPassword()); pstmt.setString(3, user.getEmail()); pstmt.setString(4, user.getGender()); // 处理Date类型:将java.util.Date转换为java.sql.Date if (user.getBirthday() != null) { pstmt.setDate(5, new java.sql.Date(user.getBirthday().getTime())); } else { pstmt.setNull(5, Types.DATE); } int affectedRows = pstmt.executeUpdate(); // 执行插入,返回影响的行数 return affectedRows > 0; } catch (SQLException e) { e.printStackTrace(); // 实际项目应使用日志框架,如Log4j return false; } finally { DBUtil.close(pstmt, conn); // 在finally块中确保资源被释放 } } // 2. 根据ID删除用户 public boolean deleteUserById(int id) { // 实现类似,SQL: DELETE FROM user WHERE id=? } // 3. 更新用户信息 public boolean updateUser(User user) { // SQL: UPDATE user SET username=?, password=?, email=?, gender=?, birthday=? WHERE id=? // 注意:更新时要设置所有字段,或者动态构造SQL(更复杂)。 } // 4. 根据ID查询单个用户 public User getUserById(int id) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; User user = null; String sql = "SELECT * FROM user WHERE id=?"; try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); rs = pstmt.executeQuery(); if (rs.next()) { // 因为id是主键,最多只有一条记录,所以用if user = resultSetToUser(rs); // 将ResultSet的一行转换为User对象 } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, pstmt, conn); } return user; // 没查到就返回null } // 5. 查询所有用户(核心中的核心) public List<User> getAllUsers() { List<User> userList = new ArrayList<>(); Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; String sql = "SELECT * FROM user ORDER BY id DESC"; // 按ID倒序,让新加的显示在前面 try { conn = DBUtil.getConnection(); pstmt = conn.prepareStatement(sql); rs = pstmt.executeQuery(); while (rs.next()) { // 遍历结果集 User user = resultSetToUser(rs); userList.add(user); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, pstmt, conn); } return userList; // 即使为空,也返回一个空的List,而不是null,避免调用方空指针。 } // 6. 将ResultSet转换为User对象的辅助方法(重要!) private User resultSetToUser(ResultSet rs) throws SQLException { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); // 注意:这里取出的密码是明文 user.setEmail(rs.getString("email")); user.setGender(rs.getString("gender")); // 处理数据库中的date/datetime类型 Date birthday = rs.getDate("birthday"); if (birthday != null) { user.setBirthday(new Date(birthday.getTime())); // java.sql.Date -> java.util.Date } Timestamp createTime = rs.getTimestamp("create_time"); // 数据库字段是下划线 if (createTime != null) { user.setCreateTime(new Date(createTime.getTime())); } return user; } }

DAO层编码核心经验:

  1. 永远使用PreparedStatement:不要用Statement拼接SQL字符串!PreparedStatement能有效防止SQL注入攻击,并且数据库会对预编译的SQL有更好的执行效率。setString,setInt等方法会自动处理特殊字符。
  2. 资源释放务必放在finally块Connection,Statement,ResultSet都是宝贵的数据库资源,必须确保在任何情况下(正常执行或发生异常)都能被关闭。否则会导致连接泄漏,最终拖垮数据库。
  3. 查询结果集到对象的转换resultSetToUser这个方法体现了ORM(对象关系映射)的雏形。手动映射虽然繁琐,但让你深刻理解数据和对象的关系。这也是MyBatis等框架帮你做的事情。
  4. 返回空集合而非nullgetAllUsers()方法返回一个空的ArrayList,而不是null。这是《Effective Java》中提倡的最佳实践,可以避免调用方进行繁琐的null检查,减少NullPointerException

3.2 控制层(Controller):Servlet的请求分发与处理

Servlet是MVC中的C,它像路由器一样工作。我们以处理用户列表请求的Servlet为例。

package com.yourdomain.usermgr.servlet; import com.yourdomain.usermgr.dao.UserDao; import com.yourdomain.usermgr.model.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; @WebServlet("/user/list") // 使用注解配置Servlet的访问路径,替代web.xml配置 public class UserListServlet extends HttpServlet { private UserDao userDao = new UserDao(); // 持有DAO实例 @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 处理业务逻辑:调用DAO,获取所有用户数据 List<User> userList = userDao.getAllUsers(); // 2. 将数据存入请求域(request scope),传递给JSP request.setAttribute("userList", userList); // 3. 请求转发到展示页面 request.getRequestDispatcher("/userList.jsp").forward(request, response); } // 通常列表页是GET请求,所以我们只重写doGet方法。 // 如果是处理表单提交(如添加用户),则需要重写doPost方法。 }

Servlet编程关键点解析:

  • 注解@WebServlet:这是Servlet 3.0之后的标准配置方式,比在web.xml中配置<servlet><servlet-mapping>更简洁直观。/user/list是这个Servlet的URL映射。
  • 请求方法doGet/doPost:根据HTTP请求方法(GET用于获取数据,POST用于提交数据)来重写对应的方法。这是RESTful风格的雏形。
  • 请求域setAttribute:Servlet和JSP之间传递数据的主要方式。将userList对象以键值对的形式存放在HttpServletRequest对象中,转发到的JSP页面可以通过EL表达式${userList}直接获取。
  • 请求转发forward:服务器端跳转,地址栏URL不变(仍是/user/list),但内容由userList.jsp生成。与之相对的是重定向sendRedirect,它是客户端跳转,地址栏会变,且request域中的数据会丢失。记住一个原则:需要传递数据给下一个资源时用转发,其他情况(如提交后跳回列表页防止重复提交)常用重定向。

3.3 视图层(View):JSP页面与JSTL/EL表达式

JSP页面负责渲染。一个良好的JSP应该尽可能少地出现Java代码块(<% ... %>)。我们使用JSTL(JSP Standard Tag Library)和EL(Expression Language)来让页面更清晰。

userList.jsp (用户列表页)

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <title>用户管理系统 - 用户列表</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"> <style> .container { margin-top: 20px; } .table th, .table td { vertical-align: middle; } </style> </head> <body> <div class="container"> <h2 class="mb-4">用户列表 <a href="addUser.jsp" class="btn btn-success btn-sm float-right">添加新用户</a> </h2> <c:if test="${not empty message}"> <div class="alert alert-info">${message}</div> </c:if> <table class="table table-bordered table-hover"> <thead class="thead-light"> <tr> <th>ID</th> <th>用户名</th> <th>邮箱</th> <th>性别</th> <th>出生日期</th> <th>注册时间</th> <th>操作</th> </tr> </thead> <tbody> <%-- 使用JSTL的c:forEach遍历Servlet传过来的userList --%> <c:forEach items="${userList}" var="user" varStatus="status"> <tr> <td>${user.id}</td> <td>${user.username}</td> <td>${user.email}</td> <td> <c:choose> <c:when test="${user.gender == 'M'}">男</c:when> <c:when test="${user.gender == 'F'}">女</c:when> <c:otherwise>未知</c:otherwise> </c:choose> </td> <td>${user.birthday}</td> <%-- EL表达式会自动调用Date的toString() --%> <td>${user.createTime}</td> <td> <a href="editUser.jsp?id=${user.id}" class="btn btn-warning btn-sm">编辑</a> <a href="javascript:if(confirm('确定删除用户【${user.username}】吗?')) location.href='delete?id=${user.id}'" class="btn btn-danger btn-sm">删除</a> </td> </tr> </c:forEach> <%-- 如果列表为空 --%> <c:if test="${empty userList}"> <tr> <td colspan="7" class="text-center text-muted">暂无用户数据</td> </tr> </c:if> </tbody> </table> </div> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>

JSP页面核心技术点:

  1. <%@ taglib %>指令:引入JSTL核心标签库。prefix="c"表示在页面中使用c:作为前缀来调用这些标签。
  2. EL表达式${}:这是页面获取数据的灵魂。${userList}会自动从pageContext,request,session,application四个作用域中查找名为userList的属性。它比传统的<%= request.getAttribute("userList") %>简洁安全得多(避免了Scriptlet和空指针)。
  3. JSTL标签
    • <c:if>:条件判断。
    • <c:forEach>:循环遍历集合,var定义循环变量,items指定要遍历的集合。
    • <c:choose>/<c:when>/<c:otherwise>:多分支选择,相当于Java的switch-case
  4. 前端框架Bootstrap:引入了Bootstrap的CSS和JS,用极少的代码就能做出美观、响应式的表格和按钮。这对于后端开发者快速搭建可用的管理界面非常友好。
  5. 删除操作的确认:使用javascript:confirm()在真正发起删除请求前给用户一次确认机会,这是一个良好的用户体验设计。

4. 项目环境搭建与详细配置实操

4.1 开发环境与工具链准备

工欲善其事,必先利其器。一个顺手的开发环境能极大提升效率。

  1. JDK:确保安装JDK 8或以上版本。在命令行输入java -versionjavac -version验证。
  2. IDEIntelliJ IDEA Ultimate(社区版对JavaWeb支持有限)或Eclipse for Enterprise Java Developers。IDEA在智能提示、代码分析和部署方面体验更佳。我以IDEA为例。
  3. 应用服务器Apache Tomcat 9.x。这是最流行的Servlet容器,轻量、稳定、文档齐全。从官网下载ZIP包,解压即可。
  4. 数据库MySQL 5.7或8.0。同时需要一个图形化管理工具,如MySQL WorkbenchNavicatDBeaver,用于执行建表SQL和查看数据。

4.2 在IDEA中创建并配置Web项目

这是从零开始的关键一步,很多新手在这里卡住。

  1. 新建项目:打开IDEA,选择File -> New -> Project
  2. 选择项目类型:左侧选择Java Enterprise,右侧确保Web Application被勾选,并选择正确的Java EE version(如Java EE 78)。点击Next
  3. 配置Tomcat:在下一步,点击Application Server旁边的New...,选择你解压的Tomcat目录。IDEA会自动识别。
  4. 完成创建:输入项目名(如UserManagementSystem),选择项目位置,点击Finish

项目创建后,你会看到标准的目录结构:

UserManagementSystem ├── src │ └── (你的Java包和类放在这里,如 com.yourdomain.usermgr) ├── web │ ├── WEB-INF │ │ ├── web.xml (部署描述符,Servlet 3.0后可选) │ │ └── lib (存放项目依赖的Jar包,如数据库驱动) │ └── index.jsp (欢迎页面) └── External Libraries (IDEA管理的库)
  1. 添加依赖Jar包:这是最容易出错的地方。你需要手动下载以下Jar包,并放入web/WEB-INF/lib/目录下:
    • mysql-connector-java-8.0.xx.jar:MySQL数据库驱动。
    • jstl-1.2.jartaglibs-standard-impl-1.2.5.jar:JSTL标签库的实现。
    • (可选)druid-1.2.8.jar:如果你打算使用阿里巴巴的Druid连接池。

关键操作:在IDEA中,选中这些Jar包,右键选择Add as Library...,将它们添加到项目的类路径中。否则,代码编译会报错“找不到类”。

4.3 数据库连接工具类(DBUtil)实现

直接在每个DAO方法里写连接代码会导致大量重复,且资源管理混乱。我们需要一个工具类来集中管理数据库连接。

package com.yourdomain.usermgr.util; import java.io.IOException; import java.io.InputStream; import java.sql.*; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; private static String driver; // 静态代码块,在类加载时执行一次,用于读取配置文件并注册驱动 static { try { // 1. 读取配置文件 db.properties Properties props = new Properties(); // 使用类加载器读取资源文件,确保无论在IDE还是服务器上都能找到 InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("db.properties"); props.load(in); // 2. 获取配置信息 url = props.getProperty("jdbc.url"); user = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); driver = props.getProperty("jdbc.driver"); // 3. 注册驱动 (对于MySQL Connector/J 6.0+,这步可以省略,但写上更稳妥) Class.forName(driver); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); throw new ExceptionInInitializerError("数据库配置初始化失败"); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 重载的关闭方法,用于关闭Connection和Statement public static void close(Statement stmt, Connection conn) { close(null, stmt, conn); } // 通用的资源关闭方法,注意关闭顺序:ResultSet -> Statement -> Connection public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

对应的配置文件src/db.properties

# 数据库连接配置 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=your_password

工具类设计精讲:

  • 为什么用静态代码块?数据库连接参数只需要在程序启动时加载一次,放在静态代码块中最合适。
  • 为什么用Properties文件?将配置与代码分离。当数据库地址、密码变更时,无需重新编译Java代码,只需修改配置文件。这在部署到不同环境(开发、测试、生产)时至关重要。
  • Class.forName(driver)的作用:早期JDBC规范要求显式加载驱动类。对于MySQL Connector/J 6.0以上版本,由于SPI机制,这行代码可以省略。但保留它是个好习惯,能明确指明所使用的驱动,且兼容旧版本。
  • 关闭资源的顺序:必须按照ResultSet -> Statement -> Connection的逆序关闭。Connection是最底层的资源,如果先关闭它,由它产生的StatementResultSet可能无法正常关闭。
  • 异常处理:关闭资源时发生的SQLException通常不重要(比如连接早已超时断开),所以我们只打印日志,不向上抛出,避免掩盖主逻辑的异常。

5. 功能扩展与高级特性实现

一个基础CRUD做完后,可以尝试加入以下特性,让项目更接近实用。

5.1 用户登录与会话管理(Session)

管理系统必须有权限控制。我们增加一个登录功能。

  1. 创建登录Servlet (LoginServlet)
    @WebServlet("/login") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // 这里应该验证密码(实际是验证密码哈希值) User user = userDao.findUserByUsernameAndPassword(username, password); if (user != null) { // 登录成功,将用户信息存入Session HttpSession session = req.getSession(); session.setAttribute("loginUser", user); // 跳转到主页或用户列表页 resp.sendRedirect(req.getContextPath() + "/user/list"); } else { // 登录失败,返回错误信息 req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }
  2. 创建登录页面 (login.jsp):一个简单的表单,提交到/login
  3. 添加过滤器 (AuthFilter):保护所有需要登录才能访问的页面(如/user/*)。
    @WebFilter("/*") // 过滤所有请求 public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 放行登录相关、静态资源等请求 if (uri.endsWith("login.jsp") || uri.endsWith("login") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(request, response); return; } // 检查Session中是否有登录用户 HttpSession session = req.getSession(false); // 不创建新session if (session == null || session.getAttribute("loginUser") == null) { // 未登录,重定向到登录页 resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 已登录,放行 chain.doFilter(request, response); } }
    过滤器原理:它像一道安检门,在所有Servlet处理请求之前和之后执行。我们在doFilter方法中判断用户是否登录,未登录则拦截并跳转。

5.2 密码加密存储

明文存储密码是严重的安全漏洞。使用BCrypt算法是当前的最佳实践。

  1. 添加依赖:下载bcrypt-xxx.jar并放入lib目录。
  2. 创建密码工具类
    import org.mindrot.jbcrypt.BCrypt; public class PasswordUtil { // 生成加密后的密码 public static String hashPassword(String plainPassword) { return BCrypt.hashpw(plainPassword, BCrypt.gensalt()); } // 验证密码 public static boolean checkPassword(String plainPassword, String hashedPassword) { return BCrypt.checkpw(plainPassword, hashedPassword); } }
  3. 修改DAO层
    • addUser方法中,调用PasswordUtil.hashPassword(user.getPassword())得到密文,再存入数据库。
    • 在登录验证的findUserByUsernameAndPassword方法中,先根据用户名查出用户(包含密文密码),然后调用PasswordUtil.checkPassword(用户输入的明文, 数据库中的密文)进行验证。

5.3 分页查询功能

当用户数据成千上万时,一次性查询所有数据是不可接受的。必须实现分页。

  1. 修改DAO接口:增加一个方法List<User> getUsersByPage(int pageNo, int pageSize)
  2. 修改SQL:使用LIMIT子句。SELECT * FROM user ORDER BY id DESC LIMIT ?, ?。第一个?是偏移量(pageNo-1) * pageSize,第二个?pageSize
  3. 修改Servlet:接收前端传递的page参数(默认为1),调用DAO的分页方法。
  4. 修改JSP:在页面底部生成分页导航条,计算总页数,并生成类似[上一页] 1 2 3 ... [下一页]的链接,每个链接都指向自己并带上page参数。

6. 项目部署、调试与常见问题排查

6.1 将项目打包成WAR并部署到独立Tomcat

在IDEA中开发调试很方便,但最终项目需要部署到独立的服务器上。

  1. 打包:在IDEA中,点击Build -> Build Artifacts...,选择你的项目,然后Build。这会在out/artifacts/目录下生成一个YourProjectName.war文件。
  2. 部署:将war文件复制到独立Tomcat的webapps/目录下。
  3. 启动:运行Tomcat的bin/startup.bat(Windows) 或bin/startup.sh(Linux/Mac)。
  4. 访问:打开浏览器,访问http://localhost:8080/YourProjectName/

6.2 开发与调试中的高频问题及解决方案

以下是我在带新人过程中,他们最容易踩的坑:

问题现象可能原因排查步骤与解决方案
404错误(页面找不到)1. URL路径写错。
2. 项目未成功部署。
3.web.xml@WebServlet配置错误。
1. 检查浏览器地址栏URL,是否包含项目上下文路径(Context Path)。
2. 查看Tomcat日志logs/catalina.out,确认项目是否启动成功。
3. 检查Servlet的注解路径或web.xml映射是否正确。
500错误(服务器内部错误)1. Java代码运行时异常(空指针、类找不到等)。
2. JSP编译错误。
1.这是最重要的线索来源!立即查看Tomcat日志logs/localhost.yyyy-MM-dd.log或控制台输出的完整异常堆栈信息。
2. 根据堆栈信息定位到具体的Java文件和行号,逐行检查。
数据库连接失败1.db.properties文件未找到或路径错误。
2. 数据库服务未启动。
3. 连接参数(IP、端口、用户名、密码)错误。
4. 驱动Jar包未正确添加到类路径。
1. 确认db.properties文件在src目录下,且编译后会在WEB-INF/classes中。
2. 用命令行或图形工具测试是否能连上MySQL。
3. 检查DBUtil静态代码块中的日志,看配置是否加载成功。
4. 确认mysql-connector-java.jarWEB-INF/lib下,且被Add as Library
JSTL标签或EL表达式不生效1. 未引入JSTL的Jar包。
2. Jar包版本冲突或未添加到类路径。
3. 页面未声明taglib指令。
1. 确保jstl.jarstandard.jar(或对应的实现包)在lib目录下。
2. 检查IDEA的Project Structure -> Modules -> Dependencies,确认这些Jar包被包含。
3. 检查JSP文件头部的<%@ taglib %>指令的URI是否正确。
中文乱码1. 数据库、连接、页面三处字符集不统一。
2. Servlet未设置请求/响应编码。
终极解决方案(四步曲)
1.数据库/表:创建时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
2.连接字符串:在JDBC URL中加入?characterEncoding=utf8&useUnicode=true
3.Servlet:在doGet/doPost方法第一行加上request.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=UTF-8");
4.JSP页面:在文件顶部加上<%@ page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>
删除或更新后,页面数据没变化1. 数据库操作成功,但页面缓存了旧数据。
2. Servlet操作成功后,跳转方式错误(应使用重定向而非转发)。
1. 在浏览器中按Ctrl+F5强制刷新。
2.重要原则:对于POST请求(如添加、删除、更新),处理成功后,应该使用response.sendRedirect("list")重定向到列表页。这能防止用户刷新页面时重复提交表单(Post/Redirect/Get模式)。

6.3 性能与安全考量进阶

当这个项目跑通后,你可以思考如何让它变得更好:

  1. 数据库连接池:现在的DBUtil每次操作都新建连接,性能极差。应引入Druid或HikariCP连接池,在应用启动时创建一批连接,用时取出,用完归还。
  2. 日志框架:将代码中的System.out.printlne.printStackTrace()替换为Log4j2SLF4J,可以灵活控制日志级别和输出目的地。
  3. 输入验证:在Servlet接收参数后,必须进行验证(非空、长度、格式等),防止无效或恶意数据进入系统。这既是健壮性要求,也是安全防线(如防止XSS脚本注入)。
  4. 异常统一处理:可以编写一个全局的异常处理器(实现ServletErrorPage配置或使用过滤器),将未捕获的异常转化为友好的错误页面,而不是暴露堆栈信息给用户。
  5. 前端验证:在JSP中使用JavaScript进行初步验证(如必填项),可以提高用户体验,但切记,前端验证不可靠,后端必须进行二次验证

这个“用户管理系统”项目,就像一副完整的骨架。你现在已经学会了如何把每一块骨头(技术点)拼接起来。接下来要做的,就是在这个骨架上增加肌肉和皮肤(更多的业务功能、更好的UI、更优的架构)。当你能够不参考任何教程,独立从头到尾实现它,并且能清晰解释每一行代码的作用时,你的JavaWeb入门阶段,才算真正圆满毕业了。

http://www.jsqmd.com/news/1389372/

相关文章:

  • 从皮肤文件规范到工作流:网易我的世界自定义皮肤上传全指南
  • Rust构建PHP虚拟机:AI辅助的编译原理与系统编程实践
  • 从机器人大会到开发实践:AI+ROS2+仿真构建智能体应用全流程
  • 轻薄本本地部署GPU加速Spark:环境搭建与实战指南
  • 从输入上下文到智能体:掌握AI交互四大核心概念,打造高效工作流
  • TypeScript成为AI应用开发标配:从GitHub趋势看2026前端技能重塑
  • 还在手动解包PKG和DMG?Brigadier一条命令搞定Mac的Boot Camp驱动
  • 基于RWEQ模型的2000-2025年中国逐年250米分辨率实际风力侵蚀数据集
  • 2026甄选:北京经开区药企车间改造品牌机构实力观察 - 卓企推荐
  • ChatGPT Plus / Pro 用户的 Codex 进阶实战:从 CLI 配置到 Agent 工作流、多文件重构与用量控制的完整指南
  • 助力东莞本土企业腾飞:美丽寮步网站建设高性能背后的技术逻辑与商业价值
  • Rolldown:基于Rust的高性能前端构建引擎解析与迁移指南
  • Claude Code架构深度解析:从AI编程工具到现代Web应用设计
  • 揭秘MoE与注意力机制:从DeepSeek-V3到开源架构的工程实践
  • 混合RAG与智能体架构:解决科学设施运维知识检索与决策难题
  • AI Agent技能评估:从主观验收到系统化Eval方法论实践
  • 佛山行业网站建设 哪家强?揭秘从0到1打造高转化率网站的底层逻辑与实践
  • 智慧园区数字孪生技术实战:从三维建模到实时数据驱动的架构设计
  • 抖音下载神器:3步搞定无水印视频批量下载
  • Pandas滚动与指数加权移动平均:时序数据平滑与趋势分析实战
  • 技术口碑鉴别指南:从SEO噪音中识别真实用户反馈
  • 基于RWEQ模型的2000-2025年中国逐年500米分辨率实际风力侵蚀数据集
  • 从OpenClaw到Hermes:下一代AI智能体开发平台迁移与实战指南
  • format函数用错?占位符大括号一丢,代码直接崩给你看
  • 基于RWEQ模型的2000-2025年中国逐年250米分辨率潜在风力侵蚀数据集
  • EMR Serverless StarRocks 2.2.0:一条SQL实现多模态数据智能检索
  • AI Agent构建:从技能堆砌到核心推理架构的实践反思
  • 从LLM到机器人控制:构建AI智能体仿真原型的技术实践
  • 文件不关?Python开发老手都栽这坑里,你还在硬扛?
  • 基于差分进化与MCMC的SiC外延层厚度智能反演与可视化系统