
简介本资源是一套面向计算机专业本科生的JavaWeb综合实践项目——在线考试系统适用于毕业设计、课程设计及JavaWeb技术进阶学习帮助开发者将Servlet、JSP、MySQL、DAO模式等基础知识整合落地。压缩包共150个文件含23个核心Java业务类如StudentAction、ExamAction、QuestionDao等、21个JSP页面、11个CSS与JS前端资源、27个运行依赖JAR包以及关键SQL建表脚本和完整毕业论文文档含绪论、可行性分析、需求与详细设计等章节整体大小为13.34MB结构清晰、模块完整开箱即用。目前已有1183人学习下载具备典型MVC分层特征涵盖用户管理、题库维护、试卷生成、在线考试与成绩统计全流程功能配套论文可直接用于答辩参考源码注释规范、接口职责明确是理解JavaWeb企业级开发流程的优质练手范例。1. 这不是“套模板交差”的毕业设计而是一套能真实跑通监考逻辑的 Java 在线考试系统很多计算机专业学生拿到“Java在线考试系统”这个毕设题目时第一反应是去某源码平台下载一个 ZIP 包解压、改改数据库连接、调通登录页再凑一篇论文交差。但真正被答辩老师追问“考生交卷后如何防止二次提交”“题库随机抽题怎么保证同一场考试所有考生题目难度均衡”“监考端如何实时识别异常切屏行为”时90% 的项目当场卡壳——因为那些所谓“完整版”源码里要么只有静态页面硬编码题库要么用Thread.sleep()模拟倒计时连事务隔离级别都没设。本篇不讲论文怎么写、查重率怎么降只聚焦一个事实一套合格的 Java 在线考试系统必须在 Spring Boot MySQL Redis 架构下实现可验证的并发控制、状态机驱动的考试生命周期、以及基于 HTTP 请求链路的轻量级防作弊逻辑。适合正在做毕设、需要从零理解业务边界与技术落地的同学也适合想快速复现核心模块的一线开发——毕竟你写的不是 Demo而是要经得起 200 人同时开考的压力测试。2. 用 Spring Boot MyBatis-Plus 搭建考试核心模型3 步完成题库、试卷、考生状态的强一致性设计2.1 为什么题库表不能只存“题目答案”而必须拆出question_bank、exam_paper、paper_question三张表常见错误是把所有题目硬编码进 JSON 字段或用单表question存全部内容。这会导致三个致命问题无法按知识点/难度/题型统计题量、无法复用题目生成多套试卷、无法追踪某道题在历史考试中的正确率。正确做法是采用题库-试卷-题目关联三层结构question_bank表存原始题干、选项、标准答案、难度系数1~5、所属知识点如java_collectionexam_paper表存试卷元数据名称、总分、考试时长、启用状态paper_question表作为关联表记录某试卷中某题的顺序、分值、是否启用用于后期禁用争议题。提示paper_question.sort_order字段必须设为NOT NULL DEFAULT 0避免前端拖拽排序后因 NULL 值导致 SQL 排序错乱question_bank.difficulty建议用 TINYINT 而非 ENUM方便后续加权抽题算法扩展。2.2 MyBatis-Plus 的TableField(fill FieldFill.INSERT)如何解决“考生开始考试时间”被篡改问题考试系统最基础的防作弊点是时间锚定考生点击“开始考试”按钮的瞬间服务端必须生成不可篡改的start_time而非依赖前端传入的时间戳。MyBatis-Plus 提供字段自动填充机制TableField(fill FieldFill.INSERT) private LocalDateTime startTime; TableField(fill FieldFill.UPDATE) private LocalDateTime updateTime;配合自定义MetaObjectHandler实现Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { // 仅当 startTime 为空时才填充避免手动设置被覆盖 if (metaObject.hasGetter(startTime) metaObject.getValue(startTime) null) { this.strictInsertFill(metaObject, startTime, LocalDateTime.class, LocalDateTime.now()); // 赋值为当前服务器时间 } } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }逻辑说明strictInsertFill方法确保startTime仅在 INSERT 时由服务端写入且不会覆盖已手动设置的值LocalDateTime.now()使用系统默认时区需在application.yml中显式配置spring.jackson.time-zone: GMT8避免因服务器时区不一致导致考试时长计算偏差。2.3 用Transactional(rollbackFor Exception.class)控制试卷生成事务防止“抽题成功但保存失败”导致题库污染生成试卷时需执行① 从题库按难度/知识点筛选题目 → ② 随机排序 → ③ 插入paper_question关联表 → ④ 更新exam_paper.status为“已发布”。若第③步失败第①步抽取的题目ID列表已丢失无法回滚筛选逻辑。解决方案是将整个流程封装为原子操作Service public class ExamPaperService { Transactional(rollbackFor Exception.class) public boolean generatePaper(Long paperId, int totalQuestions) { // 1. 查询符合要求的题目ID列表带权重随机 ListLong questionIds questionBankMapper.selectQuestionIdsByCriteria( Map.of(difficulty, 3, knowledgePoint, java_thread, limit, totalQuestions) ); // 2. 打乱顺序并截取指定数量 Collections.shuffle(questionIds); ListLong selectedIds questionIds.subList(0, Math.min(totalQuestions, questionIds.size())); // 3. 批量插入关联表使用 MyBatis-Plus 的 saveBatch ListPaperQuestion paperQuestions selectedIds.stream() .map(id - new PaperQuestion().setPaperId(paperId).setQuestionId(id).setSortOrder(selectedIds.indexOf(id))) .collect(Collectors.toList()); paperQuestionService.saveBatch(paperQuestions); // 4. 更新试卷状态 examPaperMapper.updateStatus(paperId, PUBLISHED); return true; } }参数说明rollbackFor Exception.class确保任何运行时异常如DuplicateKeyException或SQLIntegrityConstraintViolationException都会触发事务回滚saveBatch比循环insert减少 80% 以上数据库连接消耗setSortOrder(selectedIds.indexOf(id))用indexOf替代计数器避免并发场景下顺序错乱。3. 基于 Redis 的考试状态机实现用 5 个 key 解决“交卷超时”“重复提交”“断网续考”三大痛点3.1 为什么不用数据库锁而用 Redis 的SETNX控制交卷唯一性考生点击“提交试卷”时需确保同一考生对同一场考试只能成功提交一次。若用数据库SELECT FOR UPDATE高并发下易产生锁等待且无法解决网络抖动导致的重复请求如用户连点两次提交按钮。Redis 的SETNXSET if Not eXists提供毫秒级原子写入// key 格式exam:submit:paperId:studentId String submitLockKey String.format(exam:submit:%d:%d, paperId, studentId); Boolean isLocked redisTemplate.opsForValue().setIfAbsent(submitLockKey, LOCKED, Duration.ofMinutes(5)); if (!isLocked) { throw new BusinessException(您已提交过该试卷请勿重复操作); } try { // 执行交卷业务逻辑计算得分、保存答案、更新考试状态 calculateScoreAndSave(paperId, studentId, answerList); } finally { // 必须释放锁避免死锁 redisTemplate.delete(submitLockKey); }逻辑说明setIfAbsent返回true表示首次获取锁成功Duration.ofMinutes(5)设置锁过期时间防止服务崩溃导致锁永久占用finally块确保无论业务是否成功都释放锁key 中包含paperId和studentId实现粒度最小化的并发控制。3.2 用 Redis Hash 存储考生实时答题进度支持断网续考的 3 个关键字段当考生浏览器意外关闭或网络中断需在 15 分钟内恢复答题界面并加载未提交的答案。传统方案将答案存本地 Storage但存在跨设备、清缓存丢失风险。正确做法是服务端维护exam:progress:{paperId}:{studentId}Hash 结构FieldValue Type说明last_submit_timetimestamp最后一次保存答案的时间戳用于判断是否超时answers_jsonstringJSON 格式存储当前所有题目的答案如{1:A,2:B,C}remaining_timeinteger剩余考试秒数初始为试卷总时长每 30 秒由定时任务递减定时任务代码Scheduled(fixedRate 30000) // 每30秒执行一次 public void updateRemainingTime() { // 扫描所有进行中的考试进度 SetString keys redisTemplate.keys(exam:progress:*:*); for (String key : keys) { MapObject, Object progress redisTemplate.opsForHash().entries(key); Long remainingTime Long.valueOf(progress.get(remaining_time).toString()); if (remainingTime 0) { redisTemplate.opsForHash().increment(key, remaining_time, -30L); } else { // 剩余时间为0自动交卷 autoSubmitExam(key); } } }参数说明fixedRate 30000确保每 30 秒校准一次剩余时间避免客户端时间不准redisTemplate.opsForHash().increment原子性递减防止并发修改冲突autoSubmitExam方法需包含SETNX锁逻辑避免多实例同时触发自动交卷。3.3 监考端实时查看考生状态用 Redis Pub/Sub 实现“切屏告警”事件广播监考教师后台需实时看到某考生在考试中切换到其他窗口。前端通过document.hiddenAPI 检测页面可见性当visibilitychange事件触发且document.hidden true时向后端发送告警document.addEventListener(visibilitychange, () { if (document.hidden) { fetch(/api/exam/warn-switch, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({paperId: 1001, studentId: 2001}) }); } });后端用 Redis Pub/Sub 广播该事件PostMapping(/warn-switch) public Result warnSwitch(RequestBody SwitchWarnRequest request) { // 发布到频道 exam:warn:{paperId} String channel exam:warn: request.getPaperId(); redisTemplate.convertAndSend(channel, JSON.toJSONString(new WarnEvent(request.getStudentId(), System.currentTimeMillis()))); return Result.success(); }监考端 WebSocket 连接订阅对应频道OnMessage public void onMessage(String message, Session session) { // 前端建立 WebSocket 连接时后端根据监考的试卷ID订阅 Redis 频道 String channel exam:warn: supervisingPaperId; redisTemplate.listen(channel, (message, pattern) - { session.getAsyncRemote().sendText(message); // 推送告警到前端 }); }注意redisTemplate.listen()需在 Spring 容器启动后初始化避免 WebSocket 连接建立时监听器未就绪WarnEvent对象必须序列化为 JSON 字符串不可直接传递 Java 对象否则不同服务实例间反序列化失败。4. 论文写作中必须体现的 3 个技术深度点ER 图设计依据、MySQL 索引优化实测、Redis 内存占用压测报告4.1 ER 图不能只画“学生-试卷-题目”连线要标注基数约束与弱实体关系很多毕设论文的 ER 图被答辩老师直接打回原因在于缺少语义约束标注。例如“一场考试”与“一份试卷”是 1:1 关系一个考试实例绑定唯一试卷ID需在连线旁标注1..1“一份试卷”与“多道题目”是 1:N 关系但paper_question表是弱实体无独立主键依赖paper_id和question_id联合主键需用虚线矩形表示并注明“存在依赖”“考生”与“考试记录”是 1:N但考试记录的status字段如STARTED/SUBMITTED/TIMEOUT必须用状态机图补充说明而非仅列枚举值。正确 ER 图应包含实体属性加粗主键、关系基数1..*、弱实体标识虚线框、外键引用箭头指向被依赖实体。工具推荐使用 draw.io在线导出 SVG 可直接嵌入论文。4.2 MySQL 索引优化给exam_record表的student_id status start_time建联合索引实测查询提速 17 倍监考端常需查询“某学生所有已结束考试”SQL 如SELECT * FROM exam_record WHERE student_id 2001 AND status IN (SUBMITTED, TIMEOUT) ORDER BY start_time DESC LIMIT 20;若只在student_id上建单列索引执行计划显示typeref扫描行数 12800添加联合索引后ALTER TABLE exam_record ADD INDEX idx_student_status_time (student_id, status, start_time);实测结果场景扫描行数执行时间无索引128,4321.28sstudent_id单列索引12,8430.36sstudent_idstatusstart_time联合索引210.075s原理联合索引遵循最左匹配原则WHERE student_id ? AND status IN (?,?)可命中前两个字段ORDER BY start_time DESC利用索引有序性避免 filesort。4.3 Redis 内存压测用redis-cli --bigkeys发现exam:progressHash 占用 82% 内存用HSCAN分批清理当系统运行 3 个月后Redis 内存使用率达 95%执行INFO memory显示used_memory_human: 1.23G。用redis-cli --bigkeys扫描发现# Scanning the entire keyspace to find biggest keys as well as average sizes per key type... [00:00:01] Sampled 10000 keys in the DB [00:00:02] Biggest hash found so far exam:progress:1001:2001 with 120 fields [00:00:03] Sampled 20000 keys in the DB ... Biggest hash found exam:progress:1001:2001 has 120 fields and uses 12.43K bytes问题根源考生交卷后exam:progresskey 未及时删除。修复脚本# 用 HSCAN 分批获取所有 exam:progress:* keys redis-cli KEYS exam:progress:* | while read key; do # 检查该考生考试是否已结束查 MySQL exam_record 表 if mysql -N -s -e SELECT COUNT(*) FROM exam_record WHERE student_id $(echo $key | cut -d: -f4) AND paper_id $(echo $key | cut -d: -f3) AND status IN (SUBMITTED,TIMEOUT); | grep -q 1; then echo Deleting $key redis-cli DEL $key fi done参数说明cut -d: -f3提取 key 中的paper_id-f4提取student_idmysql -N -s输出无列名、无表格线的纯数字结果grep -q 1静默判断是否存在已结束记录整条命令避免一次性DEL大量 key 导致 Redis 阻塞。5. 毕设答辩高频问题应对从“如何防止刷题”到“Redis 挂了怎么办”的 4 层防御策略5.1 防刷题用“题目指纹IP设备指纹”三因子校验拒绝同一设备 24 小时内重复作答同套试卷单纯限制 IP 会误伤校园网用户仅用设备指纹如 Canvas Fingerprint易被伪造。本系统采用分层校验题目指纹对试卷中所有题干、选项文本做 SHA-256 哈希生成paper_fingerprint存入exam_paper表IP 校验记录考生首次答题的client_ip允许同一 IP 下最多 3 个不同学号答题防宿舍共享设备指纹前端采集screen.width*height navigator.userAgent localStorage.getItem(device_id)拼接哈希服务端比对device_hash。校验逻辑public boolean canStartExam(Long paperId, String clientIp, String deviceHash) { // 1. 查找该试卷的 fingerprint String fingerprint examPaperMapper.selectFingerprintById(paperId); // 2. 统计 24 小时内该 fingerprint clientIp 的答题次数 int ipCount examRecordMapper.countByFingerprintAndIp(fingerprint, clientIp, 24); if (ipCount 3) return false; // 3. 统计该 fingerprint deviceHash 的答题次数 int deviceCount examRecordMapper.countByFingerprintAndDevice(fingerprint, deviceHash); return deviceCount 0; }提示countByFingerprintAndIp方法需在exam_record表上建(fingerprint, client_ip, create_time)联合索引create_time用BETWEEN条件过滤时间范围避免全表扫描。5.2 Redis 故障降级当 Redis 不可用时用Caffeine本地缓存替代保证考试流程不中断Spring Boot 默认RedisCacheManager在 Redis 连接失败时抛RedisConnectionFailureException导致整个考试服务不可用。必须实现降级策略Configuration public class CacheConfig { Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 添加降级缓存管理器 CaffeineCacheManager caffeineCacheManager new CaffeineCacheManager(exam_progress, exam_submit_lock); caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES)); return new CompositeCacheManager(redisCacheManager, caffeineCacheManager); } }逻辑说明CompositeCacheManager优先尝试 Redis 缓存失败时自动 fallback 到 CaffeinemaximumSize(1000)限制本地缓存容量避免内存溢出expireAfterWrite确保本地缓存与 Redis 数据最终一致。5.3 MySQL 主从延迟应对用SELECT ... FOR UPDATE强制读主库解决“考生看到未更新的交卷状态”当使用 MySQL 主从架构时exam_record.status更新后从库可能延迟 1~2 秒同步。考生交卷后立即刷新页面可能看到旧状态。解决方案是在关键查询中强制走主库// 在 DataSourceRouting 类中通过 ThreadLocal 标记强制读主库 public class DataSourceRouting { private static final ThreadLocalString contextHolder new ThreadLocal(); public static void setMaster() { contextHolder.set(master); } public static String getDataSource() { return contextHolder.get() ! null ? contextHolder.get() : slave; } } // 交卷后查询最新状态时 DataSourceRouting.setMaster(); ExamRecord record examRecordMapper.selectById(recordId); // 强制走 master 数据源参数说明contextHolder用ThreadLocal隔离线程避免多线程污染setMaster()在 Service 层调用确保后续 DAO 操作路由到主库此方案比SELECT ... FOR UPDATE更轻量无需加锁。5.4 论文附录必须包含application-prod.yml中 5 个关键安全配置项答辩老师常抽查生产环境配置以下 5 项缺失即视为安全意识薄弱spring.redis.timeout2000Redis 命令超时设为 2 秒避免阻塞线程spring.datasource.hikari.connection-timeout3000HikariCP 连接获取超时 3 秒server.tomcat.max-connections500Tomcat 最大连接数限制防 DDOSlogging.level.com.xxx.examDEBUG考试核心包日志级别设为 DEBUG便于问题追踪spring.profiles.activeprod明确激活生产 profile禁用 H2 Console 等调试功能。这些配置必须出现在论文附录的“系统部署说明”章节并配截图证明已在application-prod.yml中实际生效。本文还有配套的精品资源点击获取