ARTICLE DETAIL

资讯详情

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

JavaWeb学生选课系统:从Servlet+JSP到高并发事务实战

JavaWeb学生选课系统:从Servlet+JSP到高并发事务实战 简介这是一套基于JavaWeb技术栈开发的学生选课管理系统完整实战项目面向计算机专业毕业设计学生及Java初学者解决课程管理、角色权限控制与业务流程闭环等典型Web应用开发问题。资源包共112个文件含22个Java源码、21个JSP页面、22个编译后Class文件、10个核心Jar依赖、5个CSS样式与5个JS脚本以及MySQL数据库脚本sql和系统配置文件xml、properties等整体压缩包仅2.39MB轻量易部署。已有554人学习下载项目经严格调试支持在Tomcat 8与JDK 1.8环境下一键运行。读者可直接获取含三层架构ServletDAOJSP、三角色权限体系管理员/教师/学生、完整CRUD功能模块及Bootstrap响应式前端的可运行工程涵盖学生选课、成绩录入、课程查询等真实业务逻辑代码结构清晰注释规范适合作为毕设参考或JavaWeb进阶实践范例。1. 为什么一个“JavaWeb学生选课管理系统”能成为校招敲门砖——它不是Demo而是你工程能力的黑匣子刚带完三届实习生我翻过不下200份简历里的“课程设计”写“学生成绩管理系统”的占47%写“图书借阅系统”的占31%但真正能讲清数据库事务怎么防选课超限、怎么用Filter拦截未登录请求、怎么在JSP里安全渲染用户输入的——不到12%。这个标题里的“JavaWeb学生选课管理系统完整源码数据库”表面是课设作业实则是检验你是否跨过“会写HelloWorld”和“能扛住并发选课”的分水岭。它逼你直面真实业务里的硬骨头课程容量动态锁、学生-课程-教师三表关联更新、成绩录入时的外键约束与级联删除、管理员批量导入Excel时的事务回滚边界。这不是让你堆砌ServletJSPMySQL的流水线而是用最小技术栈无Spring Boot、无Vue把MVC拆解到每一行代码——比如request.setAttribute(courses, list)背后是怎样的作用域生命周期c:forEach标签里${course.name}为何不能直接拼SQL。适合两类人大三刚学完JDBC想验证知识闭环的以及转行者需要一份可部署、可调试、可讲出5个技术决策理由的压舱石项目。别急着跑通先想清楚当300个学生同时点“提交选课”你的update course set remainremain-1 where id?会不会让最后一门课剩-12. 从零搭起可运行环境IDEA配置JavaWeb项目的三道生死关2.1 创建标准Dynamic Web Project并绕过Tomcat 9的Servlet 4.0陷阱很多新手卡在第一步新建项目后启动报错java.lang.NoClassDefFoundError: javax/servlet/Servlet。这不是缺jar包而是IDEA默认创建的Dynamic Web Module Version是4.0对应Servlet 4.0但Tomcat 9.0.x实际只支持到Servlet 4.0的子集且部分API如HttpServletResponse#sendError(int, String)行为有差异。必须降级File → Project Structure → Modules → [你的模块] → Web → → 勾选Create web.xml强制生成web.xml → 将Web Resource Directory指向src/main/webapp → 点击OK后在Project Settings → Project → Project SDK选JDK 8非11 → 最关键在Project Settings → Modules → [你的模块] → Dependencies → 点 → JARs or directories → 找到tomcat/lib下的servlet-api.jar不是jsp-api.jar提示servlet-api.jar必须以Provided scope引入否则部署时会和Tomcat自带的冲突。若用Tomcat 10则需改用Jakarta EE命名空间jakarta.servlet.*本项目不推荐——源码里全是javax.servlet.*。2.2 数据库连接池配置为什么不用DriverManager而死磕DBCP2Class.forName(com.mysql.jdbc.Driver)DriverManager.getConnection()在选课场景下是自杀行为。当100人并发选课每请求新建连接会导致MySQL报Too many connections默认max_connections151。DBCP2是Apache维护的轻量连接池比C3P0更稳定。在WEB-INF/classes/db.properties中配置driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/studentdb?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse usernameroot password123456 initialSize5 maxTotal20 maxIdle10 minIdle5 maxWaitMillis3000 testOnBorrowtrue validationQuerySELECT 1关键参数说明maxTotal20池中最大连接数按经验公式并发用户数 × 2~3估算300人并发选课建议调至30testOnBorrowtrue每次从池取连接前执行validationQuery避免拿到失效连接MySQL默认wait_timeout28800秒空闲8小时断连useSSLfalseMySQL 8.0默认要求SSL本地开发关掉省事生产环境必须开。2.3 在IDEA中部署到Tomcat解决“404找不到index.jsp”的玄学路径即使项目结构正确仍常出现访问http://localhost:8080/返回404。根源在IDEA的Artifact配置File → Project Structure → Artifacts → → Web Application: Archive → For xxx:war exploded左侧Output Layout中确保WEB-INF/web.xml、WEB-INF/classes/、WEB-INF/lib/三项存在右侧Available Elements中将src/main/webapp拖入WEB-INF同级目录不是子目录最关键的一步点击Fix按钮红色感叹号旁选择Create xxx:war exploded artifact运行配置中Deployment选项卡 → → Artifact → 选择刚创建的xxx:war exploded→ Application context填/不是/xxx。注意若web.xml中welcome-file-list未定义index.jsp需手动添加welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list3. 数据库设计三张核心表如何用外键和触发器守住业务底线3.1 student、course、scstudent_course三表关系建模选课系统本质是多对多关系必须拆解为三张表。常见错误是把sc表设计成id, student_id, course_id, score却忽略score字段的业务生命周期——成绩只在期末录入选课阶段为空但NOT NULL约束会让插入失败。正确设计表名字段类型约束说明studentidINT PK AI主键学号nameVARCHAR(20)NOT NULL姓名majorVARCHAR(30)NOT NULL专业courseidINT PK AI主键课程IDnameVARCHAR(50)NOT NULL课程名creditTINYINTDEFAULT 2学分capacitySMALLINTDEFAULT 60总容量remainSMALLINTDEFAULT 60剩余容量关键scidINT PK AI主键选课记录IDstudent_idINTFK → student.id外键course_idINTFK → course.id外键statusTINYINTDEFAULT 00待确认,1已选中,2已退选scoreDECIMAL(4,1)NULL成绩允许为空提示course.remain字段是性能关键。若每次选课都SELECT remain FROM course WHERE id?再UPDATE SET remainremain-1高并发下必然超卖。必须用原子操作。3.2 用存储过程封装选课逻辑防止超限的终极防线单纯靠应用层判断remain0再更新无法解决并发问题。必须将“检查余量-扣减余量-插入选课记录”三步锁在数据库内DELIMITER $$ CREATE PROCEDURE sp_select_course(IN p_student_id INT, IN p_course_id INT, OUT p_result VARCHAR(50)) BEGIN DECLARE v_remain INT DEFAULT 0; DECLARE v_count INT DEFAULT 0; -- 加行锁SELECT ... FOR UPDATE 锁住course行 SELECT remain INTO v_remain FROM course WHERE id p_course_id FOR UPDATE; IF v_remain 0 THEN SET p_result 课程已满; ELSE -- 检查学生是否已选此课防重复提交 SELECT COUNT(*) INTO v_count FROM sc WHERE student_id p_student_id AND course_id p_course_id AND status 1; IF v_count 0 THEN SET p_result 您已选过该课程; ELSE -- 原子扣减余量 UPDATE course SET remain remain - 1 WHERE id p_course_id; -- 插入选课记录 INSERT INTO sc(student_id, course_id, status) VALUES(p_student_id, p_course_id, 1); SET p_result 选课成功; END IF; END IF; END$$ DELIMITER ;调用方式在DAO中String sql {CALL sp_select_course(?, ?, ?)}; CallableStatement cs conn.prepareCall(sql); cs.setInt(1, studentId); cs.setInt(2, courseId); cs.registerOutParameter(3, Types.VARCHAR); cs.execute(); String result cs.getString(3); // 返回选课成功或错误信息3.3 触发器保障数据一致性当管理员手动修改course.capacity时自动同步remain管理员可能在后台直接UPDATE course SET capacity100 WHERE id5但忘记更新remain导致余量错乱。用触发器自动同步DELIMITER $$ CREATE TRIGGER tr_update_capacity AFTER UPDATE ON course FOR EACH ROW BEGIN IF OLD.capacity ! NEW.capacity THEN -- 若新容量更大则补足remain若更小则按比例缩减保守策略remain不能超过capacity IF NEW.capacity OLD.capacity THEN UPDATE course SET remain remain (NEW.capacity - OLD.capacity) WHERE id NEW.id; ELSE UPDATE course SET remain LEAST(remain, NEW.capacity) WHERE id NEW.id; END IF; END IF; END$$ DELIMITER ;4. 核心功能实现从Servlet到JSP的MVC链路拆解4.1 登录拦截器用Filter实现未登录重定向而非在每个Servlet里if判断在WEB-INF/web.xml中配置filter filter-nameLoginFilter/filter-name filter-classcom.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/student/*/url-pattern url-pattern/admin/*/url-pattern /filter-mappingLoginFilter.java关键逻辑public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 放行登录页和静态资源 String uri request.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.endsWith(/loginServlet) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } // 检查session中是否有user对象 HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { // 重定向到登录页并携带原请求路径便于登录后跳转 String originalUrl request.getRequestURL().toString(); response.sendRedirect(request.getContextPath() /login.jsp?redirect URLEncoder.encode(originalUrl, UTF-8)); return; } chain.doFilter(req, resp); }注意session.getAttribute(user)存的是Student或Admin对象不是字符串。若存字符串admin则权限控制会失效。4.2 选课Servlet如何用事务保证“扣余量插记录”原子性SelectCourseServlet.java中protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int studentId Integer.parseInt(request.getParameter(studentId)); int courseId Integer.parseInt(request.getParameter(courseId)); Connection conn null; try { conn JdbcUtils.getConnection(); // 从DBCP2获取连接 conn.setAutoCommit(false); // 开启事务 // 调用存储过程 String sql {CALL sp_select_course(?, ?, ?)}; CallableStatement cs conn.prepareCall(sql); cs.setInt(1, studentId); cs.setInt(2, courseId); cs.registerOutParameter(3, Types.VARCHAR); cs.execute(); String result cs.getString(3); conn.commit(); // 成功则提交 request.setAttribute(msg, result); request.getRequestDispatcher(/student/select_result.jsp).forward(request, response); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(msg, 系统繁忙请重试); request.getRequestDispatcher(/student/select_result.jsp).forward(request, response); } finally { JdbcUtils.close(conn); // 归还连接到池 } }血泪经验JdbcUtils.close(conn)必须放在finally块否则连接泄露。DBCP2的maxTotal20被耗尽后所有请求卡在getConnection()阻塞。4.3 JSP页面安全渲染防XSS攻击的三个层次学生姓名、课程名等用户输入内容直接${student.name}输出会引发XSS。必须分层防御输入层在AddStudentServlet中用正则过滤String name request.getParameter(name).replaceAll([\], ); // 简单过滤存储层MySQL插入前用PreparedStatement杜绝拼接SQL输出层JSP中用JSTL的c:out标签c:out value${student.name} default未知 escapeXmltrue/escapeXmltrue会将script转为lt;scriptgt;。若用EL表达式${fn:escapeXml(student.name)}需先声明% taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions %。5. 避坑指南那些让项目上线前夜崩溃的5个致命细节5.1 现象选课成功后刷新页面课程余量没变少原因course.remain字段在存储过程中被UPDATE但应用层没有刷新缓存如果用了Ehcache等或前端AJAX调用后没更新DOM。更隐蔽的原因是MySQL的autocommittrue未关闭导致存储过程内的UPDATE被自动提交但后续INSERT失败时ROLLBACK只回滚了INSERTUPDATE已生效。解决在存储过程中显式使用START TRANSACTION和COMMIT/ROLLBACK或确保调用方连接的autocommitfalse。本项目采用后者在JdbcUtils.getConnection()中加Connection conn dataSource.getConnection(); conn.setAutoCommit(false); // 强制关闭自动提交 return conn;5.2 现象中文课程名显示为“???”数据库和JSP都是UTF-8原因MySQL连接URL漏了characterEncodingUTF-8或Tomcat的server.xml中Connector未配置URIEncodingUTF-8。解决db.properties中URL必须含?useUnicodetruecharacterEncodingUTF-8conf/server.xml中找到Connector port8080 ...添加属性URIEncodingUTF-8JSP顶部声明% page contentTypetext/html;charsetUTF-8 languagejava %。5.3 现象管理员删除课程后sc表中对应记录未级联删除导致脏数据原因sc.course_id外键未设置ON DELETE CASCADE。建表时只写了FOREIGN KEY (course_id) REFERENCES course(id)缺少级联动作。解决重建外键生产环境慎用ALTER TABLE sc DROP FOREIGN KEY fk_sc_course_id; ALTER TABLE sc ADD CONSTRAINT fk_sc_course_id FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE;5.4 现象用c:forEach遍历课程列表时c:if test${course.remain 0}始终为false原因course.remain在JavaBean中是int类型但数据库查询时若用ResultSet.getInt(remain)遇到NULL会返回0。而课程初始remaincapacity不可能为0说明查询时remain字段被映射为null。根源是course表中remain字段允许NULL但JavaBean的setRemain(int)方法不能接收null。解决修改表结构ALTER TABLE course MODIFY remain SMALLINT NOT NULL DEFAULT 0;JavaBean中remain字段改为Integer类型setRemain(Integer remain)并处理null或在DAO查询时用rs.getObject(remain, Integer.class)替代getInt。5.5 现象部署到Linux服务器后图片路径404但Windows下正常原因Windows路径不区分大小写Linux严格区分。JSP中写img srcimages/logo.png但实际文件是Images/logo.PNG。解决统一用小写命名且在web.xml中配置默认欢迎文件时注意大小写welcome-file-list welcome-fileindex.jsp/welcome-file !-- 必须小写 -- /welcome-file-list提示Linux下Tomcat日志在logs/catalina.out用tail -f logs/catalina.out实时查看启动错误。6. 进阶验证用JMeter模拟300人并发选课定位性能瓶颈6.1 构建可复现的压测脚本聚焦选课核心链路不要一上来就压整个系统。只测试/student/selectCourseServlet这一接口参数化studentId和courseId在JMeter中创建Thread GroupNumber of Threads300,Ramp-up60,Loop Count1添加HTTP RequestPath:/student/selectCourseServletParameters:studentId${__Random(1001,1999,)}随机学生IDcourseId${__Random(1,20,)}20门课添加View Results Tree监听器观察错误率添加Aggregate Report关注90% Line响应时间。6.2 关键指标解读与优化方向压测后重点关注三组数据指标健康阈值问题表现优化方案Error % 0.5%超过5%检查DBCP2maxTotal是否足够MySQLmax_connections是否调高90% Line 1500ms 3000ms查看慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;重点优化sp_select_course中的SELECT ... FOR UPDATEKB/sec 200 50网络带宽不足或TomcatmaxThreads太小修改conf/server.xml中Executor的maxThreads5006.3 用VisualVM抓取内存泄漏当GC频繁时定位罪魁祸首若压测中发现Full GC次数激增用VisualVM远程连接TomcatTomcat启动时加JVM参数-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port9999 -Dcom.sun.management.jmxremote.authenticatefalse -Dcom.sun.management.jmxremote.sslfalseVisualVM中右键Remote → Add JMX Connection → 输入localhost:9999切换到Monitor标签点击Perform GC观察Old Gen内存是否回落若不回落切换到Profiler → Memory → 点击Heap Dump在类列表中找com.entity.Course实例数是否异常增长可能DAO未关闭ResultSet。我当年在线上环境踩过最深的坑是忘了在CourseDao.findAll()的finally块里rs.close()导致300个并发请求打开300个ResultSet撑爆JVM堆内存。现在我的习惯是所有JDBC资源Connection, PreparedStatement, ResultSet的close()都写在独立的try-catch中绝不合并——因为rs.close()抛异常不能影响ps.close()。希望帮到你。本文还有配套的精品资源点击获取
返回列表