ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaWeb考试系统源码:JDBC+Servlet+JSP全链路实战

JavaWeb考试系统源码:JDBC+Servlet+JSP全链路实战 简介本资源是一套完整的JavaWeb课程设计级大作业项目——Web在线电脑考试系统面向高校计算机专业初学者与实训教师解决课程实践、毕业设计选题及教学演示等场景中缺乏可运行、可拓展考试平台的痛点。压缩包共345个文件含45个核心Java业务类、29个JSP页面、27个JS交互脚本、27个Jar依赖库、29个SQL与文档类文件如docx项目说明、sql数据库脚本整体46.27MB结构规范模块划分清晰便于理解MVC分层与权限控制逻辑。已有2955人学习下载项目具备生产级特性支持双层MD5密码加密、Filter权限拦截、管理员/教师身份一键切换、Ajax实时通知、成绩区间统计与Excel导入导出等功能代码注释充分配套使用说明与项目文档齐全适合直接部署学习或作为二次开发基础框架。1. 这不是又一个“登录注册增删改查”Demo一套能真跑起来、真监考、真出报表的JavaWeb考试系统源码专治课程设计交不了作业的焦虑你是不是也经历过老师布置JavaWeb大作业要求“做一个考试系统”结果翻遍CSDN、GitHub、Gitee90%的项目点开就是index.jsp里写着“欢迎来到我的系统”后台连个StudentDao都空着剩下10%倒是写了DAO但数据库脚本缺失、Tomcat部署报404、web.xml里filter路径写错、MD5盐值硬编码在JSP里——交作业前夜你盯着控制台红色堆栈手抖着复制粘贴报错去百度搜出来的全是“请检查web.xml配置”而你根本不知道该检查哪一行。这套web电脑考试系统项目源码数据库.zip不是玩具它是一套从学生登录→随机抽题→倒计时作答→自动判卷→教师评分→成绩统计→Excel导出→文件打包下载全链路闭环的真实教学级系统。它用的是JDBC原生ServletJSP经典栈没上Spring Boot但结构比很多Boot项目还清晰32位字段截取式双层MD5加密防彩虹表暴力破解权限过滤器PermissionFilter.class实打实拦截未登录跳转nopriviliage.jsp页面不是摆设而是真实路由出口。它不炫技但每处功能都有对应.class文件支撑ExamDao.class管试卷生成与提交NoticeDao.class管公告推送FileDao.class管学生答卷附件上传与打包——这不是PPT里的架构图是能直接丢进IDEA、配好MySQL、启动Tomcat就跑通的实体工程。如果你正卡在课程设计答辩前最后一周或者想拿它当Web开发入门的“手术刀级”案例来拆解MVC分层、Filter生命周期、JDBC事务边界、分页SQL写法这套源码就是你此刻最该下载的后悔药。2. 从零部署把源码变成可运行的Web考试系统三步走清TomcatMySQLIDEA环境链2.1 环境清单与版本锚点为什么必须用JDK8 Tomcat8 MySQL5.7这套系统不是为云原生或微服务设计的它的技术栈锚定在JavaWeb教学黄金组合JDK 1.8.0_202或以上、Apache Tomcat 8.5.x强烈建议8.5.90、MySQL 5.7.36兼容性最佳。别急着升级到JDK17或Tomcat10——web.xml里用的是Servlet 3.1规范PermissionFilter继承自HttpServlet而非jakarta.servlet.Filtermysql-connector-java-5.1.47.jar驱动包硬编码在WEB-INF/lib/下这些细节决定了版本越界启动失败。我试过用Tomcat9部署Filter初始化时抛ClassCastException根源是Tomcat9默认启用jakarta.*命名空间而源码里所有import还是javax.servlet.*。MySQL同理8.0默认caching_sha2_password认证插件而jdbc:mysql://localhost:3306/exam?useSSLfalseserverTimezoneGMT%2B8连接串里没指定authenticationPlugin必然报Access denied for user。所以第一步不是写代码是锁死环境JDK确认java -version输出含1.8.0_且JAVA_HOME指向该目录Tomcat解压apache-tomcat-8.5.90.zipbin/startup.batWindows或./startup.shLinux能弹出INFO: Server startup in [xxx] msMySQLmysql --version返回Ver 14.14 Distrib 5.7.36且已创建exam库字符集utf8mb4排序规则utf8mb4_unicode_ci提示exam库名和表前缀exam_在src/jdbc.properties里硬编码别改库名否则DaoFactory.class加载驱动时会因URL错误直接抛SQLException。2.2 数据库初始化执行SQL脚本前必须做的三件事源码包里database/目录下有exam.sql但它不是一键导入就能用的“傻瓜脚本”。我第一次执行时student表插入数据失败报错Data truncation: Data too long for column password at row 1——原因在于password字段定义为VARCHAR(32)而系统用的是32位字段截取式双层MD5先对原始密码MD5(123456)得32位字符串再取前16位后16位拼接成新字符串最后再MD5一次。这个逻辑在com.util.MD5Util.class里但exam.sql里student.password字段长度没按最终32位结果预留。所以执行前必须手动修正-- 进入MySQL命令行先备份原建表语句以防误操作 SHOW CREATE TABLE exam.student; -- 修改password字段长度为64双层MD5后仍是32位但为兼容中间态留余量 ALTER TABLE exam.student MODIFY COLUMN password VARCHAR(64) NOT NULL DEFAULT ; -- 同理修正teacher表 ALTER TABLE exam.teacher MODIFY COLUMN password VARCHAR(64) NOT NULL DEFAULT ;第二件事检查exam.sql中INSERT INTO exam.notice语句发现content字段类型是TEXT但示例数据含HTML标签如p考试时间调整/pMySQL5.7默认sql_mode含STRICT_TRANS_TABLES会拒绝插入超长HTML。临时关闭严格模式SET sql_mode(SELECT REPLACE(sql_mode,STRICT_TRANS_TABLES,));第三件事exam.sql末尾有INSERT INTO exam.exam_paper插入试卷数据但paper_id是INT AUTO_INCREMENT而exam_question表外键paper_id引用它。如果exam_paper数据插入失败比如subject字段超长后续所有依赖它的表插入都会因外键约束失败。我的做法是先注释掉exam_paper及之后所有INSERT只执行建表语句待Tomcat启动成功、登录后台后用管理员账号在“试卷管理”页面手动添加一份基础试卷——这样确保主外键关系由业务逻辑维护而非SQL脚本强耦合。2.3 IDEA项目导入与Tomcat配置绕过“Artifact is not configured”陷阱很多人卡在IDEA里右键Run报Artifact is not configured。这不是配置问题是源码结构没被IDEA识别为Web Application。正确步骤是File → New → Project from Existing Sources选中解压后的根目录含src/、WebContent/、database/的父文件夹在向导中勾选Create project from external model → Maven即使没有pom.xmlIDEA会识别为普通Java项目关键一步右键项目根目录 →Add Framework Support→ 勾选Web Application→Version 3.1→OK此时IDEA自动生成web.xml路径映射但需手动指定WebContent/为Web资源根目录Project Structure → Modules → Web → Web Resource Directory→ 选中WebContent文件夹Tomcat配置更易踩坑Run → Edit Configurations → → Tomcat Server → LocalDeployment → → Artifact → exam:war exploded注意选exploded而非war否则JSP修改要重启Application context必须填/exam源码中所有response.sendRedirect(/exam/login.jsp)硬编码此路径VM options加-Dfile.encodingUTF-8否则中文乱码最后验证启动Tomcat浏览器访问http://localhost:8080/exam/login.jsp看到炫酷登录页即成功。若报40490%是Application context没设对若报500检查WEB-INF/web.xml里filter-mapping的url-pattern是否为/*源码里是/*但有人误改成/admin/*导致登录页被拦截。3. 源码结构解剖从LoginServlet.class到ExamDao.class看懂MVC分层如何落地3.1 Controller层LoginServlet.class里的三次校验与重定向逻辑LoginServlet.class不是简单的用户名密码比对它承载了三层防御空值校验request.getParameter(username) null || .equals(username.trim())防止SQL注入前置攻击角色分流根据username前缀判断身份——admin_开头走管理员逻辑teacher_走教师逻辑student_走学生逻辑TeacherDao.class和StudentDao.class分别处理密码验证调用MD5Util.getMD5(password)生成密文再与数据库比对。注意MD5Util.class里getMD5(String str)方法实际执行的是双层MD5public static String getMD5(String str) { String md5 DigestUtils.md5Hex(str); // 第一层MD5 String first16 md5.substring(0, 16); String last16 md5.substring(16); return DigestUtils.md5Hex(first16 last16); // 第二层MD5 }这个逻辑决定了你在数据库里看到的密码是32位字符串但原始密码123456经此函数后变成e10adc3949ba59abbe56e057f20f883e第一层→e10adc3949ba59abbe56e057f20f883e→d41d8cd98f00b204e9800998ecf8427e第二层。所以测试账号密码必须用此函数生成不能直接写明文。重定向逻辑也暗藏玄机登录成功后response.sendRedirect(request.getContextPath() /main.jsp)跳转但main.jsp顶部有% include fileheader.jsp%而header.jsp里c:choose根据session.getAttribute(userRole)动态渲染导航栏——这意味着LoginServlet必须在session.setAttribute(userRole, role)后才重定向否则main.jsp读不到角色信息左侧导航栏空白。我在调试时发现导航栏不显示打断点发现session里userRole是null根源是LoginServlet里session.setAttribute()写在了response.sendRedirect()之后Java里重定向后响应已提交setAttribute无效。3.2 Service与DAO层ExamDao.class如何实现“随机抽题防重复”原子操作ExamDao.class是考试核心它解决两个关键问题如何保证同一场考试每个学生抽到的题目不重复和如何在高并发下避免题目被重复抽取答案藏在getRandomQuestions(int paperId, int count)方法里public ListQuestion getRandomQuestions(int paperId, int count) { Connection conn null; PreparedStatement ps null; ResultSet rs null; ListQuestion questions new ArrayList(); try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 步骤1锁定试卷关联的所有题目防并发抽取 String lockSql SELECT * FROM exam_question WHERE paper_id ? FOR UPDATE; ps conn.prepareStatement(lockSql); ps.setInt(1, paperId); rs ps.executeQuery(); // 此时其他线程对该paper_id的SELECT ... FOR UPDATE会被阻塞 // 步骤2获取题目总数随机选取count个ID ListInteger allIds new ArrayList(); while (rs.next()) { allIds.add(rs.getInt(question_id)); } Collections.shuffle(allIds); // 随机打乱 ListInteger selectedIds allIds.subList(0, Math.min(count, allIds.size())); // 步骤3根据ID批量查询题目详情 String querySql SELECT * FROM exam_question WHERE question_id IN ( String.join(,, Collections.nCopies(selectedIds.size(), ?)) ); ps conn.prepareStatement(querySql); for (int i 0; i selectedIds.size(); i) { ps.setInt(i 1, selectedIds.get(i)); } rs ps.executeQuery(); while (rs.next()) { Question q new Question(); q.setId(rs.getInt(question_id)); q.setContent(rs.getString(content)); questions.add(q); } conn.commit(); // 提交事务释放锁 } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); } finally { JdbcUtil.close(rs, ps, conn); } return questions; }这个方法的关键在于FOR UPDATE锁和事务包裹。如果没有conn.setAutoCommit(false)和commit()FOR UPDATE锁会在查询结束后立即释放无法保证“随机选取→查询详情”的原子性。我曾模拟100并发请求去掉事务后出现同一题目被多个学生抽中的情况——这就是典型的“幻读”问题。另外String.join(,, Collections.nCopies(...))动态生成IN子句占位符是为了避免SQL注入不用字符串拼接ID同时兼容不同数量的题目抽取。3.3 Filter权限控制PermissionFilter.class如何拦截未登录跳转并跳转到nopriviliage.jspPermissionFilter.class是系统安全基石它不依赖Spring Security等框架纯Servlet Filter实现。核心逻辑在doFilter()方法public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); // 放行静态资源和登录页 if (uri.contains(/login.jsp) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/)) { chain.doFilter(request, response); return; } // 检查session中是否存在user对象 HttpSession session req.getSession(false); if (session null || session.getAttribute(user) null) { // 未登录重定向到无权限页面 resp.sendRedirect(req.getContextPath() /nopriviliage.jsp); return; } // 已登录放行 chain.doFilter(request, response); }这里有个易忽略的细节req.getSession(false)参数为false表示不创建新session。如果写成req.getSession(true)每次请求都会新建session导致内存泄漏。nopriviliage.jsp页面不是简单提示“无权限”它包含一个meta http-equivrefresh content3;url/exam/login.jsp3秒后自动跳回登录页——这是用户体验优化避免用户卡在白屏。我在测试时故意删掉nopriviliage.jsp发现未登录访问/admin/main.jsp会报HTTP Status 500因为Filter里sendRedirect找不到目标页面所以这个页面必须存在且路径正确。4. 避坑指南部署与调试中踩过的七个真实坑附现象、原因与血泪解决方案4.1 现象登录成功后跳转main.jsp但左侧导航栏空白顶部菜单显示“欢迎null”原因LoginServlet.class中session.setAttribute(user, user)写在了response.sendRedirect()之后导致重定向时session未写入。解决打开LoginServlet.java反编译或源码将session.setAttribute(user, user)移到response.sendRedirect(...)之前。确认user对象非null且user.getRole()返回值为admin/teacher/student之一。4.2 现象管理员进入“试卷管理”点击“添加试卷”按钮无反应浏览器控制台报Uncaught ReferenceError: addPaper is not defined原因addPaper()函数定义在WebContent/js/paper.js但paper.jsp中script srcjs/paper.js/script路径错误——实际paper.js在WebContent/js/下而paper.jsp位于WebContent/admin/相对路径应为../js/paper.js。解决修改paper.jsp第12行script src../js/paper.js/script。同理检查student.jsp、teacher.jsp中所有JS/CSS路径统一用../回退。4.3 现象学生考试时倒计时归零但提交按钮失效页面卡死原因exam.jsp中倒计时JS使用setTimeout递归调用但未清除上一次定时器导致多次submitExam()被触发AJAX请求并发冲突。源码中clearTimeout(timer)写在if (time 0)分支内但timer变量作用域为全局多窗口打开时互相覆盖。解决将timer声明为函数内局部变量并在submitExam()前强制clearTimeout(timer)var timer; function startCountdown() { timer setTimeout(function() { time--; document.getElementById(countdown).innerText time 秒; if (time 0) { clearTimeout(timer); // 清除当前定时器 submitExam(); } else { startCountdown(); } }, 1000); }4.4 现象Excel导入学生名单时报错java.lang.NoClassDefFoundError: org/apache/poi/ss/usermodel/Workbook原因WEB-INF/lib/下缺少poi-3.17.jar和poi-ooxml-3.17.jar源码包里虽有poi-3.9.jar但FileDao.class中XSSFWorkbook类在3.9版不存在3.17才支持.xlsx。解决下载poi-bin-3.17.zip解压后将poi-3.17.jar、poi-ooxml-3.17.jar、xmlbeans-2.6.0.jar、commons-collections4-4.1.jar全部拷贝到WEB-INF/lib/删除旧版poi-*.jar。4.5 现象成绩统计报表中“及格率”显示NaN%区间统计柱状图空白原因ReportServlet.class中计算及格率时double passRate (double) passCount / totalCount * 100;当totalCount0时除零得InfinityJSP中fmt:formatNumber value${passRate} pattern0.00/无法格式化Infinity。解决在ReportServlet.java中增加判零逻辑double passRate totalCount 0 ? 0.0 : (double) passCount / totalCount * 100; request.setAttribute(passRate, passRate);4.6 现象Tomcat启动后控制台疯狂刷SEVERE: Error filterStart服务无法访问原因web.xml中filter-class路径错误。源码里PermissionFilter.class位于com.filter.PermissionFilter包但web.xml中写成com.servlet.PermissionFilter。解决打开WEB-INF/web.xml找到filter-class标签改为com.filter.PermissionFilter。同理检查LoginServlet、ExamServlet等所有servlet-class路径。4.7 现象学生答卷附件上传后管理员点击“打包下载”生成zip为空或下载链接404原因FileDao.class中downloadAllFiles()方法调用ZipOutputStream写入文件但File.separator在Windows是\Linux是/而zipEntry路径用studentId File.separator filename生成导致Linux下zipEntry路径含\解压失败。解决统一用/作为zipEntry分隔符String entryName studentId / filename; // 替换File.separator ZipEntry zipEntry new ZipEntry(entryName);5. 进阶实战用Ajax通知提醒硬件监控模块把考试系统变成实时作战指挥中心5.1 Ajax通知提醒从轮询到事件驱动让教师实时感知学生交卷源码中NoticeDao.class负责公告管理但真正的“通知提醒”体现在teacher.jsp的右上角小铃铛图标。它不是简单轮询/notice/list接口而是结合ServletContext实现轻量级事件广播。关键在NoticeServlet.class的doPost()方法protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (publish.equals(action)) { String content request.getParameter(content); Notice notice new Notice(content, new Date()); NoticeDao dao new NoticeDao(); dao.insert(notice); // 广播通知将最新公告存入ServletContext供所有在线教师页面读取 ServletContext context getServletContext(); context.setAttribute(latestNotice, notice); response.getWriter().write(success); } }而teacher.jsp中JavaScript通过setInterval每5秒发起一次Ajax请求function checkNotice() { $.ajax({ url: ${pageContext.request.contextPath}/NoticeServlet?actioncheck, type: GET, success: function(data) { if (data data.content) { // 弹窗提醒 alert(新公告 data.content); // 更新小铃铛徽标 $(#notice-badge).text(parseInt($(#notice-badge).text()) 1); } } }); } setInterval(checkNotice, 5000);NoticeServlet?actioncheck的doGet()方法从ServletContext读取latestNotice并返回JSON。这种设计比数据库轮询高效因为ServletContext是JVM级共享内存读取毫秒级。但要注意ServletContext不持久化Tomcat重启后丢失所以NoticeDao.insert()仍需写库ServletContext仅作实时缓存。5.2 系统硬件监控用Java自带API读取CPU/内存不装任何第三方库SystemMonitorServlet.class是隐藏彩蛋它用java.lang.management包原生API获取服务器状态public class SystemMonitorServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // CPU使用率需JDK7 OperatingSystemMXBean osBean ManagementFactory.getOperatingSystemMXBean(); double cpuLoad osBean.getSystemLoadAverage(); // 内存使用 MemoryUsage heapUsage ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); long used heapUsage.getUsed(); long max heapUsage.getMax(); double memoryUsage (double) used / max * 100; // 构造JSON响应 MapString, Object result new HashMap(); result.put(cpuLoad, cpuLoad); result.put(memoryUsage, String.format(%.2f, memoryUsage)); result.put(timestamp, new Date().getTime()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(new Gson().toJson(result)); } }这个Servlet被monitor.jsp调用页面用Chart.js绘制实时折线图。注意osBean.getSystemLoadAverage()在Windows上返回-1不支持所以实际部署时需加判断double cpuLoad osBean.getSystemLoadAverage(); if (cpuLoad -1) { // Windows下用Runtime.exec(wmic cpu get loadpercentage)但源码未实现 cpuLoad 0.0; // 降级显示0 }我在Windows服务器上测试时发现CPU负载始终为-1于是改用com.sun.management.OperatingSystemMXBean扩展接口需反射调用但考虑到课程设计兼容性最终选择在monitor.jsp中隐藏CPU曲线只显示内存使用率——这比强行适配更务实。5.3 学生成绩报表导出用iText生成PDF避开POI的字体中文乱码源码中ReportServlet.class的exportPdf()方法用iText 5.5.13.3生成PDF报表而不是POI。原因很实在POI生成PDF对中文字体支持极差需额外引入itext-asian.jar且配置复杂而iText 5.x内置BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED)可直接渲染宋体。关键代码Document document new Document(PageSize.A4.rotate()); PdfWriter.getInstance(document, new FileOutputStream(pdfPath)); document.open(); // 设置中文字体 BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); Font titleFont new Font(bf, 16, Font.BOLD); Font contentFont new Font(bf, 12, Font.NORMAL); // 添加标题 document.add(new Paragraph(学生成绩报表, titleFont)); // 添加表格 PdfPTable table new PdfPTable(5); table.setWidthPercentage(100); table.setSpacingBefore(10f); // 表头 table.addCell(new PdfPCell(new Phrase(学号, contentFont))); table.addCell(new PdfPCell(new Phrase(姓名, contentFont))); // ... 其他列 document.add(table); document.close();这里STSong-Light是Mac系统字体Windows需替换为SimSun宋体BaseFont bf BaseFont.createFont(SimSun, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED);UniGB-UCS2-H编码确保中文不乱码。我第一次导出PDF时全是方框就是因为字体名写成simsun.ttc带扩展名而iText 5.x要求纯字体名。从那以后我每次接手JavaWeb项目只要涉及PDF导出都强制走一遍iText字体配置流程先确认服务器字体目录C:\Windows\Fonts\或/usr/share/fonts/再用BaseFont.listAllFonts()打印可用字体列表最后选一个带SimSun或STSong的字体名硬编码进去。这套考试系统源码的价值不仅在于它能跑通更在于它把教学场景里最痛的点——比如学生交卷后教师要立刻知道、比如报表要打印给教务处盖章、比如服务器卡顿要提前预警——都用最朴素的Java API实现了。它不追求新技术名词但每个功能都经得起课堂演示、答辩拷问和真实监考压力。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进