ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP+Servlet+MySQL选课系统开发:建模、事务与防坑要点

JSP+Servlet+MySQL选课系统开发:建模、事务与防坑要点 简介一份基于JSPServletMySQL实现的学生选课管理系统源码面向JavaWeb课程设计、毕业设计以及入门学习者。系统支持教师、学生双角色登录教师端可管理学生信息、课程信息与选课记录设置必修学分上下限学生端可选课、查看已选课程、修改个人信息并支持查看未修够最低学分的学生名单功能覆盖常见教务管理场景。资源包为zip格式共105个文件包含25个Java源码、25个class编译文件、15个JSP页面、19张界面截图另有SQL数据库脚本、论文文档、运行指导视频及项目配置文件整体约108.89MB压缩包目录结构清晰便于按模块查找。代码采用JSPServletJSJDBC技术栈可在Eclipse或MyEclipse中配合JDK1.8、Tomcat8、MySQL5.5至5.7环境直接部署三类资料相互补充便于深入理解前后端交互与数据库设计。已有4704人学习下载适合需要一份可运行完整项目源码作参考的JavaWeb开发学习者。1. 学生选课管理系统JSPServletMySQL 这套老组合凭什么还值得跑一遍一个基于 jspservletmysql 开发的学生选课管理系统说白了三张角色表加一张选课关系表把登录、选课、退课、成绩录入这些教务场景串起来。2015 年前后这类项目就是 Java Web 课设的标配到现在依然是很多刚学完 java 基础的人第一个能完整跑通的 Web 项目。它不像 Spring Boot 那样把很多东西自动配置掉反而要求你手动管理 Servlet 生命周期、数据库连接和会话状态这对理解 Web 应用的底层流程很有帮助。适合的人群很明确做课程设计的在校生、准备 java 面试时需要补一个完整项目的求职者以及想快速看一套 CRUD 系统如何落地的转行开发。下面我按数据建模、环境启动、核心代码、踩坑记录这个顺序拆开讲你照着走完就能跑起来也知道下一步往哪儿改。2. 先看数据底座选课系统的 5 张表怎么设计多角色权限落在哪2.1 为什么不合并成一张 user 表从字段差异和职责隔离看学生选课这个场景角色天然分为学生、教师、管理员。很多教程喜欢把前两者合并成一张 user 表再加一个 role 字段这样登录逻辑能省不少事。但真实做教务系统时我一般会拆成 student、teacher、admin 三张独立表再加一张课程表 course 和选课关系表 student_course。原因是学生有学号、专业、年级、已修学分教师有工号、职称、所属院系这些字段差异很大合并后要么空字段太多要么得在外围用扩展表补复杂度反而上去了。角色数据分开权限校验也更好写。登录时你只需要知道当前是哪个表里查出来的用户session 里存一个 role 字符串。后续每个 Servlet 在处理请求之前先检查 session 里的 role 是否匹配这就完成了第一层权限控制。管理员、教师、学生三者的业务入口本来就不一样分开建表之后页面路径和接口也能按目录隔离开不会出现一个用户对象里同时挂着学号和工号这种尴尬结构。2.2 五张表的字段设计与唯一索引把约束写进数据库建表脚本如下这是我在 MySQL 8.0 下验证过的版本5.7 也可以直接使用。CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_select; CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL COMMENT 学号, password VARCHAR(64) NOT NULL COMMENT 登录密码存 MD5 或加盐哈希, name VARCHAR(50) NOT NULL COMMENT 姓名, major VARCHAR(100) COMMENT 专业, grade VARCHAR(10) COMMENT 年级如 2022, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE teacher ( id INT NOT NULL AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL COMMENT 工号, password VARCHAR(64) NOT NULL, name VARCHAR(50) NOT NULL, title VARCHAR(30) COMMENT 职称讲师/副教授/教授, dept VARCHAR(100) COMMENT 院系, PRIMARY KEY (id), UNIQUE KEY uk_teacher_no (teacher_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(20) NOT NULL, password VARCHAR(64) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, teacher_id INT NOT NULL COMMENT 授课教师关联 teacher.id, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, capacity INT NOT NULL DEFAULT 60 COMMENT 容量上限, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数冗余字段, schedule VARCHAR(200) COMMENT 上课时间地点如 周一3-4节/教301, PRIMARY KEY (id), UNIQUE KEY uk_course_no (course_no), KEY idx_teacher_id (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student_course ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩教师录入, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course_id (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表脚本里有两个刻意设计。第一个是 student_course 上的联合唯一索引 uk_student_course从数据库层面杜绝同一学生重复选同一门课。哪怕代码里忘了判断并发情况下也会报 Duplicate entry 错误。很多人只在 Service 层做 select 判断两个请求同时进来都能通过检查最终表里出现两条一模一样的记录唯一索引是兜底的防线。第二个是 course 表里的 selected_count 冗余字段列表页显示剩余名额时不需要再 count 子查询效率高很多但它属于可推导数据更新时必须和选课记录放在同一个事务里这个在第 4 章会专门说。2.3 冗余字段与物理外键的取舍容量计数的性价比选择这个表结构里所有表都没有物理外键只有逻辑关联。第一个原因是 MySQL 单表并发写入时外键约束会带来额外的锁开销选课高峰时体验很明显第二个原因是课设项目经常要导入导出、批量更新数据有外键时数据清理特别麻烦得按依赖顺序删。我见过太多人因为外键把删除数据的顺序写错导致删不掉又找不出原因。物理外键丢失的完整性用联合唯一索引加事务补偿掉这是更务实的做法。如果你在公司正式系统里外键禁用与否取决于团队规范单就这个选课系统来讲不加外键能省掉大量调试时间。selected_count 这个冗余字段则需要在代码里格外小心。它不是一个可以随便改的值它的含义是“student_course 表中该课程的有效行数”。所以凡是插入选课记录的地方必须紧跟一条 selected_count1 的更新凡是删除选课记录的地方必须紧跟一条 selected_count-1 的更新。两句话必须放在同一个事务里否则就会出现容量显示和实际选课人数对不上的脏数据。第 6 章我会给一条 SQL 专门用来检查这个问题。2.4 初始化数据先造测试账号再造课程样例数据表建好之后我习惯第一时间插入几条测试数据。这样后面登录系统时才有东西可测否则页面是能打开但登录按钮点了没反应你分不清是代码问题还是数据问题。测试账号要覆盖三种角色密码统一用 123456 方便记忆存储时用 MD5 处理。INSERT INTO student (student_no, password, name, major, grade) VALUES (2022001, MD5(123456), 张三, 计算机科学与技术, 2022); INSERT INTO teacher (teacher_no, password, name, title, dept) VALUES (T1001, MD5(123456), 李老师, 副教授, 计算机学院); INSERT INTO admin (username, password) VALUES (admin, MD5(123456)); INSERT INTO course (course_no, course_name, teacher_id, credit, capacity, selected_count, schedule) VALUES (CS101, Java 程序设计, 1, 3.0, 60, 0, 周一3-4节/教301); INSERT INTO course (course_no, course_name, teacher_id, credit, capacity, selected_count, schedule) VALUES (CS102, 数据库原理, 1, 3.0, 50, 0, 周三1-2节/教202);密码用 MD5 存储。我知道 MD5 在正式场景里已经不安全但课设项目一般不会引入 Spring SecurityMD5 配合加盐已经是这个体量下的折中。如果你想更正式一点可以在 Java 代码里用DigestUtils.md5DigestAsHex(password.getBytes())先把密码算好再替换掉这里的 SQL 函数。这里要注意 course 表插入时 teacher_id 填的是 1因为前面插入的教师李老师的自增 id 默认就是 1这两个表之间的逻辑关联要自己保证对得上。3. 环境搭建与首次启动从 MySQL 8.0 安装到 Tomcat 部署很多拿到源码的人第一反应是“先配环境”然后卡在 MySQL 安装上。这个系统的运行条件其实很简单JDK 8 或 11、Tomcat 8.5/9.0、MySQL 5.7 或 8.0、mysql-connector-java 驱动包放进 WEB-INF/lib。下面按我实际跑过的顺序来。3.1 mysql 5.7 还是 8.0驱动类名、连接串与认证插件的差异mysql 5.7.44 是老项目最稳妥的选择安装包直接下一步就能用启动服务用net start mysql。MySQL 8.0 的安装包比如 8.0.46会默认带一个 MySQL Installer里面要求你设置 root 密码、选端口默认 3306这些都不难难的是驱动版本。8.0 的 Connector/J 类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver连接串里还强制要求时区参数不写serverTimezoneAsia/Shanghai会直接抛 SQLException。对比项mysql 5.7mysql 8.0驱动类名com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver连接串是否强制时区不强制强制 serverTimezone默认认证插件mysql_native_passwordcaching_sha2_password安装方式安装包或解压版MySQL Installer 或解压版这张表的最后一行是第三个坑。8.0 默认的 caching_sha2_password 插件会让老版本驱动连不上报错信息往往只有一句“Unable to load authentication plugin”一度被当成玄学。解决方式有两种一是升级驱动到 8.0.x二是把用户改回 mysql_native_password。对跑课设来说升级驱动是最省事的路径。3.2 Tomcat 版本选择为什么 9 可以用10 会翻车另一个必须注意的点是 Tomcat 版本。Tomcat 10 把 Java EE 的包名从 javax.* 迁移到了 jakarta.*而绝大多数 jspservlet 课设源码里 import 的是javax.servlet.http.HttpServlet。你把源码丢进 Tomcat 10编译期就出错页面 404 也是常事。所以跑这套系统老老实实用 Tomcat 8.5 或 9.0省掉一半的兼容性问题。具体到 IDEA 里配置 Tomcat 9 时要注意 Server 标签页的JMX port不要和本机其他进程冲突HTTP port 默认 8080不用改。Deployment 标签页里要确认打包方式是 war exploded这样修改 JSP 时不用重新构建整个 war。很多新手把 Deploy 改成 war 模式后每次改一个页面要等十几秒的重新打包其实完全没必要。3.3 初始化数据库的最小流程mysql 命令行导入与验证建库脚本在上一章已经给出你要做的是把它保存成一个 course_select.sql 文件然后执行导入命令。mysql -u root -p course_select.sqlWindows 下如果你没把 mysql 加到 PATH很常见因为安装时默认不勾选就得进到 MySQL 安装目录的 bin 下执行路径一般是D:\tool\mysql-8.0.46-winx64\bin\mysql -u root -p course_select.sql。5.7 的安装目录结构差不多只是版本目录名不同。执行完没有任何输出就说明成功了如果报 ERROR 1049说明数据库不存在先检查脚本第一行的 CREATE DATABASE 有没有被执行或者当前用户有没有建库权限。导入完成后我习惯随手验证一次避免后面代码跑了半天才发现是表结构不对。SHOW TABLES; SELECT COUNT(*) FROM student;SHOW TABLES应该返回五张表SELECT COUNT(*)必须返回 1 以上这样才能确认数据真的进去了。这一步三十秒能做完但能省下后面一小时的黑匣子排查时间。3.4 db.properties 参数说明与 DBCP2 连接池的最小配置这个项目的数据访问层通常是自己写 JDBC 工具类不用 MyBatis。连接池方面DBCP 和 C3P0 都是经典选择Tomcat 自带的 JNDI 连接池也能用。我建议用 DBCP2因为配置短、依赖少课设项目两三个 jar 就能带起来。先放 db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/course_select?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8useUnicodetrue jdbc.usernameroot jdbc.password123456 jdbc.initialSize5 jdbc.maxTotal20 jdbc.maxIdle10 jdbc.maxWaitMillis5000这里有几个参数你必须知道含义。useSSLfalse 是开发环境的标准配置不配的话 MySQL 8.0 会打一段警告日志但不报错serverTimezoneAsia/Shanghai 解决时差问题配成 UTC 会导致时间字段少 8 小时characterEncodingutf8 和 useUnicodetrue 是配合 JSP 页面编码一起工作的漏掉任何一个中文显示都会出问题。连接池的 initialSize、maxTotal 决定并发上限选课时如果同时涌入大量学生请求maxTotal 是瓶颈设置成 20 以上比较稳但同时要确认 MySQL 端 wait_timeout 是默认 8 小时长时间空闲连接被回收后 DBCP 会自动重连。对应的 DBUtil 类核心代码如下import org.apache.commons.dbcp2.BasicDataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static BasicDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); dataSource new BasicDataSource(); dataSource.setDriverClassName(props.getProperty(jdbc.driver)); dataSource.setUrl(props.getProperty(jdbc.url)); dataSource.setUsername(props.getProperty(jdbc.username)); dataSource.setPassword(props.getProperty(jdbc.password)); dataSource.setInitialSize(Integer.parseInt(props.getProperty(jdbc.initialSize))); dataSource.setMaxTotal(Integer.parseInt(props.getProperty(jdbc.maxTotal))); dataSource.setMaxWaitMillis(Long.parseLong(props.getProperty(jdbc.maxWaitMillis))); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这一段核心的工作是在类加载时把 db.properties 读成 Properties然后构造一个 BasicDataSource。注意我没有在 DBUtil 里做手动关闭连接的操作连接池管理的连接在调用 close() 时是归还给池子而不是真的关闭数据库连接这是一个新手容易绕晕的点。你在 DAO 层正常用完连接写 conn.close() 就对了它并不会把连接杀掉只是标记为空闲。3.5 IDEA 部署把源码包从压缩状态变成可运行项目拿到源码包后在 IDEA 里几乎是这个流程File → New → Project from Existing Sources选到源码目录选 Maven 就等依赖下载不是 Maven 的话直接当普通 Web 项目导入。然后把 mysql-connector、DBCP2 和 JSTL 这几个 jar 放进 WEB-INF/lib。这步很多人会漏漏掉的结果是项目能启动但一访问数据库相关的页面就报 ClassNotFoundException。配置好 Artifacts 和 Tomcat 后启动成功访问根路径比如http://localhost:8080/course_select/或者你项目的 context-path。如果直接看到登录页说明环境链路通了可以进入第 4 章看代码实现如果看到 404先看 IDEA 控制台有没有部署日志再看 Tomcat 的 webapps 目录下有没有生成对应文件夹这俩是定位部署问题最直接的入口。4. 核心链路代码登录路由、选课事务、退课回补的落地实现4.1 LoginServlet一个入口处理三种身份session 里只存必要字段登录流程是所有功能里最容易出错的部分因为它的成败取决于“查哪张表”的判断。我的实现是用一个 role 参数分流前端 form 提交时带上 role选中“学生”就提交 studentServlet 里按 role 分支执行对应的 SQL。下面的代码做了一定简化但流程完整。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); String role request.getParameter(role); String sql null; if (student.equals(role)) { sql SELECT id, name FROM student WHERE student_no? AND passwordMD5(?); } else if (teacher.equals(role)) { sql SELECT id, name FROM teacher WHERE teacher_no? AND passwordMD5(?); } else if (admin.equals(role)) { sql SELECT id, username AS name FROM admin WHERE username? AND passwordMD5(?); } try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { HttpSession session request.getSession(); session.setAttribute(userId, rs.getInt(id)); session.setAttribute(name, rs.getString(name)); session.setAttribute(role, role); response.sendRedirect(request.getContextPath() / role /index.jsp); } else { request.setAttribute(error, 账号、密码或角色不对); request.getRequestDispatcher(login.jsp).forward(request, response); } } } catch (Exception e) { e.printStackTrace(); response.sendRedirect(login.jsp?errorsyserror); } } }逻辑说明三个角色分支对应的 SQL 不同但统一返回 id 和 name 两列随后写入 session。重定向时按角色进入不同首页比如 student 登录后跳到 /student/index.jsp。这里有一个很容易被忽略的点密码比对在 SQL 里用 MD5(?) 完成数据库端做哈希比代码里先加密再传值少一道传输步骤正式系统建议在代码里加盐课设里两种都能跑。另外登录失败时我没有在重定向地址后面拼中文参数因为 URL 里带中文会涉及编码问题直接 forward 回 login.jsp 并在 request 里带 error 属性页面用 EL 表达式取出来提示这样最稳。4.2 选课操作的 JDBC 事务条件 UPDATE 防超卖是关键选课这个动作必须同时处理三件事检查课程是否已满、检查学生是否已选过、插入选课记录。很多人写的版本是“先 select 再 insert”手动跑没问题但一旦两个学生同时点同一门课的最后两个名额就会超卖。正确做法是把检查和写入放进一个事务并且让数据库约束兜底。下面这段代码用了条件 UPDATE 的技巧。public boolean selectCourse(int studentId, int courseId) { String insertSql INSERT INTO student_course (student_id, course_id) VALUES (?, ?); String updateSql UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try { PreparedStatement ps conn.prepareStatement(insertSql); ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); PreparedStatement ps2 conn.prepareStatement(updateSql); ps2.setInt(1, courseId); int rows ps2.executeUpdate(); if (rows 0) { throw new SQLException(课程容量已满或者课程状态异常); } conn.commit(); return true; } catch (Exception e) { conn.rollback(); throw new RuntimeException(选课失败: e.getMessage(), e); } } catch (SQLException e) { throw new RuntimeException(e); } }关键在 UPDATE 语句里加了AND selected_count capacity条件。MySQL 执行 UPDATE 时会对匹配行加行锁两个并发请求同时进来第二个会等第一个提交拿到锁后重新判断条件发现已满然后 rows0直接抛异常回滚。这就避免了“先查后写”的竞态问题。同时insertSql 如果碰到重复键会抛 DuplicateKeyException事务回滚student_course 表不会留下半条记录。我把两步都放进同一个事务保证要么都成功要么都回滚不会出现选了课但容量没加、或者容量加了但选课记录丢了的情况。4.3 退课与容量回补DELETE 和 UPDATE 的先后顺序不能反退课的逻辑相对简单但要反向操作先删除选课记录再把课程容量减一。这个顺序不能反。先 UPDATE 容量再 DELETE 记录的话如果 DELETE 失败会出现课程容量被白白减掉的情况。反过来先 DELETE即使 UPDATE 失败最多是课程显示容量比实际多了一个学生想要的名额还在问题不大。public void dropCourse(int studentId, int courseId) { String deleteSql DELETE FROM student_course WHERE student_id? AND course_id?; String updateSql UPDATE course SET selected_count selected_count - 1 WHERE id ?; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try { PreparedStatement ps1 conn.prepareStatement(deleteSql); ps1.setInt(1, studentId); ps1.setInt(2, courseId); ps1.executeUpdate(); PreparedStatement ps2 conn.prepareStatement(updateSql); ps2.setInt(1, courseId); ps2.executeUpdate(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } } catch (SQLException e) { throw new RuntimeException(退课失败, e); } }退课的代码比选课短但注意一点这里我故意没有在 UPDATE 里加selected_count 0的条件。你可能觉得加上更保险但在正常业务里 selected_count 不会出现负数因为每次递增都走事务如果因为历史脏数据出现负数问题应该在数据修复环节解决而不是靠退课接口挡住正常用户。数据修复我会放在第 6 章验证部分。4.4 教师模块成绩录入的 SQL 更新逻辑教师角色最常用的功能是给自己教的课程录成绩对应的是更新 student_course 表里的 score 字段。这个接口的业务约束在于教师只能改自己名下课程的成绩不能凭一个 courseId 就跨课操作。public void updateScore(int teacherId, int studentId, int courseId, double score) { String sql UPDATE student_course sc JOIN course c ON sc.course_id c.id SET sc.score ? WHERE sc.student_id ? AND sc.course_id ? AND c.teacher_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setDouble(1, score); ps.setInt(2, studentId); ps.setInt(3, courseId); ps.setInt(4, teacherId); int rows ps.executeUpdate(); if (rows 0) { throw new RuntimeException(没有找到对应的选课记录或课程不属于当前教师); } } catch (SQLException e) { throw new RuntimeException(成绩更新失败, e); } }这个写法用 JOIN 把 teacher_id 的条件放进了 UPDATE 语句里一条 SQL 同时完成了权限校验和数据更新。如果 teacherId 对不上rows 就是 0外面 catch 住就知道越权了。比先查再改少了一次数据库往返也避免了查出课程后忘了校验教师身份的漏洞。成绩录入页面的前端就是一组 input 框提交时带上 studentId 和 score这部分没什么可多说的。4.5 用 Filter 做第二道权限拦截配置一处保护一片前面提到的菜单按角色渲染只是显示层真正拦住越权的是服务端校验。最省事的实现是写一个 Filter按路径前缀做角色过滤。我一般会在 src 下建一个 AuthFilter 类WebFilter({/admin/*, /teacher/*, /student/*}) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); String path request.getRequestURI(); String role session null ? null : (String) session.getAttribute(role); boolean allowed false; if (path.contains(/admin/) admin.equals(role)) { allowed true; } else if (path.contains(/teacher/) teacher.equals(role)) { allowed true; } else if (path.contains(/student/) student.equals(role)) { allowed true; } if (allowed) { chain.doFilter(req, resp); } else { ((HttpServletResponse) resp).sendRedirect(request.getContextPath() /login.jsp); } } }这个 Filter 的逻辑很直白URI 里包含 /admin/ 就必须是 admin 角色包含 /teacher/ 就必须是 teacher 角色以此类推。只要路径规范一段代码保护了所有管理页面和业务接口。如果是用 web.xml 配置的话映射关系类似filter-mapping 里写死这六个前缀即可。很多人把 Filter 的request.getSession(false)写成request.getSession()这会导致未登录用户也创建一个空 session然后被重定向到登录页时白占了内存细节虽小但面试聊到会话管理时能说清楚这一点会很加分。5. 避坑指南多角色权限绕过、选课超卖与中文乱码的 5 个排查这一章全是实际跑项目时翻车过、或者帮别人调代码时见过的真实问题按“现象 → 原因 → 解决”整理出来。即使你的源码包和我描述的实现不完全一样排查思路是通用的。5.1 现象普通学生能直接访问教师管理页面原因很多课设源码只在菜单显示上做了角色判断但页面文件放在 webapp 目录下学生知道了路径就能直接输 URL 打开。更糟的是有些数据修改接口完全没有角色校验比如一个 DeleteStudentServlet不管谁调它都能执行。这属于权限校验只做了表面功夫没有在业务层落第二道闸。解决在业务 Servlet 的 doGet/doPost 开头加统一拦截最省事的方式是写一个 BaseAuthServlet继承 HttpServlet重写 service() 方法先检查 session 里的角色是否符合该模块要求不符合就重定向回 login.jsp。当然也可以写第 4.5 节说的 Filter专门匹配 /admin/、/teacher/这些前缀路径匹配到就先看角色。两种方式都行我一般用 Filter因为不用改动已有的 Servlet 代码。5.2 现象同一门课最后几个名额被多人同时抢到已选人数超过容量原因这就是第 4.2 节说的竞态问题。如果选课实现是先查询剩余容量、判断大于 0、再插入记录那么并发时两个请求都通过容量判断然后先后插入成功。MySQL 默认隔离级别是 REPEATABLE READ但两个独立事务之间的查询互不阻塞完全可能都读到容量为 1 的瞬间。解决把容量判断从查询里挪到 UPDATE 里用UPDATE course SET selected_count selected_count 1 WHERE id? AND selected_count capacity这种条件更新。后到的事务会被行锁挡住等前一个事务提交后重新执行条件判断发现已满就返回 0 行。同时给 student_course 加上联合唯一索引双保险。这一段是选课系统里最值得较真的代码也是面试官最喜欢问的“如何防止超卖”的落地答案。5.3 现象MySQL 8.0 启动后连接池报 Public Key Retrieval is not allowed原因MySQL 8.0 默认的密码插件是 caching_sha2_password客户端第一次连接时如果走非 SSL 通道需要额外机制获取公钥。Connector/J 驱动出于安全考虑默认不允许自动获取于是抛出这个异常。很多教程让你直接加 allowPublicKeyRetrievaltrue但这相当于降低了安全性不适合公网环境。解决本地开发最省事的做法是连接串加allowPublicKeyRetrievaltrueuseSSLfalse。如果你要严谨一点也可以把 MySQL 用户的密码插件改回 mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;。课设项目直接加参数就行别在这种地方多花时间但要知道这个参数是干嘛的面试官经常顺带问一句。5.4 现象JSP 页面和数据库里的中文都正常但读取出来全是问号原因这是老生常谈的编码链问题但每届都有人栽。问题可能出在三处JSP 文件本身不是 UTF-8 编码、jsp 页面头部的 contentType 没写 UTF-8、JDBC 连接串没带 characterEncoding 参数。这三处只要有一处不是 UTF-8最终页面上就会出现一串问号。解决从上到下检查三处。第一IDEA 里把 Project Encoding、Default File Encoding 都设成 UTF-8第二JSP 页面第一行写% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%第三db.properties 里的连接串 url 确保带着characterEncodingutf8useUnicodetrue。还有一个容易漏的地方是 POST 请求乱码需要在 Servlet 里对 request 调用setCharacterEncoding(UTF-8)而且这个方法必须在读取任何参数之前调用放 doGet 里没用因为 GET 参数走的是 URI 编码Tomcat 8.5 默认已经处理了。5.5 现象用 Tomcat 10 部署后所有 Servlet 一律 404控制台报 NoClassDefFoundError原因Tomcat 10 是一次大版本变更把 Java EE 换成了 Jakarta EEServlet API 的包名从 javax.servlet 变成了 jakarta.servlet。老源码里全是import javax.servlet.http.HttpServlet编出来的 class 和 Tomcat 10 的类加载器没法对接。这不是项目 bug是环境匹配问题。解决换 Tomcat 9 或者更低版本这是最快的路径。如果你想坚持用 Tomcat 10就得手动把整个项目里的 javax.servlet 全部替换成 jakarta.servlet再把依赖里的 servlet-api.jar 换成 Tomcat 10 自带的 jakarta.servlet-api工程量不小对课设来说没有必要。我的建议是直接下载 Tomcat 9.0.x解压后配置一个 CATALINA_HOME 环境变量IDEA 的 run configuration 里选本地 Tomcat 指向它就没有这个问题。提示上面 5 条按出现频率排的前两条是业务逻辑上的硬伤中间两条是环境配置的坑最后一条是版本兼容陷阱。如果你在跑项目时遇到的报错不在这个列表里优先去看 Tomcat 本地日志的 catalina.out 和 localhost.log这两个文件通常能把真正的堆栈信息打出来。不要只看浏览器里的 500 页面那个一般只显示一句话没有任何定位价值。6. 二次开发的验证方法SQL 数据一致性检查与选课冲突检测6.1 用一条 SQL 校验容量数据和选课记录是否一致很多源码包能跑通但你真的要改起来第一件事其实是学会验证。下面这段 SQL 是我每次改完选课逻辑之后必跑的检查用来发现容量和选课记录对不上的脏数据。SELECT c.id, c.course_name, c.capacity, c.selected_count, COUNT(sc.id) AS actual_count FROM course c LEFT JOIN student_course sc ON sc.course_id c.id GROUP BY c.id, c.course_name, c.capacity, c.selected_count HAVING actual_count ! c.selected_count;这个查询把 course 表里冗余的 selected_count 和 student_course 表里实际的选课行数做对比只要不一致就说明某个选课或退课事务没做完整。跑完如果有结果不要直接改数据库而是去查那段业务代码的事务边界到底是插入没有回滚还是 UPDATE 语句漏了条件。数据修复的 SQL 是另一段这里不展开重要的是建议你在每次发布前把这条查询当成系统自检工具顺手跑一遍。6.2 增加时间冲突检测把 schedule 拆成可比较的字段验证之外第二个值得做的小改进是给选课加时间冲突检测。现在的表结构里 schedule 字段只是存了一个描述性的字符串“周一3-4节/教301”要做冲突检测得把它拆成可比较的结构最简单的是加一列 week_day 和一列 class_slot比如周一到周五用 1 到 5 表示课时段用 1、2、3、4 代表上午、下午的不同节次。这样选课之前多查一步。SELECT COUNT(*) FROM course c JOIN student_course sc ON sc.course_id c.id WHERE sc.student_id ? AND c.week_day ? AND c.class_slot ?返回 0 就说明没有时间冲突返回大于 0 就提示“该时间段已有课程”。这个改进不大但能让系统的行为从一个“放课表”的系统变成一个“能排课”的系统面试讲项目时也更有内容。顺带一提这类基于 jsp 的课设项目比如毕业论文管理那些多半是同一个套路多角色权限、课程或文档管理、核心操作用事务包住最后再用 SQL 做一致性校验。我的个人习惯是每改一次和事务有关的代码就跑一遍上面那条 HAVING 语句同时记录改动前后的日志。这套系统本身不大但把验证写进习惯你后面再去接同构的课设项目时就很少因为改动数据而留下脏数据。希望你在这个项目上顺顺利利跑通也借此摸清 Java Web 从请求到数据库的完整链路希望帮你少踩几个坑。本文还有配套的精品资源点击获取
返回列表