资讯动态

JSP+Servlet+MySQL学生选课管理系统课设开发指南

发布时间:2026/10/8 3:53:23 来源:尧图企业网站定制
简介基于Java、JSP、Servlet与MySQL实现的学生选课管理系统面向Java Web初学人群与课程设计、毕业设计开发者解决学生在线选课、课程信息维护、成绩录入等场景需求。项目覆盖登录鉴权、课程管理、选课冲突校验、选课记录查看、成绩管理等核心模块并采用MVC思想将视图、控制层与数据访问分离便于阅读和二次开发。压缩包共408个文件大小约6.59MB包含43个Java源文件及对应class文件、25个JSP页面、50个JS脚本、15个CSS样式、11个jar依赖库以及SQL数据库脚本与相关配置文件另有大量gif图片和界面截图可直观展示运行效果。已有6906人学习下载配套完整源码、数据库脚本和资源目录可帮助开发者深入理解Servlet请求处理、JSP动态页面生成、JDBC操作MySQL等关键链路从代码中还能学习到JDBC增删改查封装、Servlet路由分发、JSP页面复用等实用技巧适合作为课程设计或项目实践的参考模板。1. 学生选课管理系统为什么用 JSPServlet 这一套才是课设的正确打开方式“学生选课管理系统”是 Java Web 课设里出现频率最高的题目没有之一。它不要求炫技而是要把 Java、JSP、Servlet、MySQL 串成一个能登录、能选课、能管课的真实 web 项目。我见过太多人一上来就赶 Spring Boot 的时髦结果被问一句“Servlet 生命周期是什么”就卡壳答辩现场翻了车。反直觉的是这套被不少人嫌弃的“老四样”恰恰是把 Web 原理吃透的最低成本路径JSP 渲染页面Servlet 接收请求JDBC 读写 MySQL三层职责清清楚楚。这篇按“环境→建库→写代码→排坑”的顺序走一遍适合正在赶课设的在校生、准备 java 面试的初学者以及想把手底 Java Web 功底补扎实的从业者。2. 环境与项目骨架JDK、Tomcat、MySQL 的版本搭配和最小可运行结构2.1 为什么还在用 JSPServlet它真正练到的是 MVC 的底子JSP 最终会被容器编译成 Servlet所以“JSPServlet”本质上是一套Servlet 处理请求和业务流转JSP 负责把数据渲染成 HTML。在 Spring Boot 出现之前这是 Java Web 的标准形态现在回头看它依然是最直观的 MVC 教材。M 是 JavaBean 和 DAO 层负责读写 MySQLV 是 JSP 页面负责展示C 是 Servlet负责接收请求、调 DAO、决定跳转。三者界线比任何框架都清楚。写代码时你能明确说出“这一行该放哪一层”这个能力在框架里反而不容易练出来。有同学问“课设能不能直接用 Spring Boot”能但前提是你说得清 DispatcherServlet 到底替你做了什么。手写一遍 Servlet 之后你会发现 RequestMapping 背后不过是“按 URL 找方法”的映射表拦截器不过是 Filter 的升级版。java 面试题里高频的“Servlet 生命周期”“转发和重定向的区别”“Session 和 Cookie 的关系”没有手写经验答起来很难有画面感。这门课设的隐性目标就是把这些概念变成肌肉记忆这也是 JAVAWEB 课设到现在还坚持用 ServletJSP 的原因。常见的分工方式是这样JSP 管首页、登录页、课程列表、消息提示Servlet 管登录、选课、退课、课程管理DAO 管 SQL 执行和结果集封装Filter 管统一编码和登录拦截。这样分完整个 web 项目的边界立刻清晰答辩时被问到“某块功能在哪”你抬手就能指到对应文件。前置知识要有 java 基础里的集合、IO、JDBC 概念再补一点 SQL 的增删改查这套系统就不难啃下来。2.2 版本搭配与驱动参数JDK8 Tomcat9 MySQL 怎么选才不折腾版本先给结论JDK 1.8 Tomcat 9.0 MySQL 5.7.44 是课设环境里最稳的组合。Tomcat 9 对应 Servlet 4.0 规范支持 WebServlet 注解javax.servlet 包名没变网上大量老教程可以直接对照JDK 11 也能跑但老 IDE 偶尔弹“模块不可见”的提示没必要在这个环节消耗精力。MySQL 8.0 也能用只是默认认证插件是 caching_sha2_password老驱动连不上的时候要么把用户改回 mysql_native_password要么在 JDBC URL 里加 allowPublicKeyRetrievaltrue。网上的 mysql 安装教程8.0 通常不会提这件事很多人装完才发现连不上所以这里单列出来。组件推荐版本说明JDK1.8与 Tomcat 9、老教程兼容性最好Tomcat9.0Servlet 4.0注解可用MySQL5.7.44 或 8.0.x5.7 更省心8.0 需处理认证插件mysql-connector-java8.0.x驱动类名必须是 com.mysql.cj.jdbc.DriverJDBC URL 我固定用下面这个参数含义要能说清楚jdbc:mysql://localhost:3306/course_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8参数说明useSSLfalse本机开发不需要 TLS 握手避免证书校验告警serverTimezoneAsia/ShanghaiMySQL 5.7 之后必须指定时区否则直接报时区错误characterEncodingutf8指定 JDBC 层字符集和后面第 5 章的乱码排查直接相关allowPublicKeyRetrievaltrue仅 MySQL 8.0 配合 caching_sha2_password 时需要5.7 不用加。在动手写 web 业务之前先写一个独立类验证连接。这一步能排除“代码还没写就先被环境拦住”的尴尬后面出问题时也知道往哪查。import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; public class JdbcCheck { public static void main(String[] args) throws Exception { Class.forName(com.mysql.cj.jdbc.Driver); Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/course_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8, root, 你的密码); Statement stmt conn.createStatement(); stmt.execute(SELECT 1); System.out.println(连接成功); conn.close(); } }上面的 main 方法跑通后说明驱动 jar、MySQL 服务和账号密码都没问题。com.mysql.cj.jdbc.Driver 是 8.x 驱动类名5.7 时期是 com.mysql.jdbc.Driver网上老教程如果写的是旧类名配 8.x 驱动会报 ClassNotFoundException复制前先确认你的驱动版本和类名是否配套。2.3 IDEA 部署结构与验证连接先跑通 JdbcCheck 再写业务代码mysql 驱动 jar 放进项目的 WEB-INF/lib而不是 Tomcat 的 lib 目录。Tomcat/lib 是全容器共享影响所有应用WEB-INF/lib 只对当前项目生效war 包自带依赖换机器不丢包。一个典型的 web 工程目录如下src/ Java 源码bean、dao、servlet、filter、util web/ JSP、CSS、JS web/WEB-INF/web.xml 部署描述符 web/WEB-INF/lib/*.jar mysql 驱动、JSTL 等IDEA 里的配置顺序经常漏先加 Tomcat 运行时再选 war exploded 构建产物最后在 Deployment 里把 Application context 设成 /course_system。上下文路径决定访问前缀JSP 里的链接要统一用 ${pageContext.request.contextPath} 拼前缀避免换路径时链接全断。servlet demo 能跑通的标准很简单访问 http://localhost:8080/course_system/login.jsp 能看到登录页登录后能跳到首页。这个最小闭环成立之前不要急着写业务代码。很多翻车发生在“页面写完了但 Tomcat 根本没部署上新版本”所以先把部署动作练熟后面每改一段代码都有把握验证。3. 数据库设计五张表的字段、建表 SQL 和三个必会查询3.1 从角色和动作拆出五张表系统里有三类人学生看课、选课、退课教师开课、看名单、录成绩管理员维护基础数据。这个业务模型里最核心的关系是“学生”和“课程”的多对多关系多对多必须拆一张中间表这是选课系统数据模型的骨架也是数据库设计题的必考逻辑。表名职责user登录账号用户名、密码、角色student学生扩展信息学号、姓名、专业、班级teacher教师扩展信息工号、姓名、职称course课程主数据名称、教师、学分、容量、已选人数course_selection选课关系学生选课记录、成绩student 表存学号、姓名、专业、班级user_id 指向 user.idteacher 表存工号、职称user_id 指向 user.idcourse 表存教师、学分、容量、已选人数course_selection 表存 student_id、course_id 和成绩。为什么把账号和个人资料拆成两张表因为登录只需要账号密码个人资料属于业务字段拆开以后学生和教师共用一张账号表登录逻辑只需查一张表不用判断角色再去对应表里找密码。3.2 建表 SQL 与关键约束唯一索引、容量字段和逻辑外键建表脚本直接复制可用。字符集统一使用 utf8mb4引擎用 InnoDB这样的组合能正确存储中文且支持事务和行锁后面防超卖也要靠它。CREATE DATABASE course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, role VARCHAR(10) NOT NULL DEFAULT student ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, major VARCHAR(100), class_name VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, teacher_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, title VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, credit DECIMAL(2,1) DEFAULT 2.0, capacity INT NOT NULL DEFAULT 40, selected_count INT NOT NULL DEFAULT 0, weekday TINYINT, slot TINYINT, classroom VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, score DECIMAL(4,1), UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关键设计点说明password 列留 VARCHAR(50)因为 MD5 哈希有 32 位再加盐会更长按现在的长度收紧字段以后扩展麻烦course 表同时保留 capacity 和 selected_count容量控制就靠这两个字段比较缺一个都做不了“满员”判断course_selection 的 uk_student_course 唯一索引是在数据库层拦截重复选课比在 Java 代码里先查再插可靠得多外键我没有建采用逻辑外键。删除课程时如果已有选课记录物理外键会阻止删除或触发级联删除课设阶段这个约束会让“维护课程”功能变得复杂。保留关联字段但不建 FOREIGN KEY由业务代码保证一致性答辩时能解释清楚这个权衡比盲目建外键再被卡住要好。初始化几条数据方便后面直接登录测试INSERT INTO user (username, password, role) VALUES (admin, MD5(123456), admin), (student01, MD5(123456), student), (teacher01, MD5(123456), teacher); INSERT INTO student (user_id, student_no, name, major, class_name) VALUES (2, 2024001, 张三, 软件工程, 软件2401); INSERT INTO teacher (user_id, teacher_no, name, title) VALUES (3, T001, 李老师, 副教授); INSERT INTO course (name, teacher_id, credit, capacity, weekday, slot, classroom) VALUES (Java程序设计, 1, 3.0, 40, 1, 3, A101), (数据库原理, 1, 2.5, 30, 2, 5, B203);注意初始化数据里密码用了 MD5(123456)这是 MySQL 内置函数登录程序里也要对输入做相同处理两边算法保持一致。数据库里存明文密码这件事很多课设答辩会被直接问住提前用哈希至少说明你有安全意识。3.3 三个必会查询可选课程、是否已选、选课人数统计选课系统最常用的三条 SQL也是 Mysql 数据库常用命令里值得重点掌握的三条多表 JOIN、聚合统计、条件过滤。第一条用于学生端课程列表SELECT c.id, c.name, t.name AS teacher_name, c.credit, c.capacity, c.selected_count, c.capacity - c.selected_count AS remain FROM course c LEFT JOIN teacher t ON c.teacher_id t.id WHERE c.selected_count c.capacity ORDER BY c.id;LEFT JOIN 保证即使 teacher 信息缺失课程也能显示出来。WHERE 里过滤掉已满课程减轻前端判断压力。但这里要明确一点查询结果里不显示满课并不代表恶意请求不能直接带 courseId 来选课所以容量校验必须在后端再做一次这就是下一章选课 Servlet 要做的事。第二条用于学生点击选课前判断是否已选SELECT COUNT(*) FROM course_selection WHERE student_id ? AND course_id ?;这个查询只是辅助提示不能单独指望它防重复选课因为两个请求同时通过 COUNT0 的判断后还是会并发插入两条记录。真正兜底是 course_selection 表上的唯一索引 uk_student_course数据库会拒绝第二个插入。第三条用于教师端查看每门课的选课人数SELECT c.id, c.name, COUNT(cs.id) AS cnt FROM course c LEFT JOIN course_selection cs ON c.id cs.course_id GROUP BY c.id, c.name;这里用了 LEFT JOIN 而不是 INNER JOIN原因很实际新开的课程可能还没有人选INNER JOIN 会让这门课从统计结果里消失教师端会以为课程没创建成功。GROUP BY 里要把 c.id 和 c.name 都带上MySQL 的 ONLY_FULL_GROUP_BY 模式在 5.7.5 之后默认开启漏写会直接报错。4. 核心功能实现登录、课程列表和选课三个 Servlet 的完整写法4.1 包结构划分与 DBUtil 封装代码按三层职责分包答辩时也好讲bean 放实体类dao 放 JDBC 操作servlet 放请求入口filter 放拦截逻辑util 放工具类。src/com/course ├── bean User、Student、Teacher、Course、Selection ├── dao UserDao、CourseDao、SelectionDao ├── servlet LoginServlet、CourseListServlet、SelectCourseServlet ├── filter EncodingFilter、LoginFilter └── util DBUtil、MD5UtilDBUtil 负责统一获取数据库连接避免每个 DAO 都重复写一遍 DriverManager 样板代码。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/course_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码的静态块是关键Class.forName 只需要执行一次放在 static 里保证整个应用生命周期内只注册一次驱动避免每次取连接都重复加载。密码直接写在代码里属于硬编码课设可以接受但要能说出改进方向把连接参数放到 src 下的 jdbc.properties用 Properties 读取。答辩问到“安全问题”时这个答案就是加分项。4.2 登录逻辑session 存用户LoginFilter 拦未登录请求登录流程分四步接收用户名密码、查 user 表、把用户信息放进 session、按角色跳转。下面这段 LoginServlet 是完整可运行的版本WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password MD5Util.md5(request.getParameter(password)); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp?error1); return; } HttpSession session request.getSession(); session.setAttribute(loginUser, user); if (student.equals(user.getRole())) { Student student new StudentDao().findByUserId(user.getId()); session.setAttribute(studentInfo, student); response.sendRedirect(request.getContextPath() /student/home.jsp); } else if (teacher.equals(user.getRole())) { Teacher teacher new TeacherDao().findByUserId(user.getId()); session.setAttribute(teacherInfo, teacher); response.sendRedirect(request.getContextPath() /teacher/home.jsp); } else { response.sendRedirect(request.getContextPath() /admin/home.jsp); } } }逻辑说明密码在进入查询前先做 MD5数据库中存的也是 MD5 值这样即使数据库泄露拿到的也不是明文密码。登录成功后把 user 对象和对应的扩展信息对象都放进 session后续页面想获取当前用户信息时直接 getAttribute 就行。注意 sendRedirect 和 forward 的区别——这里是浏览器重新发出的请求网址会变成目标地址而 forward 是服务器内部跳转地址栏不变面试题里经常拿这个做区分。登录之后必须做未登录拦截。用 LoginFilter 过滤所有页面请求检查 session 中是否存在 loginUser不存在就重定向回登录页。这个 Filter 建议放到所有需要限制访问的路径上比如 /student/、/teacher/、/admin/*而 login.jsp 和 /login 本身要放行否则连登录页都被拦死。4.3 选课逻辑用一条原子 UPDATE 占座防止超卖新手最常见的选课写法是先查余量、判断大于 0、再 UPDATE这在单机测试时表现正常一旦两个同学同时提交就会翻车。两个请求都读到余量 1都判断“还有名额”然后都执行 UPDATE最终 selected_count 变成 2超过 capacity。正确做法是把判断和更新压缩成一条 SQL让数据库用行锁来保证原子性。public int occupySeat(int courseId) { String sql UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; return executeUpdate(sql, courseId); }执行这条 SQL 时MySQL 会先对 course 表中对应行加锁再比较 selected_count 和 capacity只有余量大于 0 才更新并返回受影响行数。返回 1 表示占座成功返回 0 说明课程已满不需要再走插入逻辑。这种写法在并发下不会有先后读取旧值的问题因为第二个请求会等第一个提交后重新读取最新数据。SelectCourseServlet 完整流程WebServlet(/selectCourse) public class SelectCourseServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); int courseId Integer.parseInt(request.getParameter(courseId)); Student student (Student) request.getSession().getAttribute(studentInfo); if (student null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } CourseDao courseDao new CourseDao(); SelectionDao selectionDao new SelectionDao(); if (selectionDao.exists(student.getId(), courseId)) { request.setAttribute(msg, 你已经选过这门课了); request.getRequestDispatcher(/student/selectCourse.jsp).forward(request, response); return; } int rows courseDao.occupySeat(courseId); if (rows 0) { request.setAttribute(msg, 课程已满); request.getRequestDispatcher(/student/selectCourse.jsp).forward(request, response); return; } try { selectionDao.insert(student.getId(), courseId); response.sendRedirect(request.getContextPath() /student/courseList); } catch (SQLException e) { courseDao.releaseSeat(courseId); throw new ServletException(选课失败, e); } } }逻辑说明先调用 exists 判断是否重复选课这只是第一道检查最终防线是数据库唯一索引然后调用 occupySeat 占座这一步能锁住课程行并发下不会超卖占座成功后再插入选课记录。如果插入失败比如唯一索引冲突说明有人比你更早选了同一门课此时要把刚才占掉的座位归还否则会出现“没选上课但余量少了”的脏数据。归还坐位的方法就是反向 UPDATEpublic void releaseSeat(int courseId) { String sql UPDATE course SET selected_count selected_count - 1 WHERE id ?; executeUpdate(sql, courseId); }占座之后再插记录中间有一个时间窗口事务可以解决得更彻底但课设阶段这套写法已经能挡住最典型的并发翻车且能在答辩时讲清楚行锁的概念。4.4 JSP 渲染课程列表用 JSTL 而不是 % % 脚本课程列表页需要先有一个 Servlet 把数据准备好再转发给 JSP 渲染。CourseListServlet 只负责查数据库、存 request、转发页面三件事WebServlet(/student/courseList) public class CourseListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListCourse courseList new CourseDao().findAvailableForStudent(); request.setAttribute(courseList, courseList); request.getRequestDispatcher(/student/courseList.jsp).forward(request, response); } }JSP 页面里不要写 % % 脚本片段用 JSTL 和 EL 表达式页面干净、逻辑清晰答辩也更好看。下面是课程列表的核心片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % ... c:forEach items${courseList} varc tr td${c.name}/td td${c.teacherName}/td td${c.credit}/td td${c.selectedCount} / ${c.capacity}/td td form action${pageContext.request.contextPath}/selectCourse methodpost input typehidden namecourseId value${c.id} button typesubmit选课/button /form /td /tr /c:forEach三个常见问题提前避掉第一taglib 的 uri 要写对JSTL 1.2 的 core 就是这个地址抄错会导致标签不解析第二form 的 action 必须用 ${pageContext.request.contextPath} 拼前缀直接写死 /course_system 在换部署路径时全部失效第三${c.teacherName} 取的是 Course 类里 getTeacherName() 方法的返回值JavaBean 属性命名不规范会导致 EL 取不到值。JSTL 的 jar 包要放进 WEB-INF/lib和 mysql 驱动放同一个目录。如果漏放页面会直接报 500或者标签原样输出在浏览器里。这个坑在下面第 5 章还会专门提到。5. 避坑乱码、驱动、404、超卖与 JSP 不生效的排查清单5.1 中文乱码四层编码链哪一层断了都会翻车现象页面显示正常但插入 MySQL 后中文全部变成问号或者 JSP 上直接出现 é 这样的乱码。原因乱码不是玄学而是编码链断了。请求参数编码、响应输出编码、JDBC URL 字符集、数据库表字符集这四层里任何一层不一致中文就走不到终点。最常见的是只设置了页面编码没处理请求编码导致 POST 表单里的中文一进 Servlet 就乱了。解决最省事的方法是写一个过滤器把请求和响应编码统一成 UTF-8WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); res.setCharacterEncoding(UTF-8); res.setContentType(text/html; charsetUTF-8); chain.doFilter(req, res); } }过滤器只管请求和响应数据库侧要确认两件事建表时用了 utf8mb4JDBC URL 里带了 characterEncodingutf8。四个环节都对齐后中文乱码基本绝迹。另外 GET 请求传中文参数时Tomcat 默认按 ISO-8859-1 解码需要在 server.xml 的 Connector 上加 URIEncodingUTF-8。过滤器解决 POSTURIEncoding 解决 GET两个都校验一遍才叫完整。5.2 驱动找不到或连不上先跑 JdbcCheck 再查 webapp现象启动后访问某个 Servlet 报 ClassNotFoundException: com.mysql.cj.jdbc.Driver或者报 Communications link failure。原因绝大多数是驱动 jar 没进 WEB-INF/lib或者 jar 虽然在了但驱动类名写错。也有少量情况是 MySQL 服务没启动、密码不对、时区参数缺失。Tomcat 9 的 javax.servlet 和 Tomcat 10 的 jakarta.servlet 包名不兼容是老项目换容器时最容易踩的坑。解决回到第 2 章的 JdbcCheck独立跑一次。能跑通说明环境没问题问题在 webapp 部署或 jar 位置跑不通就依次查 MySQL 服务、密码、URL 参数。这里有个血泪经验改代码前先看 Tomcat 的 logs/catalina.out日志末尾的 Caused by 才是真正的根因不要盯着前几行报错信息猜。5.3 404 与 500部署路径、注解映射和 Tomcat 日志现象访问 http://localhost:8080/course_system/login.jsp 直接 404或者表单提交到 Servlet 后返回 500。原因404 多数是 IDEA 部署里的 Application context 和实际访问前缀不一致或者 JSP 放错了目录。JSP 必须放在 webapp 根目录或子目录下放进 WEB-INF 里直接访问会 404因为 WEB-INF 下的资源受容器保护只能通过 Servlet forward 访问。500 的常见原因则是 WebServlet 注解路径写错、类名和 URL 不匹配或者 JSP 里 EL 表达式取不到属性。解决先分清是静态资源访问失败还是动态逻辑失败。直接访问 JSP 404查 Deployment 里的 context path提交表单后 404查 WebServlet(/xxx) 里的路径是否和 form action 一致进入页面后 500看 Tomcat 的 localhost.2024-xx-x.log 日志文件拖到最底部找 Caused by。Tomcat 日志是最后的黑匣子看不懂完整日志就只看 Caused by 那一行多半能定位。5.4 选课超卖重复提交与并发写为什么必须用原子 UPDATE现象最后 1 个名额两个浏览器同时点击选课结果课程表里 selected_count 超出 capacity或者同一个学生的选课记录出现了两条。原因本质上是 check-then-act 竞态条件。先查余量、判断、再更新三步之间数据可能已经被别人改了。前端重复点击也是诱因但前端只是体验问题真正的并发控制必须放在数据库层。解决第 4 章的 occupySeat 写法把“余量 0”的判断直接写进 UPDATE 的 WHERE 条件DB 的行锁保证同一时刻只有一个请求能更新成功。再配合 course_selection 的唯一索引数据库层彻底拦截同一学生对同一课程的重复记录。最后可以给选课按钮加一个 JS 的 disabled点击后置灰防止手抖连点。但要记住前端 disabled 只是体验优化不是安全机制后端校验必须以数据库的行为为准。5.5 JSP 改了不生效work 目录缓存和浏览器缓存的组合拳现象改了一个 JSP 的标题刷新页面还是旧内容重启 Tomcat 也没用。原因JSP 第一次被访问时会被容器编译成 class 文件缓存在 Tomcat 的 work/Catalina/localhost 目录里。IDEA 部署时如果没有自动清缓存旧 class 就会一直生效。浏览器这边也可能把 JSP 响应结果当成静态资源缓存下来。解决开发期把 IDEA 的 On frame deactivation 设为 Update classes and resources让修改的类和资源自动同步遇到顽固情况直接手动删除 Tomcat 的 work 目录后重启JSP 会强制重新编译。浏览器端用 CtrlF5 强制刷新排除本地缓存的干扰。通常“改了没生效”的问题清 work 强刷两件套能解决八成。6. 答辩前的三个小优化事务、密码加盐和可复现的验证清单第一个优化是给选课流程加事务。当前代码里 occupySeat 和 selectionDao.insert 各自拿连接、各自提交一旦插入失败归还座位的那条 SQL 和前面的操作不在同一个事务里极端情况下会出现数据不一致。常见做法是在 Servlet 里手动控制把连接从 DBUtil 取出来后 setAutoCommit(false)两条 SQL 执行成功后 commit任何一步异常就 rollback。关键点是连接要由调用方传入 DAO 方法而不是在 DAO 内部 new 连接否则两个 DAO 用的是不同事务commit 各管各的。第二个优化是密码加盐。现在的 MD5Util.md5 只是对密码本身做哈希张三和李四若密码相同哈希值也一样。改进方法是注册时生成一个随机盐值比如 8 位字符串存进 user 表新增的 salt 列密码存成 md5(password salt)。登录校验时先查出 salt再做拼接哈希比对。加了盐之后即使数据库中两个账号的密码哈希相同也看不出明文规律这一项改动量不大但安全维度上的加分很明显。第三个优化是一个可复现的验证清单。答辩演示之前按顺序过一遍核心链路学生登录、查看课程列表、选课、查看已选课程、退课再用教师账号登录查看选课名单最后管理员登录确认课程维护正常。每步写一个预期结果例如“选课后列表里余量减 1”“再次选同一门课提示已选”。我第一次演示就翻过车上午改了一个字段下午登录直接 404原因是某台演示机没有重新部署。后来每次改完代码先按清单跑一遍再上投影就再没出过同类问题。这套 JSPServletMySQL 的选课系统技术含量不在“新”字而在你能把一条请求从 URL 走到 MySQL 再走回来的每一层都解释明白。能答清楚三层架构的边界、能说明白原子 UPDATE 防超卖、能处理乱码和部署问题这个项目就是你简历里拿得出手的 Java Web 实践。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑