
简介本资源是一套完整的JavaWeb在线考试系统实战项目面向计算机专业本科毕设学生及Java Web初学者解决课程设计、项目实训与就业能力提升中的典型业务开发需求。压缩包共148个文件含21个JSP页面实现前后端交互、23个Java源码涵盖StudentAction、ExamAction等核心控制器与Dao层操作、27个Jar依赖库支撑SSH或基础Servlet架构、11个CSS/JS前端资源保障界面美观与交互流畅以及SQL数据库脚本和配置类文件整体大小13.69MB。已有1384人学习下载说明其在教学实践与自学验证中具备较强认可度。用户可直接导入Eclipse在Tomcat 6与MySQL 5环境下一键运行项目功能覆盖考生在线考试、成绩查询、密码修改以及管理员对试卷、题目、考生信息的全流程管理代码结构清晰、模块职责分明附带完整数据库脚本与调试说明是理解MVC分层设计与Web系统工程化落地的优质参考范例。1. JavaWeb在线考试系统为什么它仍是高校与企业培训场景里最稳的“压舱石”你可能已经见过太多标榜“AI阅卷”“智能组卷”的新概念考试平台但真正在某高校教务处、某职业培训机构、某国企内部培训系统里跑满三年以上、日均并发超800人、零重大故障的往往还是那个用 Servlet JSP MySQL 搭出来的 JavaWeb 在线考试系统。它不炫技但能扛住考前30分钟集体刷新试卷、交卷瞬间数据库写入洪峰、监考老师后台实时踢人不卡顿——这种确定性恰恰是教育类业务最不敢赌的变量。它适合谁不是想发论文的算法团队而是手头有2000名学员要下周开考、运维只有1人、服务器还是两台老旧物理机的中小型教学单位。它解决的从来不是“能不能做”而是“能不能在没专职 DevOps、没云原生架构、没预算买商业题库系统的情况下7天内上线一个可用、可查、可追溯、不丢分”的刚性交付问题。别被“JavaWeb”三个字劝退——它不是古董而是经过二十年真实考场锤炼的最小可靠范式。2. 从零搭起核心骨架Servlet JSP MySQL 的三件套怎么选型、怎么连通2.1 为什么坚持用传统 JavaWeb 而不直接上 Spring Boot这不是守旧是权衡。某高校教务处曾试过 Spring Boot Vue 做新版考试系统开发快但部署时发现运维人员只会重启 Tomcat不会配 Nginx 反向代理、搞不懂 Actuator 端点暴露风险、更不会调优 JVM GC 参数应对交卷高峰。结果上线后第2周因 HikariCP 连接池未设最大空闲时间数据库连接数缓慢爬升至上限导致监考端无法刷新考生状态。而传统 JavaWeb 架构下所有逻辑都在一个 WAR 包里web.xml配置清晰context.xml里数据源参数一目了然Tomcat manager 页面点几下就能看实时会话数和内存占用——对非专业 Java 运维来说这是可感知、可干预、可回滚的确定性。常见做法是用 Tomcat 9.0.xJDK 11 兼容性好、MySQL 5.7事务稳定、主从同步成熟、前端用 Bootstrap 4轻量、兼容 IE11很多老机房电脑还跑着 Win7。2.2 数据库设计一张exam_paper表如何撑起动态组卷与防作弊考试系统最怕“静态试卷”——题库更新后旧试卷不生效或同一场考试不同考生看到重复题目。我们不用“试卷表试题表关联表”三层嵌套而是用单表exam_paper加 JSON 字段存结构化题目CREATE TABLE exam_paper ( id bigint NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 试卷标题, paper_json text NOT NULL COMMENT JSON格式{ sections: [ { type:single, count:10, from_pool_id:1 }, ... ] }, duration_minutes int NOT NULL DEFAULT 60, status tinyint NOT NULL DEFAULT 0 COMMENT 0-草稿,1-启用,2-停用, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;提示paper_json字段不参与查询条件只作存储真正组卷逻辑由 Java 服务层执行——根据from_pool_id查题库表按count随机抽取题目 ID 列表再拼装成考生专属试卷 JSON。这样既避免大表 JOIN 拖慢生成速度又保证每次生成都是真随机非伪随机种子固定。题库表question_pool用category和difficulty建联合索引抽题时WHERE pool_id ? AND status 1 ORDER BY RAND() LIMIT ?改为ORDER BY id % 1000, id取模扰动LIMIT实测比RAND()快 17 倍。2.3 登录与权限用 Filter 实现“监考员只能看本考场”而非 RBAC 大框架RBAC基于角色的访问控制听起来规范但在实际中某职业培训机构反馈他们有 12 个分校每个分校 3 名监考员但监考员只允许查看自己分校的考生列表和交卷记录。如果走标准 RBAC得建分校表、监考员分校关联表、再加数据级权限拦截器……太重。我们用更轻的方案在登录成功后将监考员所属分校 ID 存入 Session并在关键 Servlet如GetExamListServlet前加BranchFilterpublic class BranchFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(); Long branchId (Long) session.getAttribute(branch_id); // 拦截 /admin/exam/list 接口强制添加 branch_id 查询参数 String uri request.getRequestURI(); if (uri.startsWith(/admin/exam/list)) { // 将 branch_id 注入 request attribute后续 Servlet 可直接取 request.setAttribute(filter_branch_id, branchId); } chain.doFilter(request, resp); } }然后在GetExamListServlet中protected void doGet(HttpServletRequest req, HttpServletResponse resp) { Long branchId (Long) req.getAttribute(filter_branch_id); ListExam exams examService.listByBranch(branchId); // DAO 层 SQL 显式加 WHERE branch_id ? req.setAttribute(exams, exams); req.getRequestDispatcher(/admin/exam/list.jsp).forward(req, resp); }逻辑清晰、无侵入、易调试——这才是小团队能快速迭代的关键。3. 核心功能落地自动计时、防切屏、实时交卷状态同步的三道硬坎3.1 前端倒计时用setTimeout还是setInterval血泪经验告诉你别踩坑考试页面顶部必须显示剩余时间且需与服务端时间严格一致防客户端篡改。新手常犯错用setInterval(() { time--; }, 1000)看似简单但 JS 单线程下若页面切到后台或浏览器休眠定时器会积压唤醒后瞬间执行多次导致时间跳变。正确做法是服务端下发考试开始时间戳startTime毫秒前端用requestAnimationFrame做平滑渲染并每 10 秒向服务端校准一次let startTime ${startTime}; // JSP 输出服务端时间戳 let duration ${durationMinutes} * 60 * 1000; let lastSync Date.now(); function updateTimer() { const now Date.now(); const elapsed now - startTime; let remaining Math.max(0, duration - elapsed); // 每 10 秒主动校准 if (now - lastSync 10000) { fetch(/api/exam/time-sync?examId ${examId}) .then(r r.json()) .then(data { startTime data.serverStartTime; // 服务端返回最新 startTime lastSync Date.now(); }); } document.getElementById(timer).innerText formatTime(remaining); if (remaining 0) { requestAnimationFrame(updateTimer); // 比 setInterval 更精准 } else { submitExam(); // 自动交卷 } } updateTimer();服务端/api/exam/time-sync接口只做一件事查数据库exam_record表中该考生的start_time字段并返回不计算、不缓存——确保绝对权威。3.2 防切屏与防复制visibilitychangecopy事件的组合拳教育场景下“考生切出考试页面查答案”是刚需防控点。仅靠onblur不可靠Chrome 84 对非激活 Tab 限制setTimeout。我们采用双保险监听document.addEventListener(visibilitychange, ...)页面不可见时立即触发警告并记录日志同时监听document.addEventListener(copy, e { e.preventDefault(); })禁用 CtrlC防复制题目到搜索引擎但要注意visibilitychange在移动端 Safari 上行为异常切到微信再切回状态可能不触发。因此必须叠加服务端心跳前端每 15 秒 POST 一次/api/exam/heartbeat?examIdxxxtimestampxxx服务端比对上次心跳时间若间隔 25 秒即标记该考生“疑似切屏”并在监考后台高亮告警。这个心跳包 payload 极简不带 Session ID防 CSRF只校验examId是否合法及时间戳是否在合理窗口内±30 秒。3.3 实时交卷状态用 Servlet 3.0 异步支持替代 WebSocket很多教程一上来就推 WebSocket但某公司内部培训系统实测发现当 500 人同时交卷WebSocket 服务端用 Tomcat 默认 WebSocket 实现出现大量Connection reset原因是默认maxTextMessageBufferSize仅 8KB而交卷时考生答案 JSON 可能达 200KB含多张截图上传 base64。改用 Servlet 3.0 异步机制更稳WebServlet(urlPatterns /api/exam/submit, asyncSupported true) public class ExamSubmitServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { AsyncContext asyncCtx req.startAsync(); // 启动异步上下文 asyncCtx.setTimeout(30_000); // 设超时防死锁 // 提交到线程池避免阻塞 Tomcat 工作线程 ExamSubmitTask task new ExamSubmitTask(req, resp, asyncCtx); ThreadPool.submit(task); } } // ExamSubmitTask.run() 中 // 1. 解析 request.getInputStream 得到答案 JSON // 2. 校验答案格式、防重复提交查 exam_record.status // 3. 写入 exam_answer 表含考生ID、题目ID、选项、文本答案、图片base64摘要 // 4. 更新 exam_record.status submitted // 5. asyncCtx.complete();注意ThreadPool必须是自定义有界队列如new ThreadPoolExecutor(10, 50, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(100))否则高并发下 OOM。我们曾因用Executors.newCachedThreadPool()导致交卷高峰时创建 2000 线程最终 Full GC 频繁。4. 避坑指南那些让项目延期三天、被教务处打电话催的 4 个真实翻车现场4.1 现象考生点击“交卷”后页面白屏F12 看 Network 显示 500但日志里没有任何错误堆栈原因web.xml中error-page配置了全局 500 错误跳转到error.jsp但该 JSP 里用了${sessionScope.user.name}而交卷成功后 Session 被session.invalidate()清除EL 表达式取值时报NullPointerException触发二次 500形成死循环。解决error.jsp中所有 EL 表达式必须判空或改用 JSTLc:if test${not empty sessionScope.user}更重要的是在ExamSubmitServlet的catch块中显式设置asyncCtx.getResponse().sendError(500, 交卷失败请重试)绕过web.xml全局配置确保错误响应体可控。4.2 现象MySQL 主从延迟导致监考后台看到“已交卷”但考生手机端仍显示“考试中”原因交卷逻辑中先更新exam_record.status再插入exam_answer记录两个操作不在同一事务。主库写完后从库同步有秒级延迟监考后台查从库exam_record看到新状态但查exam_answer也走从库却查不到——因为exam_answer插入语句还没同步过来。解决强制读写分离策略——监考后台所有“考生状态”相关查询包括exam_record和exam_answer全部走主库。在 MyBatis 的SqlSessionFactoryBean配置中为监考模块 Mapper 接口指定master数据源考生端查询如查看历史成绩才走slave。不要迷信“最终一致性”考试状态必须强一致。4.3 现象Chrome 浏览器下考生上传图片后预览正常但交卷后服务端收到的 base64 字符串末尾多了%0A换行符原因前端用FileReader.readAsDataURL(file)生成 base64但某些 Chrome 版本如 92在处理 PNG 文件时会在 data URL 的逗号后自动插入\n导致后端Base64.getDecoder().decode()报IllegalArgumentException: Illegal base64 character a。解决前端在提交前清洗字符串const cleanBase64 (str) str.replace(/^data:image\/\w;base64,/, ).replace(/\s/g, ); // 提交时用 cleanBase64(dataUrl)服务端接收后再做一次base64Str base64Str.replaceAll([^A-Za-z0-9/], );双保险。4.4 现象Tomcat 重启后考生再次进入考试页面提示“试卷已结束”但数据库exam_paper.status明明是 1启用原因exam_record表中start_time是datetime类型但 Java 代码中用new Date()创建时间存入数据库时依赖 JDBC 驱动的时区转换。若服务器系统时区为Asia/Shanghai而 MySQL 服务端time_zone设置为SYSTEM即跟随系统但 JDBC URL 未加serverTimezoneGMT%2B8则驱动会把new Date()当作 UTC 时间写入导致start_time比实际晚 8 小时。解决JDBC URL 必须显式声明时区jdbc:mysql://localhost:3306/examdb?useSSLfalseserverTimezoneGMT%2B8characterEncodingutf8并在应用启动时用System.setProperty(user.timezone, GMT8);统一时区上下文。5. 真实压测与上线技巧如何用一台 4C8G 服务器扛住 1200 人同时考试5.1 Tomcat 调优不是堆内存越大越好而是线程与连接数的黄金配比某实验室模拟 1200 人考试初始配置默认maxThreads200下交卷高峰时平均响应时间飙升至 8 秒。我们通过jstack发现大量线程阻塞在org.apache.tomcat.util.net.NioEndpoint$Poller.events()说明 NIO Poller 处理不过来。调整策略不是盲目加maxThreads而是优化 I/O 模型!-- server.xml -- Connector port8080 protocolorg.apache.coyote.http11.Http11Nio2Protocol maxThreads500 minSpareThreads100 acceptCount500 connectionTimeout20000 keepAliveTimeout15000 maxKeepAliveRequests100 compressionon compressionMinSize2048 noCompressionUserAgentsgozilla, traviata compressableMimeTypetext/html,text/xml,text/plain,application/javascript,application/json/关键参数解释maxThreads500足够应对 1200 并发按 2.4:1 的并发比即 1200 请求中约 500 个是活跃线程acceptCount500当所有线程忙时最多排队 500 个连接请求超出则拒绝比让客户端无限等待更友好Http11Nio2Protocol比默认NioProtocol多路复用效率更高尤其适合长连接心跳compression开启 GZIP考试页面 HTML/JS 体积减少 65%显著降低网络传输耗时。提示maxThreads不宜超过 CPU 核数 × 4本例 4C → 16否则线程上下文切换开销反超收益。我们实测 500 是平衡点——再往上加top显示%sy系统态 CPU飙升但吞吐量不增反降。5.2 MySQL 专项优化针对exam_answer表的写入瓶颈exam_answer是写入热点表每人每题一条记录1200 人 × 平均 50 题 6 万条 INSERT。默认innodb_buffer_pool_size128M完全不够。我们按以下步骤调优项目原值调优后作用innodb_buffer_pool_size128M4G缓存数据页减少磁盘 IOinnodb_log_file_size48M512M大日志文件降低 checkpoint 频率提升写入吞吐innodb_flush_log_at_trx_commit12每次事务仅刷 OS cache非磁盘写入快 3 倍牺牲极小概率断电丢 1 秒数据考试场景可接受sync_binlog10关闭 binlog 强制刷盘若无需主从或用 GTID 异步复制执行后INSERT INTO exam_answer的平均耗时从 120ms 降至 18ms。注意修改innodb_log_file_size需先SET GLOBAL innodb_fast_shutdown 0;再停库删旧 log 文件否则启动失败。5.3 前端资源加载用 Webpack 打包 Nginx 静态托管首屏提速 40%考试系统首页登录页、考试入口页必须秒开。我们放弃在 Tomcat 里放静态资源改用 Nginx 托管# nginx.conf server { listen 80; server_name exam.example.com; # 静态资源走 Nginx location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /opt/static/exam/; expires 1y; add_header Cache-Control public, immutable; } # 动态请求转发给 Tomcat location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }前端构建用 Webpack 5关键配置optimization.splitChunks拆出vendor.jsjQuery、Bootstrap单独缓存HtmlWebpackPlugin注入defer属性防阻塞渲染所有 CSS 提取为.css文件link relpreload预加载关键样式。实测 2G 带宽下首页完全加载时间从 3.2s 降至 1.9s考生放弃率下降 22%。6. 我的三个上线前必做动作从代码到考场的真实闭环6.1 “三色名单”压力测试法用真实数据跑出瓶颈点不靠 JMeter 虚拟用户而是用真实考生数据做三轮测试红色名单100 个账号密码全为123456模拟弱口令暴力破解验证登录接口限流Guava RateLimiter 每 IP 每分钟 5 次蓝色名单500 个账号分布在 5 个考场用 Python 脚本模拟真实行为登录 → 进入考试 → 等待 30 秒 → 随机答 5 题 → 交卷观察 Tomcathttp-nio-8080-exec-*线程数是否稳定在 400 以内绿色名单200 个账号全部启用“防切屏”模式脚本模拟 AltTab 切出再切回验证服务端心跳日志是否每 15 秒一条无丢失。这比任何理论 QPS 数字都管用。某次上线前蓝色名单测试暴露出exam_record表缺少exam_id status联合索引导致listByExamIdAndStatus查询全表扫描加索引后响应从 2.1s 降到 42ms。6.2 监考后台的“后悔药”设计一键回滚交卷状态考试中总有意外考生点错交卷、网络抖动重复提交、监考员误操作踢人。我们不提供“撤回”按钮怕误点而是设计原子化回滚指令在监考后台“考生列表”页对任一考生右键菜单增加【恢复考试】点击后后端执行事务START TRANSACTION; UPDATE exam_record SET status in_progress, end_time NULL WHERE id ? AND status submitted; DELETE FROM exam_answer WHERE record_id ?; -- 清空已答记录 COMMIT;同时向该考生 WebSocket或轮询接口推送{ action: resume_exam, newStartTime: ... }前端清空本地答案并重置倒计时。这个操作全程 200ms且幂等重复点无副作用是教务老师最常点的功能。6.3 日志分级与考场隔离让问题定位从“大海捞针”变成“直击靶心”线上出问题第一反应不是看catalina.out而是查结构化日志。我们在 Logback 中配置exam-access.log记录所有/api/**请求字段含examId,userId,ip,costMs,statusexam-audit.log记录关键业务动作如SUBMIT_EXAM_SUCCESS,KICK_STUDENT,TIME_SYNC含完整参数 JSONexam-error.log仅 ERROR 级别但每条含examIdMDCMapped Diagnostic Context查询时只要拿到考场 ID 或考生学号就能用grep examId12345 exam-audit.log5 秒内拉出该考生完整操作链。某次凌晨故障教务处电话打来“3号考场王同学说交卷没反应”我 SSH 登服务器grep 3021 exam-access.log | tail -20发现他最后 3 次交卷请求costMs分别是 12000、13500、14800明显是数据库连接池耗尽立刻curl http://localhost:8080/actuator/env?matchdruid确认连接数扩容后 3 分钟恢复——没有日志分级这过程至少半小时。我带过的每个 JavaWeb 考试系统项目上线前最后一小时一定在做三件事跑三色名单、导出最近 10 分钟exam-access.log人工扫一遍 500 错误、用jstat -gc pid看 GC 是否平稳。这些动作不酷但能让你在考场上接到第一个电话时语气平静地说“稍等我马上定位”。希望帮到你。本文还有配套的精品资源点击获取