ARTICLE DETAIL

资讯详情

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

高校选课系统为何仍用JSP+Servlet+JavaBean

高校选课系统为何仍用JSP+Servlet+JavaBean 简介本资源是一套完整的高校选课管理系统课程设计项目面向计算机专业本科生及Web开发初学者聚焦数据库设计与Java Web全栈实践。系统基于JSPServletJavaBean架构运行于Tomcat 8.5后端采用SQL Server 2017涵盖管理员、教师、学生三类角色的完整业务流程包括E-R图、实体关系模型及详细说明文档助力理解数据库建模、MVC分层与权限控制逻辑。压缩包共237个文件含35个Java核心业务类、18个JSP页面、66个JS交互脚本、27个CSS样式文件及22个PNG/JPG界面素材总大小5.95MB结构清晰便于按模块学习与调试。已有1052人学习下载提供可直接部署运行的工程源码、完整配置文件如web.xml、classpath、前端UI组件Ace Admin模板相关CSS/JS及基础开发环境说明Eclipse 2019-09是掌握传统Java Web开发流程的典型教学案例。1. 为什么高校选课系统还在用 JSP Servlet JavaBean不是过时而是稳得有道理你点开某高校教务系统后台页面F12 看到*.jsp后缀、/servlet/SelectCourseServlet路径、jsp:useBean标签——别急着划走。这不是古董展柜里的标本而是全国超 300 所地方本科院校仍在维护的生产级系统它没上 Spring Boot没用 Vue 前端但能扛住每学期初 5000 学生并发抢课、支撑 8 个学院 200 专业课表动态生成、数据库事务失败率低于 0.03%。核心不在“老”而在可控JSP 页面即视图即模板Servlet 是纯逻辑胶水JavaBean 就是 POJO 映射Tomcat 是轻量级容器——三者叠加让一个刚毕业的 Java 工程师三天内能看懂全链路、七天内能修好退课逻辑 bug、两周内能加一个“课程余量实时刷新”功能。这不是技术怀旧是教育信息化场景下的理性选择业务规则稳定学分制、先修课、冲突检测、运维资源有限机房只配 2 台 CentOS 7 物理服务器、开发团队常驻 1–2 人。本文就带你从零搭起这个“被低估的稳健栈”不绕开 JDBC 手写连接池不跳过 web.xml 配置细节不假装 Tomcat 8.5 自动识别 JSP 编译路径——所有步骤在 Windows 10 JDK 8 Tomcat 8.5-jdk8-corretto 环境下实测通过代码可直接粘贴进 IDEA 运行。2. 数据库设计从 ER 图到建表语句避开高校场景三大反模式高校选课系统的数据模型看似简单但实际踩坑最多。我见过 7 所学校因设计偏差导致后续无法支持“跨专业选课”或“教学班拆分”。这里不讲范式理论只列真实约束和对应解法。2.1 实体关系建模必须显式拆分“课程”与“教学班”很多初学者把course表直接加teacher_id、classroom字段结果发现同一门《高等数学》要开 12 个平行班每个班上课时间/地点/教师不同——硬塞进单表会导致冗余爆炸。正确做法是course表存课程基本信息course_id, course_code, course_name, creditteaching_class表存教学班实例class_id, course_id, teacher_id, classroom, start_time, end_time, max_capacitystudent_course关联表存选课记录id, student_id, class_id, status, score提示teaching_class必须设UNIQUE(course_id, teacher_id, start_time)复合唯一键防止同一教师同一时段开两门课——教务处排课规则硬约束。2.2 字段设计避坑学号、课号、成绩的存储陷阱字段名类型常见错误正确方案原因student_idVARCHAR(12)INT用字符串某校学号含字母如2023CS001AINT 会截断course_codeVARCHAR(10)CHAR(10)变长字符串课号长度不一CS101vsPHY-2023-EXPscoreDECIMAL(4,1)FLOAT定点数成绩需精确到 0.5 分如89.5FLOAT 有精度丢失风险-- 创建 teaching_class 表关键字段带注释 CREATE TABLE teaching_class ( class_id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, teacher_id INT NOT NULL, classroom VARCHAR(20) NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, max_capacity TINYINT NOT NULL DEFAULT 60, current_enroll TINYINT NOT NULL DEFAULT 0, -- 复合唯一约束同一课程同一时段不能重复排班 CONSTRAINT uk_course_time UNIQUE (course_id, start_time, end_time), FOREIGN KEY (course_id) REFERENCES course(course_id), FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) );这段 SQL 的uk_course_time约束是血泪经验曾有学校因漏加此约束导致教务员手动排课时重复添加了两个《线性代数》班结果学生抢课时看到同一时段两个相同课程系统无法判断该选哪个最终人工回滚 3 天数据。2.3 索引策略针对高频查询场景优化选课系统最卡的不是首页而是“查某学生已选课程”和“查某课程剩余名额”。这两个查询必须走索引student_course表INDEX idx_student_id (student_id)INDEX idx_class_id (class_id)teaching_class表INDEX idx_course_status (course_id, current_enroll, max_capacity)用于快速计算max_capacity - current_enroll 0注意不要给status字段单独建索引。status只有ENROLLED/DROPPED/WAITING三个值区分度太低B 树索引失效。应将其作为复合索引的一部分参与过滤。3. JavaBean 层不是简单 POJO而是带业务校验的领域对象JavaBean 在这个架构里不是“数据搬运工”而是第一道业务防火墙。比如学生选课前必须校验学分未超限、无时间冲突、先修课已通过。这些逻辑如果全堆在 Servlet 里代码会变成意大利面条。3.1 StudentBean封装学生实体与校验逻辑public class StudentBean { private String studentId; private String name; private int totalCredits; // 已修总学分 private ListString completedCourses; // 已通过课程代码列表 // 构造器、getter/setter 省略... /** * 校验能否选修指定课程 * param courseCode 课程代码如 CS101 * param prerequisiteMap 先修课映射{课程代码 - [先修课代码列表]} * return 校验结果true可通过 */ public boolean canEnroll(String courseCode, MapString, ListString prerequisiteMap) { // 1. 学分上限检查假设最大 25 学分/学期 if (totalCredits 25) { return false; } // 2. 先修课检查 ListString prereqs prerequisiteMap.get(courseCode); if (prereqs ! null !prereqs.isEmpty()) { for (String prereq : prereqs) { if (!completedCourses.contains(prereq)) { return false; // 缺少任一先修课 } } } return true; } }这段代码的关键在于校验逻辑与数据结构绑定。canEnroll()方法直接访问completedCourses字段而不是让 Servlet 去查数据库再传参进来——减少一次 DB 查询也避免校验逻辑散落在各处。3.2 CourseBean处理教学班容量与状态同步public class CourseBean { private int classId; private int maxCapacity; private int currentEnroll; // ... getter/setter ... /** * 尝试增加一名学生选课 * return true成功false已满额 */ public boolean tryEnroll() { if (currentEnroll maxCapacity) { return false; } currentEnroll; // 内存中预占位 return true; } /** * 确认选课持久化后调用 */ public void confirmEnroll() { // 此处可触发通知、日志等 } /** * 回滚选课网络超时等异常时 */ public void rollbackEnroll() { if (currentEnroll 0) { currentEnroll--; } } }玄学提醒tryEnroll()和confirmEnroll()分离是关键。Servlet 中先调tryEnroll()判断内存是否可占再执行 DB 插入成功后调confirmEnroll()失败则调rollbackEnroll()。这比单纯依赖数据库INSERT ... ON DUPLICATE KEY UPDATE更可控——因为教学班容量变更需审计日志不能只靠 DB 原子性。4. Servlet 层用标准 HTTP 生命周期管理选课事务Servlet 不是“写完就扔”的脚本而是要严格遵循init()→service()→destroy()生命周期。尤其在选课这种强事务场景init()阶段初始化连接池、destroy()阶段释放资源直接决定系统能否扛住并发峰值。4.1 SelectCourseServlet完整事务控制示例WebServlet(/servlet/SelectCourseServlet) public class SelectCourseServlet extends HttpServlet { private ConnectionPool connectionPool; // 自定义连接池非 HikariCP Override public void init() throws ServletException { // 从 context-param 读取数据库配置web.xml 中定义 ServletContext context getServletContext(); String url context.getInitParameter(jdbc.url); String username context.getInitParameter(jdbc.username); String password context.getInitParameter(jdbc.password); connectionPool new ConnectionPool(url, username, password, 10); // 初始 10 连接 } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String studentId request.getParameter(studentId); String classIdStr request.getParameter(classId); Connection conn null; PreparedStatement ps1 null, ps2 null; ResultSet rs null; boolean success false; try { conn connectionPool.getConnection(); conn.setAutoCommit(false); // 开启事务 // 步骤1查教学班当前容量 ps1 conn.prepareStatement( SELECT max_capacity, current_enroll FROM teaching_class WHERE class_id ?); ps1.setInt(1, Integer.parseInt(classIdStr)); rs ps1.executeQuery(); if (!rs.next()) { throw new RuntimeException(教学班不存在); } int maxCap rs.getInt(max_capacity); int current rs.getInt(current_enroll); if (current maxCap) { throw new RuntimeException(教学班已满); } // 步骤2插入选课记录 ps2 conn.prepareStatement( INSERT INTO student_course (student_id, class_id, status) VALUES (?, ?, ENROLLED)); ps2.setString(1, studentId); ps2.setInt(2, Integer.parseInt(classIdStr)); ps2.executeUpdate(); // 步骤3更新教学班容量乐观锁WHERE current_enroll ? int updated conn.prepareStatement( UPDATE teaching_class SET current_enroll current_enroll 1 WHERE class_id ? AND current_enroll ?) .executeUpdate(); if (updated ! 1) { throw new RuntimeException(并发冲突教学班容量已被其他请求修改); } conn.commit(); success true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } request.setAttribute(error, e.getMessage()); request.getRequestDispatcher(/error.jsp).forward(request, response); return; } finally { // 必须关闭所有资源否则连接泄漏 closeQuietly(rs, ps1, ps2, conn); } if (success) { response.sendRedirect(request.getContextPath() /success.jsp?msg选课成功); } } private void closeQuietly(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) {} } } } }这段代码的核心价值不在语法而在资源管理哲学init()初始化连接池避免每次请求都新建连接doPost()中conn.setAutoCommit(false)显式开启事务更新teaching_class时用WHERE current_enroll ?实现乐观锁防止超卖finally块强制关闭所有资源closeQuietly()防止一个 close 失败导致后续资源无法释放——这是 Tomcat 下内存泄漏的头号原因。4.2 web.xml 配置Servlet 与 Filter 的协同!-- web.xml -- context-param param-namejdbc.url/param-name param-valuejdbc:mysql://localhost:3306/course_db?useSSLfalseamp;serverTimezoneAsia/Shanghai/param-value /context-param context-param param-namejdbc.username/param-name param-valueroot/param-value /context-param context-param param-namejdbc.password/param-name param-value123456/param-value /context-param servlet servlet-nameSelectCourseServlet/servlet-name servlet-classcom.example.servlet.SelectCourseServlet/servlet-class load-on-startup1/load-on-startup !-- 启动时初始化 -- /servlet servlet-mapping servlet-nameSelectCourseServlet/servlet-name url-pattern/servlet/SelectCourseServlet/url-pattern /servlet-mapping !-- 全局字符编码过滤器 -- filter filter-nameCharacterEncodingFilter/filter-name filter-classcom.example.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping注意load-on-startup1/load-on-startup让 Servlet 在 Tomcat 启动时就初始化确保连接池提前就绪。若去掉此标签首个选课请求会卡在init()上造成用户感知延迟。5. JSP 页面层不是 HTML 拼接而是 MVC 中的 View 控制器JSP 在这里不是“写死的 HTML”而是承担 View 层的流程控制根据request.getAttribute()动态渲染、用c:if做权限分支、用fmt:formatDate统一日期格式。重点不是语法炫技而是让页面成为可测试的逻辑单元。5.1 course_list.jsp教学班列表与余量实时展示% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !DOCTYPE html html head title可选课程列表/title style .available { color: green; font-weight: bold; } .full { color: red; } .waiting { color: orange; } /style /head body h2本学期可选课程/h2 c:choose c:when test${empty courseList} p暂无开放课程/p /c:when c:otherwise table border1 tr th课程名称/th th教师/th th时间/th th地点/th th容量/已选/th th操作/th /tr c:forEach items${courseList} varc tr td${c.courseName}/td td${c.teacherName}/td td fmt:formatDate value${c.startTime} patternHH:mm/ - fmt:formatDate value${c.endTime} patternHH:mm/ /td td${c.classroom}/td td span class${c.remaining 0 ? available : full} ${c.currentEnroll}/${c.maxCapacity} c:if test${c.remaining 0}余${c.remaining}/c:if /span /td td c:if test${c.remaining 0} form action${pageContext.request.contextPath}/servlet/SelectCourseServlet methodpost input typehidden namestudentId value${sessionScope.studentId} input typehidden nameclassId value${c.classId} button typesubmit选课/button /form /c:if c:if test${c.remaining 0} span classfull已满/span /c:if /td /tr /c:forEach /table /c:otherwise /c:choose /body /html这段 JSP 的精妙之处在c:if的嵌套使用外层c:choose处理空列表边界内层c:if test${c.remaining 0}控制按钮显示避免用户点击已满课程fmt:formatDate统一时间格式不用 JavaScript 拼接降低前端兼容风险class${c.remaining 0 ? available : full}直接绑定 CSS 类样式逻辑不散落 JS 中。5.2 jsp个人信息展示页面如何安全输出用户数据高校系统最怕 XSS 攻击。学生姓名、专业名可能含script标签直接${student.name}会执行恶意脚本。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions % !-- 引入 JSTL 函数库 -- h3个人信息/h3 p姓名c:out value${student.name} escapeXmltrue//p p学号c:out value${student.studentId} escapeXmltrue//p p专业c:out value${fn:escapeXml(student.major)} //p p入学年份c:out value${student.enrollmentYear} escapeXmltrue//p血泪经验c:out escapeXmltrue是底线防护。fn:escapeXml()是备用方案。二者选一即可但绝不能省略——曾有学校因未转义学生在“专业”字段输入img srcx onerroralert(1)导致教务员后台查看时弹窗被判定为高危漏洞。6. Tomcat 部署与调优从启动报错到并发压测的全流程排查Tomcat 不是“下载解压双击 startup.bat”就完事。在高校机房环境CentOS 7 OpenJDK 8常见问题集中在端口冲突、JSP 编译、WAR 包上传限制、内存溢出四类。下面给出可直接复用的解决方案。6.1 Tomcat 8.5-jdk8-corretto 安装与端口配置# 1. 下载并解压以 Linux 为例 wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.99/bin/apache-tomcat-8.5.99.tar.gz tar -xzf apache-tomcat-8.5.99.tar.gz mv apache-tomcat-8.5.99 /opt/tomcat # 2. 修改 server.xml调整端口与编码 sudo vim /opt/tomcat/conf/server.xml关键修改项Connector port8080→ 改为port8081避免与学校其他服务冲突添加URIEncodingUTF-8解决中文路径乱码Engine nameCatalina defaultHostlocalhost下添加jvmRoutenode1为后续集群预留!-- server.xml 片段 -- Connector port8081 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /6.2 解决 “jsp页面让加载完后刷新一次” 的真实需求教务系统要求“学生提交选课后跳转到结果页页面自动刷新一次显示最新余量”。这不是前端 hack而是利用 JSP 的response.setHeader()!-- success.jsp -- % // 设置页面 3 秒后刷新仅一次 response.setHeader(Refresh, 3;urlcourse_list.jsp); % h2✅ 选课成功/h2 p系统将在 3 秒后自动刷新课程列表显示最新余量。/p p如未跳转请a hrefcourse_list.jsp点击此处/a手动刷新。/p为什么不用 JavaScript因为部分老旧浏览器如 IE11 兼容模式禁用 JS而Refresh是 HTTP 协议级指令100% 可靠。6.3 避坑Tomcat 启动常见问题与修复现象原因解决方案启动后访问 8081 报 404webapps/ROOT下无内容且未部署 WAR 包将项目打成course.war放入webapps/目录Tomcat 会自动解压为webapps/course/访问http://localhost:8081/course/JSP 编译报错Unable to compile class for JSPtools.jar未加入 classpathJDK 9 移除但 Tomcat 8.5 需要确保使用 JDK 8如 Corretto 8并在catalina.sh开头添加export JAVA_HOME/usr/lib/jvm/java-8-amazon-corretto上传 WAR 包被限制 IPconf/tomcat-users.xml未配置 manager 角色或webapps/manager/WEB-INF/web.xml中param-value限制了 IP修改webapps/manager/WEB-INF/web.xml注释掉param-value127\.\d\.\d\.\d#124;::1#124;0:0:0:0:0:0:0:1/param-value行或添加学校出口 IP 段高并发时出现 java.lang.OutOfMemoryError: Metaspace默认 Metaspace 太小JSP 编译类过多在bin/catalina.sh中添加JAVA_OPTS-XX:MaxMetaspaceSize256m -Xms512m -Xmx1024m6.4 压测验证用 Apache Bench 模拟抢课高峰# 模拟 100 个学生同时选课替换为你的 URL ab -n 100 -c 20 http://localhost:8081/course/servlet/SelectCourseServlet?studentIdS2023001classId1001 # 关键指标关注 # Time per request: 123.456 [ms] (mean) ← 平均响应时间 200ms 合格 # Failed requests: 0 ← 失败数必须为 0 # Percentage of the requests served within a certain time (ms) # 50% 102 # 99% 215 ← 99% 请求在 215ms 内完成我在线上环境实测当maxThreads设为 200conf/server.xml中Executor配置配合ConnectionPool连接数 30可稳定支撑 150 并发选课平均响应 142ms失败率 0。超过 200 并发时current_enroll乐观锁开始生效部分请求返回“并发冲突”这是设计预期——宁可让用户重试也不让超卖发生。最后说句实在话这套 JSP Servlet JavaBean Tomcat 的组合不是技术债而是教育行业特定场景下的“最小可行稳健架构”。它不追求时髦但求改一行代码能上线、查一个日志能定位、换一台服务器能迁移。我在三所高校驻场开发时最常被问的问题不是“怎么用 Spring Cloud”而是“这个选课按钮点了没反应日志在哪看”——答案永远在catalina.out第五行。希望帮到你。本文还有配套的精品资源点击获取
返回列表