
简介这是一套基于 ServletJSP 实现的学生选课管理系统完整源码包面向计算机相关专业正在准备毕业设计的学生以及需要项目实战练习的 Java 学习者。系统覆盖管理员、教师、学生三种角色管理员维护学生、教师与课程信息教师可查看课程、查看学生并录入成绩学生能浏览课程、在线选课并查询成绩。技术栈为 JavaWeb、Servlet、MySQL前端使用 JSP、CSS、Bootstrap、jQuery开发环境适用 IDEA/Eclipse 与 NavicatJDK 1.8 即可运行。压缩包共 112 个文件大小仅 2.39MB包含 22 个 Java 源文件、22 个 class 编译文件、21 个 JSP 页面、10 个 jar 依赖库以及 sql 数据库脚本、css/js 样式交互文件等结构清晰导入后可直接部署调试。资源经过严格调试确保能运行可作为毕业设计或课程设计的核心参照。目前已有 553 人学习下载适合需要完整后端逻辑与基础前端页面配合的初学者快速上手。1. 学生选课管理系统一份能直接复现的 JavaWeb 完整源码「学生选课管理系统」在高校 JavaWeb 课程设计清单里几乎年年出现需求也高度固定学生登录后能选课、退课、查成绩管理员登录后能开课、调容量、录成绩。这套完整源码的价值不在于功能花哨而在于把 Servlet JSP MySQL Tomcat 这条经典链路完整走通。解压、建库、启动 Tomcat十分钟左右就能在浏览器里看到登录页。它适合两类人一类是刚开始写 JavaWeb、想找个完整工程对照着理解请求转发和事务提交的新手另一类是课程设计时间紧、需要先跑通闭环再逐步改界面的同学。跟着这篇把环境搭起来把核心代码过一遍再动自己的需求比自己从零敲省力得多。2. 先看清这份资源的内部结构三层架构与四张表拿到源码压缩包先别急着点 Run。先花十分钟看目录结构能少走很多弯路。JavaWeb 课程设计源码通常不是 Spring Boot 那种自动化工程而是手写 Servlet JSP 的老三样包结构基本是 controller / service / dao 三层。我一般先扫一眼包名和 web 目录就能判断它是不是纯 Servlet 实现以及登录跳转大概是怎么写的。2.1 从工程目录看请求链路的职责划分代码包解压后典型的目录结构长这样src/com/course ├── controller # Servlet接收请求、校验参数、跳转 JSP ├── service # 业务事务控制、容量判断、成绩录入 ├── dao # 持久化JDBC 增删改查 ├── entity # User、Course、CourseSelection 实体类 └── util # DBUtil、MD5Util 等工具类 web ├── admin # 管理员页面 JSP ├── student # 学生页面 JSP ├── login.jsp └── WEB-INF/web.xml sql └── course_system.sql先看 controller 和 jsp 的对应关系。controller 里每个 Servlet 通常对应一个功能页面比如 LoginServlet 对应 login.jspCourseListServlet 对应 student/courseList.jsp。service 层是重头戏选课防超员、退课回退人数的逻辑都在这层dao 层只负责把 SQL 发出去。下面是这份源码里几个关键文件的作用目录或文件职责改动时注意controller 包接收请求、解析参数、跳转页面与 JSP 表单的 action 必须一一对应service 包业务规则、事务提交回滚选课并发和容量判断都在这层dao 包封装 JDBC 操作表结构变了这里要同步改web/admin管理员端页面访问控制靠 role 判断web/student学生端页面同一份代码按角色渲染菜单sql/course_system.sql建库建表并预置数据启动前必须执行且要先看一眼这套结构的好处是功能点多了以后新增模块只需照着已有的 Servlet 复制一个再在 dao 里加对应方法即可。改 JSP 的时候不会误伤业务代码查问题也知道去哪一层看。2.2 web.xml 与 URL 映射注解还是配置Servlet 3.0 之后每个 Servlet 可以用 WebServlet 注解直接声明 URL不一定非要在 web.xml 里配 servlet-mapping。这几年写的课程设计源码web.xml 里往往只剩欢迎页和会话配置?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list session-config session-timeout30/session-timeout /session-config /web-app这里主要看两点。一是 session-timeout默认 30 分钟课程设计演示时如果嫌频繁重新登录可以调大或改成 60。二是 welcome-file如果访问根路径直接跳 login.jsp入口就没有问题。有的老资源还在用 web.xml 里配 servlet-mapping 的方式那么在 2.1 的 controller 包里是看不到 WebServlet 注解的URL 映射集中在 web.xml 的 servlet 和 servlet-mapping 标签里改 URL 时需要两处同步改。判断方法很简单在 IDEA 里打开 controller 任意一个 Servlet看类名上方有没有 WebServlet(/xxx)。有就以注解为准没有去 web.xml 里搜同一个类名。很多 404 问题就是注解路径和 JSP 里 form action 路径没对上。2.3 SQL 建表脚本四张表怎么关联数据库脚本是这份资源里最值得先看的东西。学生选课的核心数据模型就是用户表、课程表、选课记录表三张表。常见做法是把学生和管理员都放进 user 表用 role 字段区分省去复杂的关联CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT student COMMENT admin-管理员 student-学生, real_name VARCHAR(50) ) ENGINEInnoDB; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL, course_name VARCHAR(50) NOT NULL, credit DECIMAL(2,1) DEFAULT 2.0, teacher_name VARCHAR(50), max_capacity INT DEFAULT 60, selected_count INT DEFAULT 0, course_time VARCHAR(100) ) ENGINEInnoDB; CREATE TABLE course_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME, score DECIMAL(5,2) DEFAULT NULL, KEY idx_course (course_id), KEY idx_student (student_id) ) ENGINEInnoDB;user 表负责登录和角色判断course 表存课程基本信息course_selection 是中间表把学生和课程做多对多关联。score 字段放在中间表而不是 course 表因为成绩描述的是「某个学生对某门课」的结果不是课程本身的属性。max_capacity 是整门课容量selected_count 是当前已选人数。这里必须说明selected_count 是一个冗余字段。冗余的目的是列表页直接显示「已选人数 / 容量」不用每次用 COUNT(*) 聚合。代价是每次选课、退课都要同步 UPDATE 这个字段忘了更新就会出现人数对不上的问题。第 4 章会专门讲这个 UPDATE 的事务写法。另外脚本结尾的预置数据非常重要。很多号称完整的源码表结构没问题但 INSERT 里没插 admin 账号拿到手登录页面怎么输入都进不去最后才发现要手动补一条管理员数据。3. 本地部署把系统跑起来JDK、Tomcat 与 MySQL 的版本搭配复现的第一步是版本对齐。这个项目是 Servlet JSP 的 Web 工程不是 Spring Boot不需要 Maven 拉一大串依赖但 Tomcat 版本必须选对。JavaWeb 项目里版本问题占了一半以上的报错剩下的才是代码问题。3.1 版本组合为什么我推荐 JDK 1.8 Tomcat 9 MySQL 8.0课程设计源码大多是在 JDK 1.8 时代写的JDK 8 兼容性最好Tomcat 9 对应 Servlet 4.0还沿用 javax.servlet 包名。Tomcat 10 起改成了 jakarta.servlet直接运行老代码会报 NoClassDefFoundError这个坑第 5 章细讲。MySQL 用 8.0 的话驱动类要选 com.mysql.cj.jdbc.Driver。组件推荐版本说明JDK1.8与老代码的编译级别一致Tomcat9.0.x支持 javax.servlet避开包名迁移问题MySQL8.0 或 5.78.0 需用新驱动类名JDBC 驱动mysql-connector-java 8.0.x同时兼容 5.7 和 8.0数据库连接信息通常在 util 包或 src 根目录的 db.properties 里。常见的配置长这样drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot password123456url 里的参数每一个都是经验值。characterEncodingutf8 保证中文不乱码serverTimezoneAsia/Shanghai 解决 MySQL 8.0 的时区报错useSSLfalse 关掉 SSL 警告allowPublicKeyRetrievaltrue 配合 MySQL 8.0 的 caching_sha2_password 认证方式不加它有时会报 Public Key Retrieval is not allowed。如果数据库是 5.7最后两个参数可以去掉但留着也不影响。如果这份源码的 util 包里用的是最朴素的 DriverManager核心就是一个 DBUtilpublic class DBUtil { private static final String URL jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; public static Connection getConnection() throws Exception { Class.forName(com.mysql.cj.jdbc.Driver); return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明Class.forName 加载驱动类DriverManager.getConnection 建立连接。这个类在课程设计里足够用。如果想升级成连接池把 getConnection 方法体换成 dataSource.getConnection()dao 层其他代码完全不用改。连接池的 jar 包放到 web/WEB-INF/lib 下非 Maven 工程手动加 jar 就是这个路径。3.2 IDEA 导入工程与 Tomcat 启动配置IDEA 里跑 JavaWeb 项目配置核心是把源码识别成 Web 工程再关联到本地 Tomcat。步骤按顺序来File → Open选到解压后的工程目录。如果里面有 .idea 或 .iml直接打开没有的话 IDEA 会按普通工程识别。File → Project Structure → ProjectSDK 选 JDK 1.8语言级别选 8。Project Structure → Facets点 号选 Web把 web 目录指定为 Web 资源目录。Project Structure → Artifacts点 号选 Web Application: Exploded把刚才的 Web Facet 加进去。Run → Edit Configurations点 号选 Tomcat Server → Local指定 Tomcat 目录。在 Deployment 页签把这个 Artifact 加进去Application context 写 /course_system或按源码里的跳转路径来定。启动 Tomcat浏览器访问 http://localhost:8080/course_system/。最后一步最容易卡住的是 Application context。如果源码里所有 sendRedirect 都带 request.getContextPath()那么 context 叫什么都能自适应如果源码里写死 sendRedirect(/login)context 就必须是 /否则跳转全是 404。遇到白屏先看地址栏路径再查 Servlet 里的重定向写法。# Linux / macOS 下查看 Tomcat 实时日志 tail -f $CATALINA_HOME/logs/catalina.out # Windows 下直接在 IDEA 的 Run 窗口看输出 # 出现 Starting ProtocolHandler 表示端口和连接器已就绪日志里搜关键字 Caused by比从头读堆栈高效。如果报端口占用说明 8080 被占改 Tomcat 配置里的 port 或杀掉占用进程。提示Tomcat 端口被占用时优先改 server.xml 的 Connector port不要盲目杀进程否则可能误伤其他服务。3.3 初始化数据库并验证预置数据数据库初始化有两种方式。工具类图形界面比较简单打开 Navicat左上角新建连接host 填 localhost端口 3306用户名 root密码按本地 MySQL 的设置填然后右键运行 SQL 文件选 sql/course_system.sql。命令行方式则是mysql -u root -p sql/course_system.sql执行完先看两张关键表里有没有数据USE course_system; SELECT id, username, password, role FROM user; SELECT COUNT(*) AS total_course FROM course;user 表至少要有一条管理员数据。如果脚本里没有预置 INSERT登录就会一直是失败的。此时手动补一条测试数据INSERT INTO user(username, password, role, real_name) VALUES (admin, 123456, admin, 系统管理员), (stu001, 123456, student, 张三);注意如果源码的登录逻辑里用了 MD5 工具类password 字段就不能直接插明文得先用 MD5Util 加密一次再插入。判断方法还是去登录 Servlet 里看 userService.login 的查询语句查的是 password ? 还是 password MD5(?)。这一步对了登录页才能真正进去。4. 核心功能逐段拆解登录、选课防超选与成绩录入整个系统的核心逻辑集中在三件事上登录后按角色分流、选课时防止超出课程容量、退课和成绩录入时保持数据一致。这三段代码看得懂后续改功能基本没有障碍。4.1 登录与角色分流Session 和重定向路径的配合登录模块是所有页面的入口。用户提交表单后LoginServlet 接收 username 和 password调 service 层查询 user 表返回一个 User 对象WebServlet(/login) public class LoginServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); UserService userService new UserService(); User user userService.login(username, password); if (user null) { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } HttpSession session request.getSession(); session.setAttribute(loginUser, user); if (admin.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /admin/courseList); } else { response.sendRedirect(request.getContextPath() /student/courseList); } } }逻辑说明登录失败用 forward 转发回 login.jsp而不是重定向。这样 errorMsg 能带到页面显示并且地址栏不会跳走用户可以直接重新输入。登录成功用 sendRedirect浏览器地址变成新的 URL刷新页面时不会重复提交表单。role 字段决定跳转方向同时被放进 session后续每个 JSP 都可以通过 session 里的 loginUser 判断应该渲染哪些菜单。参数说明request.getContextPath() 返回当前应用的部署上下文比如 /course_system。把它拼在重定向路径前面工程部署到任何 context 下都不会因为路径写死而 404。这是一个值得养成的习惯包括 JSP 里的静态资源引用最好都用 ${pageContext.request.contextPath} 拼路径。4.2 选课防超选FOR UPDATE 行锁与事务边界选课是并发场景最集中的地方。两个学生在同一秒提交选课如果代码只是先查 selected_count 再判断是否小于 max_capacity两个请求都可能读到同一个旧值最后课程超员。标准做法是让查询和更新处于同一个事务并给课程行加锁public boolean selectCourse(int studentId, int courseId) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 锁住课程行防止其他事务同时修改 course String lockSql SELECT max_capacity, selected_count FROM course WHERE id ? FOR UPDATE; ps conn.prepareStatement(lockSql); ps.setInt(1, courseId); rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int maxCapacity rs.getInt(max_capacity); int selectedCount rs.getInt(selected_count); if (selectedCount maxCapacity) { conn.rollback(); return false; // 课程已满 } // 2. 插入选课记录 String insertSql INSERT INTO course_selection(student_id, course_id, select_time) VALUES (?, ?, NOW()); ps conn.prepareStatement(insertSql); ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); // 3. 更新已选人数 String updateSql UPDATE course SET selected_count selected_count 1 WHERE id ?; ps conn.prepareStatement(updateSql); ps.setInt(1, courseId); ps.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { // 按 rs、ps、conn 的顺序关闭连接归还连接池 } }逻辑说明SELECT ... FOR UPDATE 是 InnoDB 的行锁语法。第一个事务锁住课程 id 对应的行第二个事务执行同样的 SELECT 时会被阻塞直到第一个事务 commit。这样第二个事务读到的一定是更新后的 selected_count从根上杜绝超选。事务边界从 setAutoCommit(false) 开始到 commit 或 rollback 结束中间任何异常都能整体回滚不会出现选课记录插进去了、人数却没加的情况。参数说明setInt(1, studentId) 是 JDBC 的预编译占位参数第一个 ? 对 studentId第二个 ? 对 courseId。用 PreparedStatement 而不是拼 SQL 字符串既能防止 SQL 注入又让 MySQL 可以复用执行计划。代码里返回 false 的场景前端 JSP 一般会弹提示或者显示「课程已满」具体效果取决于资源里前端部分的写法。总容量和已选人数的校验放在 service 层是为了让这段逻辑能同时被 Servlet 和可能的定时任务调用。4.3 退课与成绩录入UPDATE 的顺序和 WHERE 条件退课是选课的逆操作但顺序有讲究。常见做法是先删 course_selection 里的记录再回退 course 表的 selected_countpublic boolean cancelCourse(int studentId, int courseId) { Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 1. 先删选课记录 String deleteSql DELETE FROM course_selection WHERE student_id ? AND course_id ?; PreparedStatement ps conn.prepareStatement(deleteSql); ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); // 2. 再回退已选人数 String updateSql UPDATE course SET selected_count selected_count - 1 WHERE id ?; ps conn.prepareStatement(updateSql); ps.setInt(1, courseId); ps.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } }逻辑说明course_selection 里的记录是业务事实selected_count 只是冗余统计字段。先删事实再调统计值即使第二步 UPDATE 失败记录已经删掉了退课成立最多是人数多算一个可以靠脚本校准。反过来如果先减人数、DELETE 失败就会出现记录还在、人数却少了学生其实没退成课数据更难看。成绩录入是另一个容易出事的地方。核心 SQL 是String sql UPDATE course_selection SET score ? WHERE course_id ? AND student_id ?; PreparedStatement ps conn.prepareStatement(sql); ps.setBigDecimal(1, score); ps.setInt(2, courseId); ps.setInt(3, studentId); int rows ps.executeUpdate(); if (rows 0) { // 提示该学生没有选这门课不要静默失败 }注意 WHERE 条件必须同时带 course_id 和 student_id。如果只写 course_id会把这门课所有学生的成绩一次性改成同一个分数这是成绩录入模块最常见的事故。更新行数 rows 一定要判断等于 0 时说明选课记录不存在应该提示操作者重新核对不能假装成功。5. 复现避坑四个高频翻车现场与排查步骤这一章把本地复现时最容易卡住的问题集中列一遍。每一条都是实际运行过的坑按「现象 → 原因 → 解决」的顺序写遇到问题可以直接按图索骥。5.1 中文乱码从 JSP 到数据库的四个设置点现象页面上课程名称显示成问号或者往数据库插中文时 MySQL 报 Incorrect string value: \xE8\xAF\xBE...。原因字符集在 JSP 文件、请求接收、JDBC 传输、数据库存储四个环节不一致。最常见的是 JSP 页面是 UTF-8数据库表却是 latin1或者 Tomcat 接收 POST 请求时默认用 ISO-8859-1 解析。解决四个地方逐一确认。JSP 顶部要写 % page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8%web.xml 里加一个编码过滤器JDBC URL 带上 characterEncodingutf8建库时用 utf8mb4。Tomcat 自带的过滤器可以直接复用filter filter-nameencodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping这个过滤器只处理请求编码。响应编码最好在每个 JSP 的 page 指令里声明确认因为有的 Servlet 直接 write 字符串时不经过 JSP。IDEA 的 Tomcat 控制台如果出现中文乱码一般不影响功能但看着难受可以在 Tomcat 的 catalina.bat 里加 -Dfile.encodingUTF-8 解决。5.2 Tomcat 10 导致 NoClassDefFoundError: javax/servlet/xxx现象Tomcat 启动后访问页面报 java.lang.NoClassDefFoundError: javax/servlet/ServletException或者 ClassNotFoundException: javax.servlet.Filter。原因Tomcat 10 开始把 Servlet API 从 javax.servlet 迁移到 jakarta.servlet。老项目 import javax.servlet.*Tomcat 10 的类库已经不带这些包了。解决最省事的方案是换回 Tomcat 9在 IDEA 的 Run Configuration 里把 Tomcat Server 换成 9.0.x代码一行不用改。如果机器上只有 Tomcat 10也可以用 IDEA 的 Edit → Find → Replace in Files把 javax.servlet 全局替换成 jakarta.servlet同时把 web.xml 的命名空间换成 jakarta 版本web-app xmlnshttps://jakarta.ee/xml/ns/jakartaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttps://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd version5.0替换的时候注意范围只替换 import 和 web.xml 头部不要动业务包名。替换完重新编译如果工程里有第三方库内置了 javax.servlet 的类还可能冲突所以我的建议是课程设计阶段直接用 Tomcat 9不值得在这上面浪费时间。5.3 MySQL 8 连不上驱动类、时区、公钥检索三个参数现象启动后页面报 Communications link failure或者 Public Key Retrieval is not allowed也可能直接 Access denied for user rootlocalhost。原因MySQL 8.0 默认用 caching_sha2_password 认证老版本的 5.1.x 驱动不认识同时新版驱动要求 URL 里明确时区不写 serverTimezone 就可能报错allowPublicKeyRetrieval 不开启时第一条查询会被拒绝。解决先把 3.1 节 db.properties 里的完整 URL 复制过去驱动类用 com.mysql.cj.jdbc.Driver。如果这样还报 Access denied多半是密码本身不对要么是 root 密码没改成配置里的值要么是 MySQL 用户用了别的 host。作为兼容手段也可以把 root 的认证方式临时改回 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;这条命令在开发环境有效生产环境不建议这样改。MySQL 8 的 caching_sha2_password 其实更安全只是因为老驱动不支持才需要降级。排查时优先看完整堆栈的第一行 Caused by不要只看末尾的 MySQLConnection 字样。5.4 删除课程失败与已选人数对不上现象管理员删除一门课时 MySQL 报 Cannot delete or update a parent row: a foreign key constraint fails或者选课后列表里人数不增加、退课后人数不减少。原因如果 course_selection 表建了外键指向 course 表course 表有子记录时直接删主记录必然被外键拦住。人数对不上则是 selected_count 冗余字段没有和选课记录同步更新或者同步更新不在同一个事务里。解决删课之前先删选课记录再删课程。如果建表脚本里没写外键业务上也要按这个顺序否则会留下孤儿记录。人数对不上时先别急着改代码用一条子查询校准UPDATE course c SET c.selected_count ( SELECT COUNT(*) FROM course_selection s WHERE s.course_id c.id );这条 SQL 以 course_selection 的真实记录数为准一次性修正所有课程的已选人数。跑完之后再看页面如果人数还是不对就是缓存或者页面没刷新。业务代码里彻底解决要回到第 4 章的事务写法让选课、退课和人数更新要么一起成功要么一起回滚。6. 把管理系统当骨架复用最小改动加一个功能课程设计最后大多要求「至少有一个自己扩展的功能」最常见的加分项是修改密码。这个功能不动表结构、不引入新依赖、不改已有流程是最小改动路径的典型代表。6.1 最小改动路径修改密码功能怎么加改动只涉及三处新建一个 Servlet、加一个 JSP 表单、在页面导航里加一个入口。Servlet 的核心逻辑是校验旧密码、更新新密码WebServlet(/student/updatePassword) public class UpdatePasswordServlet extends HttpServlet { private static final long serialVersionUID 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User loginUser (User) request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } String oldPwd request.getParameter(oldPassword); String newPwd request.getParameter(newPassword); // WHERE 里带旧密码更新成功说明旧密码正确 String sql UPDATE user SET password ? WHERE id ? AND password ?; // 执行后如果更新行数为 0提示旧密码错误不直接修改 } }逻辑说明UPDATE 语句把旧密码校验直接写进 WHERE更新行数为 1 说明旧密码正确且已改密为 0 说明旧密码不对或用户不存在不需要先 SELECT 再 UPDATE。密码字段如果源码里用的是 MD5新密码在 executeUpdate 之前要先走一遍同一个工具类否则登录时对不上。session 里的 loginUser 不包含密码改密后无需销毁 session保持登录状态即可。参数说明newPwd 一定要加长度校验和字符校验至少不能为空、不能短于 6 位。明文密码在任何课程设计源码里都是减分项如果原工程登录已经是 MD5扩展功能时保持同样的处理方式不要破坏一致性。6.2 自己验证一遍手工回归的固定套路改完功能别只点一下新页面就交差。JavaWeb 工程页面跳转多任何一个 context 路径改错都会白屏所以我每轮改动后都固定走一遍完整链路步骤操作预期结果1admin 登录新增一门课程course 表多一条记录selected_count 为 02学生登录选择这门课course_selection 多一条记录selected_count 变为 13学生执行退课选课记录删除selected_count 回到 04admin 给学生录入成绩score 字段有值学生端能查到每步都要同时看页面和数据库两处。页面跳转对了但数据没变问题在 Servlet 或 dao数据变了但页面没显示问题在 JSP 渲染或重定向路径。确认这四步都通再继续加新功能可以省掉大量返工时间。有一回我为了赶进度直接改了一个 JSP 里的跳转地址没同步改 Servlet 的重定向路径结果页面白屏了一下午最后发现只是 context 拼写问题。从那以后我每次动这类 JavaWeb 工程都强制把数据库先备份一份再按登录、选课、退课、录成绩的顺序手动走一遍确认页面和数据两边完全一致才敢继续。希望帮到你。本文还有配套的精品资源点击获取