资讯详情

JSP+Servlet+MySQL学生选课系统实战(教学级可运行版)

📅 2026/9/12 22:03:31 | 华诺云谱 👁 阅读
JSP+Servlet+MySQL学生选课系统实战(教学级可运行版)
简介这是一套基于Java Web技术栈开发的完整学生选课管理系统源码面向计算机专业本科生、Java初学者及Web开发入门者解决课程管理、角色权限控制与学分统计等典型教务场景需求。系统采用JSPServletMySQL三层架构支持教师管理学生/课程/选课、设置学分上下限和学生选课、查课、修改信息双角色登录功能完整且经实测可稳定运行。资源包共105个文件含25个Java业务类、25个Class编译文件、15个JSP页面、19个界面截图PNG、2个CSS样式表、1个SQL建库脚本、1个Word论文及1个MP4操作视频教程总大小108.89MB结构清晰、模块划分明确。已有4701人学习下载配套提供数据库导入指南、Eclipse项目导入说明、高清运行演示视频及完整毕业论文覆盖从环境搭建、代码调试到功能验证的全流程学习支撑。1. 为什么还在用 JSP Servlet MySQL 做学生选课管理系统不是过时了而是它仍在真实教学场景中“扛大梁”很多刚学完 Java Web 基础的开发者看到这个标题会下意识皱眉“JSP 都淘汰了吧现在不都 Spring Boot”——但现实是全国超 70% 的高校《Java 程序设计》《Web 应用开发》课程实验、课程设计、毕业设计仍明确要求使用原生 JSP Servlet MySQL技术栈。原因很实在不引入 Spring 容器、不依赖注解扫描、不抽象 MVC 层所有请求流转、页面跳转、数据库交互都暴露在代码里学生能真正看懂request.getParameter()怎么拿到表单值、response.sendRedirect()和RequestDispatcher.forward()的本质区别、Connection.prepareStatement()如何防 SQL 注入。这套系统不是“古董”而是教学级 Web 开发的“解剖标本”。它不追求高并发或微服务架构但必须做到多角色管理员/教师/学生权限隔离清晰、选课退课事务强一致、MySQL 表结构符合第三范式、JSP 页面无脚本片段硬编码、Servlet 层无业务逻辑堆积。本文就带你从零搭起一个可直接导入 Eclipse/IDEA、无需改包名、MySQL 5.7 兼容、登录后自动跳转对应首页、所有按钮均有响应、无 404/500 错误的完整可运行版本。2. 用 JSP Servlet MySQL 搭建学生选课系统的核心技术选型与分层依据2.1 为什么坚持不用框架三层结构如何手动落地在 Spring Boot 成为默认选项的今天坚持用原生 Servlet 并非守旧而是教学目标倒逼的技术选择。Spring Boot 的自动配置会隐藏web.xml加载顺序、ServletContextListener初始化时机、Filter链执行路径等关键机制而这些恰恰是理解 Web 容器生命周期的必经之路。我们采用经典的MVC 手动分层View 层JSP仅负责数据展示与简单流程控制如c:if test${user.role student}禁止写 Java 脚本% %所有动态内容通过 EL 表达式${}和 JSTL 标签库输出Controller 层Servlet每个功能点一个 Servlet如LoginServlet、SelectCourseServlet统一继承自HttpServletdoPost()处理表单提交doGet()处理页面跳转绝不混用Model 层JavaBean DAOStudent.java、Course.java等 POJO 类仅含属性与 getter/setterStudentDAO.java封装所有 JDBC 操作使用PreparedStatement防注入连接池选用DBCP2轻量、无额外依赖、教学环境稳定。提示不使用 Hibernate 或 MyBatis 是因为它们会掩盖 SQL 执行细节。学生必须亲手写INSERT INTO student (name, gender) VALUES (?, ?)并理解executeUpdate()返回值含义。2.2 MySQL 数据库设计从 ER 图到可执行建表语句学生选课系统的实体关系非常典型学生Student、课程Course、教师Teacher、选课记录Selection。核心约束有三① 一名学生可选多门课一门课可被多名学生选 → 多对多关系需中间表selection② 教师开设课程一门课仅由一名教师负责 → 一对多course.teacher_id外键指向teacher.id③ 角色字段必须枚举化避免字符串硬编码导致权限错乱。以下是可直接在 MySQL 中执行的建表语句兼容 MySQL 5.7已亲测无语法错误-- 创建数据库字符集强制 utf8mb4避免中文乱码 CREATE DATABASE IF NOT EXISTS course_selection CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE course_selection; -- 用户表统一管理三类角色 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- 存储 BCrypt 加密后的密文 real_name VARCHAR(30) NOT NULL, role ENUM(admin, teacher, student) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 学生扩展信息表与 user 一对一 CREATE TABLE student ( id INT PRIMARY KEY, major VARCHAR(50), grade INT, FOREIGN KEY (id) REFERENCES user(id) ON DELETE CASCADE ); -- 教师扩展信息表 CREATE TABLE teacher ( id INT PRIMARY KEY, title VARCHAR(20), department VARCHAR(50), FOREIGN KEY (id) REFERENCES user(id) ON DELETE CASCADE ); -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) UNIQUE NOT NULL, -- 课程编号如 CS101 name VARCHAR(100) NOT NULL, credit INT NOT NULL DEFAULT 2, max_capacity INT NOT NULL DEFAULT 60, teacher_id INT NOT NULL, FOREIGN KEY (teacher_id) REFERENCES teacher(id) ); -- 选课记录表核心事务表 CREATE TABLE selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, status ENUM(selected, dropped) DEFAULT selected, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id), -- 防止重复选课 FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE );2.2.1 关键设计说明与教学价值字段/约束为什么这样设教学意义user.role用ENUM而非VARCHAR避免插入非法值如stu、adminn数据库层强制校验让学生理解数据库约束不仅是性能优化更是业务安全第一道防线selection表的联合唯一索引uk_student_course确保同一学生不能重复选择同一门课且删除学生时自动级联清理选课记录直观演示外键ON DELETE CASCADE的实际效果比口头讲解更深刻password字段长度设为 100为 BCrypt 加密后的密文预留空间BCrypt 输出约 60 字符引导学生思考密码存储为何不能用MD5为何要加盐2.3 JSP 页面结构规范从 login.jsp 到 student_home.jsp 的命名与跳转逻辑所有 JSP 页面必须放在WebContent/下的严格目录结构中这是 Tomcat 容器识别资源的基础WebContent/ ├── index.jsp ← 公共入口重定向到登录页 ├── login.jsp ← 统一登录界面含角色选择下拉框 ├── admin/ │ ├── admin_home.jsp ← 管理员首页课程/教师/学生管理入口 │ └── manage_courses.jsp ← 课程管理列表页含增删改按钮 ├── teacher/ │ ├── teacher_home.jsp ← 教师首页所授课程列表、学生成绩录入入口 │ └── course_detail.jsp ← 单门课程详情页显示已选学生名单 └── student/ ├── student_home.jsp ← 学生首页可选课程列表、已选课程列表 └── select_result.jsp ← 选课结果提示页成功/失败/名额已满注意login.jsp中角色选择使用select namerole而非隐藏域因为LoginServlet需根据request.getParameter(role)值决定后续查询哪张扩展表student或teacher这是权限路由的起点。3. 实现多角色登录的核心 Servlet 逻辑与安全控制要点3.1 LoginServlet一次请求完成身份验证、角色识别、会话建立三件事LoginServlet是整个系统的门禁其doPost()方法必须原子性完成三步操作① 查询user表验证账号密码② 根据role字段加载对应扩展信息③ 将用户对象存入HttpSession并重定向至角色专属首页。关键代码如下已去除异常捕获简化逻辑实际项目需补全// LoginServlet.java 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); // 来自 login.jsp 的 select // 1. 查询 user 表使用 PreparedStatement 防注入 String sql SELECT id, username, password, real_name, role FROM user WHERE username ? AND role ?; PreparedStatement ps connection.prepareStatement(sql); ps.setString(1, username); ps.setString(2, role); ResultSet rs ps.executeQuery(); if (rs.next()) { // 2. 验证密码此处用 BCrypt.checkpw非明文比对 if (BCrypt.checkpw(password, rs.getString(password))) { // 3. 构建用户对象含角色特有属性 User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRealName(rs.getString(real_name)); user.setRole(rs.getString(role)); // 根据角色加载扩展信息 if (student.equals(role)) { StudentDAO studentDAO new StudentDAO(); Student student studentDAO.findByUserId(rs.getInt(id)); user.setStudent(student); // User 类中定义 Student student 字段 } else if (teacher.equals(role)) { TeacherDAO teacherDAO new TeacherDAO(); Teacher teacher teacherDAO.findByUserId(rs.getInt(id)); user.setTeacher(teacher); } // 4. 存入 session 并重定向 HttpSession session request.getSession(); session.setAttribute(currentUser, user); // 根据角色跳转不同首页 String redirectPath; switch (role) { case admin: redirectPath /admin/admin_home.jsp; break; case teacher: redirectPath /teacher/teacher_home.jsp; break; case student: redirectPath /student/student_home.jsp; break; default: redirectPath /login.jsp?errorinvalid_role; } response.sendRedirect(request.getContextPath() redirectPath); return; } } // 登录失败返回错误提示 request.setAttribute(error, 用户名、密码或角色不正确); request.getRequestDispatcher(/login.jsp).forward(request, response); }3.1.1 参数说明与教学陷阱解析request.getParameter(role)必须与login.jsp中select namerole的name属性完全一致大小写敏感BCrypt.checkpw(password, hash)密码验证必须使用 BCrypt严禁password.equals(rs.getString(password))—— 这是 Java Web 安全最常考的面试题之一session.setAttribute(currentUser, user)将整个User对象存入 session而非仅存 ID方便后续页面直接取real_name、role等字段response.sendRedirect(...)使用重定向而非转发确保地址栏显示真实角色首页 URL避免用户手动修改 URL 跳转越权页面。3.2 权限拦截 Filter用 web.xml 配置全局访问控制仅靠登录跳转无法防止用户直接在浏览器输入/admin/manage_courses.jsp访问管理员页面。必须添加LoginFilter进行会话校验。在web.xml中配置!-- web.xml -- filter filter-nameLoginFilter/filter-name filter-classcom.example.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/admin/*/url-pattern url-pattern/teacher/*/url-pattern url-pattern/student/*/url-pattern /filter-mappingLoginFilter的doFilter()方法核心逻辑// LoginFilter.java public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); // 不创建新 session // 未登录则重定向到登录页 if (session null || session.getAttribute(currentUser) null) { resp.sendRedirect(req.getContextPath() /login.jsp?errornot_logged_in); return; } // 已登录但访问路径与角色不匹配如学生访问 /admin/ User user (User) session.getAttribute(currentUser); String requestURI req.getRequestURI(); if (requestURI.contains(/admin/) !admin.equals(user.getRole())) { resp.sendRedirect(req.getContextPath() /login.jsp?erroraccess_denied); return; } if (requestURI.contains(/teacher/) !teacher.equals(user.getRole())) { resp.sendRedirect(req.getContextPath() /login.jsp?erroraccess_denied); return; } if (requestURI.contains(/student/) !student.equals(user.getRole())) { resp.sendRedirect(req.getContextPath() /login.jsp?erroraccess_denied); return; } chain.doFilter(request, response); // 放行 }提示web.xml中的url-pattern必须覆盖所有受保护路径漏配一个就会导致越权访问。这是学生调试时最常见的 403 错误根源。4. 学生选课核心功能实现从课程列表展示到事务性选课提交4.1 用 JSTL EL 在 student_home.jsp 中安全展示可选课程student_home.jsp需同时显示两组数据① 当前未选的课程可选列表② 已选的课程已选列表。为避免在 JSP 中写 Java 代码全部交由StudentHomeServlet查询后存入 request// StudentHomeServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); User user (User) session.getAttribute(currentUser); int studentId user.getId(); // user.id 即 student.id因 user.id student.id // 查询可选课程排除已选、未满、状态正常 CourseDAO courseDAO new CourseDAO(); ListCourse availableCourses courseDAO.findAvailableForStudent(studentId); request.setAttribute(availableCourses, availableCourses); // 查询已选课程 SelectionDAO selectionDAO new SelectionDAO(); ListCourse selectedCourses selectionDAO.findSelectedByStudent(studentId); request.setAttribute(selectedCourses, selectedCourses); request.getRequestDispatcher(/student/student_home.jsp).forward(request, response); }对应的student_home.jsp片段使用 JSTLc:forEach循环!-- student_home.jsp -- h3可选课程列表/h3 c:choose c:when test${empty availableCourses} p暂无课程可选/p /c:when c:otherwise table border1 trth课程编号/thth课程名称/thth学分/thth授课教师/thth操作/th/tr c:forEach items${availableCourses} varcourse tr td${course.code}/td td${course.name}/td td${course.credit}/td td${course.teacherName}/td !-- Course 对象中已封装 teacherName -- td form action${pageContext.request.contextPath}/SelectCourseServlet methodpost input typehidden namecourseId value${course.id} button typesubmit选课/button /form /td /tr /c:forEach /table /c:otherwise /c:choose4.1.1 JSTL 使用要点与常见报错必须在 JSP 顶部声明% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %否则c:forEach会报 500 错误items${availableCourses}中的availableCourses必须与request.setAttribute(availableCourses, ...)的 key 完全一致${course.teacherName}要求Course类中存在getTeacherName()方法若字段名为teacher_name则方法名必须是getTeacherName()JavaBean 规范。4.2 SelectCourseServlet用 MySQL 事务保证选课操作的原子性选课不是简单插入一条记录而是涉及三重校验① 该课程是否已满员② 该学生是否已选此课③ 插入selection表是否成功。任何一步失败都必须回滚。SelectCourseServlet的关键事务代码// SelectCourseServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); User user (User) session.getAttribute(currentUser); int studentId user.getId(); int courseId Integer.parseInt(request.getParameter(courseId)); Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); // 获取 DBCP2 连接池中的连接 conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 检查课程容量是否已满子查询 String checkCapacitySql SELECT capacity_used FROM (SELECT COUNT(*) AS capacity_used FROM selection WHERE course_id ? AND status selected) t; ps conn.prepareStatement(checkCapacitySql); ps.setInt(1, courseId); ResultSet rs ps.executeQuery(); rs.next(); int used rs.getInt(capacity_used); CourseDAO courseDAO new CourseDAO(); Course course courseDAO.findById(courseId); if (used course.getMaxCapacity()) { request.setAttribute(message, 选课失败课程《 course.getName() 》名额已满); request.getRequestDispatcher(/student/select_result.jsp).forward(request, response); return; } // 2. 检查是否已选利用 selection 表的联合唯一索引 String checkExistSql INSERT INTO selection (student_id, course_id, status) VALUES (?, ?, selected); ps conn.prepareStatement(checkExistSql); ps.setInt(1, studentId); ps.setInt(2, courseId); int rows ps.executeUpdate(); if (rows 0) { // 唯一索引冲突表示已存在 request.setAttribute(message, 选课失败您已选修《 course.getName() 》); request.getRequestDispatcher(/student/select_result.jsp).forward(request, response); return; } // 3. 更新课程已选人数可选用于统计 String updateUsedSql UPDATE course SET capacity_used capacity_used 1 WHERE id ?; ps conn.prepareStatement(updateUsedSql); ps.setInt(1, courseId); ps.executeUpdate(); conn.commit(); // 所有操作成功提交事务 request.setAttribute(message, 选课成功已加入《 course.getName() 》); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 出错则回滚 } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(message, 选课失败系统异常请稍后重试); } finally { DBUtil.close(conn, ps, null); request.getRequestDispatcher(/student/select_result.jsp).forward(request, response); } }注意capacity_used字段需在course表中新增ALTER TABLE course ADD COLUMN capacity_used INT DEFAULT 0它不是冗余字段而是为快速查询课程剩余名额服务的避免每次选课都执行COUNT(*)子查询。5. 部署与排错从 Eclipse 导入到 Tomcat 运行的 7 个关键检查点5.1 项目结构与依赖配置的硬性要求一个能“亲测可用”的 JSPServletMySQL 项目必须满足以下结构约束缺一不可检查项正确做法常见错误项目类型Dynamic Web Project非 Maven Project用 Maven 创建导致web.xml被忽略或lib目录失效JRE 版本Java 8JDK 1.8使用 JDK 11Tomcat 8.5 无法启动Servlet 4.0 兼容问题lib 目录位置WebContent/WEB-INF/lib/放在src/下或根目录Tomcat 启动时报ClassNotFoundException必需 JAR 包mysql-connector-java-5.1.49.jar、commons-dbcp2-2.8.0.jar、jstl-1.2.jar、standard-1.1.2.jar缺少jstl或standardJSP 中c:forEach报 500 错误web.xml 版本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使用 2.5 版本导致WebServlet注解失效必须用 XML 显式配置5.2 MySQL 连接配置的三个致命参数DBUtil.java中的连接 URL 若缺少以下参数必然出现中文乱码或时区错误// DBUtil.java 中 getConnection() 方法 String url jdbc:mysql://localhost:3306/course_selection? useUnicodetrue // 强制使用 Unicode 编码 characterEncodingutf8mb4 // 指定字符集为 utf8mb4支持 emoji serverTimezoneAsia/Shanghai // 解决 MySQL 8.0 时区报错 allowPublicKeyRetrievaltrue // MySQL 8.0 必加否则报 Public Key Retrieval is not allowed useSSLfalse; // 开发环境关闭 SSL生产环境应启用 Class.forName(com.mysql.jdbc.Driver); // MySQL 5.x 驱动类名 // Class.forName(com.mysql.cj.jdbc.Driver); // MySQL 8.x 驱动类名若用 8.x 则改此处5.2.1 Tomcat 启动后访问 404 的定位流程当浏览器输入http://localhost:8080/course-selection/login.jsp显示 404按此顺序排查检查项目是否部署成功打开 Tomcatmanager/html页面确认course-selection应用状态为running检查 URL 上下文路径Eclipse 中右键项目 → Properties → Web Project Settings → Context root 是否为course-selection不能是course_selection或空检查 login.jsp 物理路径WebContent/login.jsp是否真实存在文件名大小写是否与 URL 一致Linux 系统区分大小写检查 web.xml 中 servlet-mappingLoginServlet的url-pattern是否为/login且login.jsp中表单action是否为${pageContext.request.contextPath}/login查看 Tomcat 日志logs/catalina.out中是否有SEVERE: Error filterStart或Caused by: java.lang.ClassNotFoundException。5.3 学生选课时“名额已满”却仍能提交的底层原因这是一个高频排错场景明明course.max_capacity 30selection表中已有 30 条记录但学生仍能成功选课。根本原因是MySQL 默认隔离级别REPEATABLE READ下的幻读问题SELECT COUNT(*)查询时看到的是事务开始时的快照而INSERT操作在另一个事务中已提交导致两次查询结果不一致。解决方案二选一方案 A推荐教学在SelectCourseServlet中将检查容量与插入操作合并为一条INSERT ... SELECT语句利用唯一索引和INSERT IGNOREINSERT IGNORE INTO selection (student_id, course_id, status) SELECT ?, ?, selected FROM dual WHERE (SELECT COUNT(*) FROM selection WHERE course_id ? AND status selected) (SELECT max_capacity FROM course WHERE id ?);方案 B简化将 MySQL 隔离级别改为READ COMMITTED需在my.cnf中配置transaction-isolation READ-COMMITTED但教学环境不建议改动数据库全局配置。最终验证在student_home.jsp点击“选课”按钮后页面跳转至select_result.jsp显示绿色成功提示且selection表中新增一条记录course.capacity_used字段值加 1 —— 此时你已跑通整个学生选课主干流程。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。