简介:本资源是一套基于Java Web技术栈开发的完整学生选课管理系统实战项目,面向Java初学者与Web开发入门学习者,聚焦MVC架构实践、前后端交互及数据库集成能力训练。系统采用Java语言编写业务逻辑,JSP实现动态页面展示,Servlet处理HTTP请求与流程控制,MySQL存储学生、课程、选课记录等核心数据,并通过JDBC完成持久化操作,覆盖用户登录鉴权、课程管理、选课冲突校验、成绩录入与查询等典型教务功能。压缩包共408个文件,含43个Java源码、25个JSP页面、50个JS脚本、43个Class编译文件、15个CSS样式表及配套SQL、XML、properties配置文件等,结构清晰,便于分层理解与调试,整体大小为6.59MB。已有6905人学习下载,提供可直接部署运行的完整工程,含备份文件(.bak)与典型DAO/Servlet实现类,是掌握Java Web基础开发流程与项目组织规范的优质练手案例。
1. 这不是“过时技术堆砌”,而是一套能跑通、能调试、能交毕设、能改出真实业务逻辑的 Java Web 基础栈闭环
你搜“JSP 学生选课系统”,十有八九点开的是 GitHub 上 2016 年上传、README 里写着“仅供学习参考”、连 Tomcat 版本都没标清楚的压缩包——解压后web.xml里还配着<servlet-class>com.servlet.AddCourseServlet</servlet-class>,但 src 目录下根本没这个类;MySQL 表结构脚本里字段全用VARCHAR(255)硬扛学号、成绩、学期,插入2024-2就报错;更别说 IDEA 里右键 Run on Server 提示 “No servlet container configured”,或者浏览器打开 localhost:8080 直接 404——连 index.jsp 都找不到。这不是你环境没配好,是整套资源从设计第一天就没考虑过“在 JDK 17 + Tomcat 10.1 + MySQL 8.0 下能否真正跑起来”。而这份「Java+JSP+Servlet+MySQL 实现 Web 学生选课管理系统」,是我去年带三届毕设学生复现、逐行 debug、重写 DAO 层、补全事务控制、适配 MySQL 8.0 认证插件后沉淀下来的可执行最小闭环:它包含完整可运行的 7 张表(student、course、teacher、selection、grade、department、admin),所有 Servlet 均继承HttpServlet并显式覆盖doGet/doPost,JSP 页面用 JSTL 替代 scriptlet(<% %>),MySQL 连接字符串强制启用useSSL=false&serverTimezone=Asia/Shanghai,且每个关键操作(如选课冲突检测、成绩录入校验、管理员批量导入)都附带单元测试入口和日志埋点。适合正在赶毕设 deadline 的大三学生、需要快速搭建教学演示环境的实训教师,以及想亲手摸透“请求怎么从浏览器进 Servlet、数据怎么经 JDBC 落库、页面怎么用 EL 表达式取值”的 Java Web 入门者——它不炫技,但每一步都能 debug 到栈帧,每一处报错都有明确修复路径。
2. 从零启动:环境链路必须对齐 JDK/Tomcat/MySQL 三版本坐标
这套系统不是“写完就能跑”,它的可运行性高度依赖三端版本协同。我见过太多同学卡在第一步:IDEA 新建项目选了 “Web Application” 模板,却用 JDK 17 编译,Tomcat 9 启动时报java.lang.UnsupportedClassVersionError;或者 MySQL 8.0 默认启用caching_sha2_password插件,而旧版 JDBC 驱动不兼容,导致Connection refused却误以为是端口没开。下面这三步,缺一不可,且顺序不能乱。
2.1 JDK 8u291 或 JDK 11:避免字节码与容器不兼容的玄学翻车
Tomcat 9 及以下版本官方仅支持到 JDK 11,Tomcat 10.1+ 才正式支持 JDK 17 的新特性(如 sealed classes)。而本系统 Servlet 接口调用方式、web.xml DTD 声明均基于 Servlet 4.0 规范(对应 Tomcat 9.0.x),强行用 JDK 17 编译会导致javax.servlet包无法解析。实测安全组合是 JDK 8u291(长期支持版) + Tomcat 9.0.83 + MySQL 8.0.33。安装后验证:
# 终端执行,确认输出为 1.8 或 11 java -version # 检查 JAVA_HOME 是否指向正确路径(尤其 Windows 用户注意路径含空格时需加引号) echo $JAVA_HOME # Linux/macOS echo %JAVA_HOME% # Windows提示:若已装 JDK 17,请勿卸载,而是通过 IDE 切换 Project SDK。在 IntelliJ IDEA 中:File → Project Structure → Project → Project SDK → 选择已安装的 JDK 8 或 11;同时 Settings → Build → Compiler → Java Compiler → Target bytecode version 也设为 1.8。
2.2 Tomcat 9.0.83:手动部署 WAR 包而非依赖 IDE 自动配置
IDEA 的 “Run on Server” 功能常因自动添加额外 context path 或忽略 web.xml 中的<welcome-file-list>导致首页 404。强烈建议采用纯手工部署:
- 下载 Tomcat 9.0.83 (注意是
bin目录下的 zip,非源码包); - 解压到无中文、无空格路径(如
/opt/tomcat或D:\tomcat9); - 将本系统导出的
student-selection.war文件(非整个 project 文件夹!)直接复制到$TOMCAT_HOME/webapps/目录下; - 启动 Tomcat:Linux/macOS 执行
$TOMCAT_HOME/bin/startup.sh,Windows 执行$TOMCAT_HOME/bin/startup.bat; - 观察
$TOMCAT_HOME/logs/catalina.out是否出现INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [...] has finished in [...] ms字样,确认 WAR 包解压成功。
此时访问http://localhost:8080/student-selection/(注意末尾斜杠),应看到登录页。若提示 404,请检查webapps/student-selection/WEB-INF/web.xml是否存在,且<welcome-file-list>中<welcome-file>值为index.jsp。
2.3 MySQL 8.0.33:必须显式配置时区与 SSL 关闭
MySQL 8.0 默认启用caching_sha2_password认证插件,而本系统使用的mysql-connector-java-5.1.47.jar(经典驱动)不支持该插件。解决方案不是降级 MySQL,而是在连接 URL 中强制指定参数:
# jdbc.properties 文件中配置(路径:src/main/resources/jdbc.properties) jdbc.url=jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=your_password关键参数说明:
useSSL=false:关闭 SSL(开发环境无需加密,开启反而因证书缺失报错);serverTimezone=Asia/Shanghai:解决The server time zone value 'XXX' is unrecognized错误;characterEncoding=UTF-8:确保中文姓名、课程名不乱码;useUnicode=true:配合上一条生效。
注意:若使用 Navicat 或 DBeaver 连接 MySQL 8.0,需在连接属性中将 “Authentication Plugin” 改为
mysql_native_password,否则客户端登录失败。命令行可执行:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;
3. 代码层拆解:Servlet 生命周期与 JDBC 操作的真实映射关系
很多教程把 Servlet 写成“接收参数→调用 DAO→跳转页面”的黑匣子,却不说清doGet()和doPost()为何要分开处理、request.setAttribute()怎么被 JSP 的${}拿到、DAO 层为什么不能直接new Connection()。本系统将这些隐含逻辑全部暴露出来,下面以“学生选课”核心流程为例,逐层解析。
3.1 Servlet 分发逻辑:GET 查列表,POST 提交动作,绝不混用
SelectCourseServlet.java是典型双态 Servlet:
// src/main/java/com/servlet/SelectCourseServlet.java public class SelectCourseServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // GET 请求:展示可选课程列表(查询 course 表) List<Course> courseList = new CourseDAO().findAllCourses(); request.setAttribute("courseList", courseList); // 数据存入 request 域 request.getRequestDispatcher("/student/select_course.jsp").forward(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // POST 请求:处理选课提交(插入 selection 表) String studentId = request.getParameter("studentId"); String courseId = request.getParameter("courseId"); // 业务校验:是否已选?是否满员? if (new SelectionDAO().isAlreadySelected(studentId, courseId)) { request.setAttribute("error", "您已选修该课程!"); request.getRequestDispatcher("/student/select_course.jsp").forward(request, response); return; } if (!new CourseDAO().hasVacancy(courseId)) { request.setAttribute("error", "该课程名额已满!"); request.getRequestDispatcher("/student/select_course.jsp").forward(request, response); return; } // 执行插入 boolean success = new SelectionDAO().selectCourse(studentId, courseId); request.setAttribute("message", success ? "选课成功!" : "选课失败,请重试"); request.getRequestDispatcher("/student/select_result.jsp").forward(request, response); } }关键点说明:
doGet()只做查询和页面跳转,不修改数据库;doPost()处理表单提交,执行 INSERT/UPDATE;这是 RESTful 设计原则,也是防止刷新页面重复提交的基石;request.setAttribute()设置的属性,仅在本次请求生命周期内有效(即 forward 到 JSP 时可用),JSP 中通过${courseList}直接访问;- 校验逻辑放在 Servlet 层而非前端 JS,因为 JS 可被禁用或绕过,服务端校验是最后一道防线。
3.2 DAO 层事务控制:Connection 不是每次 new,而是统一管理
BaseDAO.java定义了连接获取与释放的模板:
// src/main/java/com/dao/BaseDAO.java public abstract class BaseDAO { private static final String DB_URL = "jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String DB_USER = "root"; private static final String DB_PASS = "your_password"; protected Connection getConnection() throws SQLException { return DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); } protected void closeResources(Connection conn, PreparedStatement pstmt, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } if (pstmt != null) try { pstmt.close(); } catch (SQLException e) { e.printStackTrace(); } if (conn != null) try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }而具体 DAO 如SelectionDAO.java则复用此连接:
// src/main/java/com/dao/SelectionDAO.java public class SelectionDAO extends BaseDAO { public boolean selectCourse(String studentId, String courseId) { String sql = "INSERT INTO selection (student_id, course_id, selection_time) VALUES (?, ?, NOW())"; try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, studentId); pstmt.setString(2, courseId); return pstmt.executeUpdate() > 0; // executeUpdate 返回影响行数 } catch (SQLException e) { e.printStackTrace(); return false; } } }为什么不用连接池?
本系统定位教学闭环,刻意省略 HikariCP 等第三方池化组件,让初学者看清Connection从创建到关闭的完整链路。实际生产环境必须替换为连接池,否则高并发下会耗尽数据库连接。
3.3 JSP 页面渲染:EL 表达式 + JSTL 标签替代 scriptlet
select_course.jsp中不再出现<% while(rs.next()) { %>这类易出错的 Java 脚本,而是用标准标签:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>选课页面</title></head> <body> <h2>可选课程列表</h2> <c:if test="${not empty error}"> <div style="color:red">${error}</div> </c:if> <table border="1"> <tr><th>课程编号</th><th>课程名称</th><th>学分</th><th>操作</th></tr> <c:forEach items="${courseList}" var="course"> <tr> <td>${course.courseId}</td> <td>${course.courseName}</td> <td>${course.credits}</td> <td> <form action="SelectCourseServlet" method="post"> <input type="hidden" name="studentId" value="${sessionScope.student.studentId}"> <input type="hidden" name="courseId" value="${course.courseId}"> <button type="submit">选课</button> </form> </td> </tr> </c:forEach> </table> </body> </html>JSTL 使用前提:
webapp/WEB-INF/lib/下必须有jstl-1.2.jar和standard-1.1.2.jar(本系统已内置);<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>声明不可少;${sessionScope.student.studentId}明确指定作用域,避免pageContext与request混淆。
4. 避坑指南:五个血泪经验总结,专治“明明代码一样却跑不通”
这套系统最大的价值,不是功能多炫,而是把新手必踩的坑提前挖出来、标清楚、给解法。以下五条,每一条都来自真实 debug 场景,不是理论推测。
4.1 现象:启动 Tomcat 后访问http://localhost:8080/student-selection/报 404,但http://localhost:8080/能看到 Tomcat 欢迎页
原因:WAR 包未正确解压,或web.xml中<welcome-file-list>配置错误。常见于 IDEA 导出 WAR 时勾选了 “include libraries in the WAR” 导致包体过大解压失败,或web.xml里<welcome-file>写成了index.html而实际文件是index.jsp。
解决:
- 进入
$TOMCAT_HOME/webapps/,确认存在student-selection/文件夹(而非student-selection.war文件残留); - 检查
student-selection/WEB-INF/web.xml,确认<welcome-file-list>内容为:
<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>- 若仍 404,临时将
index.jsp复制一份命名为index.html,测试是否是 JSP 引擎问题(Tomcat 默认支持 JSP,此步仅用于排除)。
4.2 现象:登录成功后跳转到student_home.jsp,页面显示${student.name}为空白,但数据库里姓名字段有值
原因:Servlet 中request.setAttribute("student", student)的 key 与 JSP 中${student.name}的变量名不匹配,或student对象的getName()方法返回 null。
解决:
- 在 Servlet 的
doPost()登录逻辑中,打印System.out.println("Login success, student name: " + student.getName());确认对象属性已加载; - 检查
Student.java的 getter 方法是否为public String getName()(注意大小写,JSP EL 严格匹配驼峰命名); - JSP 中
${student.name}的student必须与setAttribute("student", ...)的第一个参数完全一致(区分大小写)。
4.3 现象:MySQL 插入中文时显示???,如 “张三” 变成 “??”
原因:MySQL 服务端、数据库、表、连接 URL 四层字符集未统一为 UTF8MB4。
解决:
- 登录 MySQL,执行
SHOW VARIABLES LIKE 'character_set%';,确认character_set_server为utf8mb4; - 创建数据库时指定字符集:
CREATE DATABASE student_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;; - 修改表字符集:
ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;; - 连接 URL 中必须含
useUnicode=true&characterEncoding=UTF-8(注意是UTF-8,不是utf8)。
4.4 现象:点击“选课”按钮后页面空白,控制台无报错,但数据库无新增记录
原因:表单action属性指向错误的 Servlet URL,或web.xml中<servlet-mapping>的url-pattern与实际请求路径不匹配。
解决:
- 浏览器按 F12 打开 Network 标签,点击选课按钮,查看发出的 POST 请求 URL(如
http://localhost:8080/student-selection/SelectCourseServlet); - 对照
web.xml中的配置:
<servlet> <servlet-name>SelectCourseServlet</servlet-name> <servlet-class>com.servlet.SelectCourseServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>SelectCourseServlet</servlet-name> <url-pattern>/SelectCourseServlet</url-pattern> </servlet-mapping>- 确保表单
action="/SelectCourseServlet"(相对路径)或action="SelectCourseServlet"(同目录下),而非action="selectCourse"或绝对路径漏掉上下文名。
4.5 现象:ClassNotFoundException: com.mysql.jdbc.Driver
原因:MySQL 8.0+ 驱动类名已从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,但代码中仍用旧类名。
解决:
- 检查
src/main/java/com/dao/BaseDAO.java中Class.forName("com.mysql.jdbc.Driver")是否存在; - 删除该行(JDBC 4.0+ 规范要求驱动自动注册,无需显式加载);
- 确保
webapp/WEB-INF/lib/下的 MySQL 驱动 JAR 是mysql-connector-java-8.0.33.jar(本系统已提供),而非 5.x 版本。
5. 进阶验证:用 Postman 模拟请求 + 日志埋点定位真实瓶颈
当系统跑通基础功能后,下一步不是加新模块,而是建立可验证的观测能力。很多同学改完代码不敢提交,因为不知道“改对没”,只能靠反复点页面猜。我习惯用两个低成本手段构建信心:Postman 发送原始 HTTP 请求验证 Servlet 接口契约,以及在 DAO 层加System.out.println()输出 SQL 执行耗时。
5.1 Postman 验证 Servlet 接口:绕过前端,直击后端逻辑
以“管理员添加课程”为例,其 ServletAddCourseServlet接收 POST 请求,参数为courseId,courseName,credits。在 Postman 中新建请求:
- Method:
POST - URL:
http://localhost:8080/student-selection/AddCourseServlet - Body → x-www-form-urlencoded:
Key Value courseId CS202401 courseName 数据结构 credits 3
发送后,观察响应 Body 是否为{"status":"success","message":"课程添加成功"}(本系统 JSON 响应由response.getWriter().write(jsonString)输出)。若返回 404,说明web.xml中AddCourseServlet的 mapping 未生效;若返回 500,则检查 Tomcat logs 中 stack trace,定位是空指针还是 SQL 语法错误。
为什么不用浏览器表单?
浏览器表单提交会触发页面跳转,掩盖了服务器真实响应;Postman 显示原始响应体,且可保存请求模板,下次测试只需改参数,效率提升 3 倍以上。
5.2 DAO 层 SQL 执行耗时埋点:一行代码揪出慢查询
在BaseDAO.java的closeResources()方法前,加入执行时间统计:
protected void logExecutionTime(long startTime, String sql) { long duration = System.currentTimeMillis() - startTime; if (duration > 100) { // 超过 100ms 认为慢查询 System.out.println("[SLOW QUERY] " + sql + " took " + duration + "ms"); } }然后在CourseDAO.java的findAllCourses()方法中调用:
public List<Course> findAllCourses() { long startTime = System.currentTimeMillis(); String sql = "SELECT * FROM course ORDER BY course_id"; // ... 执行查询 ... logExecutionTime(startTime, sql); return courseList; }启动系统后,选课页面加载时,控制台会输出类似:[SLOW QUERY] SELECT * FROM course ORDER BY course_id took 128ms
此时立刻去 MySQL 执行EXPLAIN SELECT * FROM course ORDER BY course_id;,发现type为ALL(全表扫描),说明course_id缺少索引。执行ALTER TABLE course ADD INDEX idx_course_id (course_id);即可优化。
5.3 毕设答辩必备:三份可展示材料清单
作为过来人,我深知答辩时老师最常问“你怎么证明这个系统真的能用”。光说“我跑过了”没说服力,必须准备三份材料:
- 数据库截图:Navicat 中
student_db库的 7 张表结构(含字段类型、主键、外键)、以及selection表的几条真实选课记录(证明数据写入); - Tomcat 日志片段:
catalina.out中包含Deployment of web application archive [...] has finished和SELECT * FROM student WHERE student_id=?的日志行(证明部署成功且 SQL 执行); - Postman 测试报告:导出 Postman Collection 的 JSON 文件,或截图展示
AddCourseServlet、SelectCourseServlet、GradeQueryServlet三个关键接口的 200 响应体(证明接口契约成立)。
从那以后我每次交付毕设系统,都强制走一遍 Postman 全接口测试 + 日志耗时分析 + 数据库快照留存。不是怕出错,而是让“能跑”变成“可证”,让答辩时的每一句“老师您看这里……”都有据可依。希望帮到你。
本文还有配套的精品资源,点击获取