news 2026/9/23 11:20:52

Java在线考试系统:高并发、防作弊与可落地的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java在线考试系统:高并发、防作弊与可落地的工程实践

简介:本资源是一套完整的基于Java的在线考试管理系统毕业设计实践材料,面向计算机专业本科生、Java初学者及Web开发入门学习者,解决课程设计、毕设选题与全栈项目实操需求。压缩包共含1.39MB,虽未提供具体文件数量,但明确包含源代码、项目报告、开题报告、外文翻译、英文文献及答辩PPT等核心交付物——源代码体现Spring或Struts框架下的MVC分层实现;项目报告详述系统需求分析、数据库ER设计(MySQL)、试题库管理逻辑与自动评分机制;开题报告与答辩PPT支撑学术规范表达;外文翻译与英文文献则强化技术视野与文献调研能力。已有59人学习下载,内容覆盖从环境搭建、功能模块开发到文档撰写全流程,结构完整、文档齐备,特别适合用于理解Java Web项目工程化落地路径、掌握考试系统典型业务闭环(组卷→考试→阅卷→反馈)及提升毕业设计综合交付能力。

1. 这不是又一个“学生管理系统”:一个能真正在教务场景跑通的 Java 在线考试管理项目,到底要解决什么?

你手头这个压缩包里写着“基于Java的在线考试管理项目设计与实现(源代码+项目报告+开题报告+外文翻译+英文文献+答辩PPT)”,但别急着解压——先问一句:它真能应付一次300人同时登录、50道题随机组卷、防切屏+时间倒计时+自动收卷+客观题秒批+主观题分题归档的期末考吗?很多所谓“Java课程设计”项目,跑在localhost:8080上连登录页都卡顿,数据库用H2硬编码,试卷生成靠手动填表单,监考端就是个静态HTML。而真正能落地的在线考试系统,核心不在“用了Spring Boot还是SSM”,而在并发承载力、状态一致性、考试过程不可逆性、阅卷链路可追溯性这四根钢丝上。本项目不是教学Demo,而是按高校教务处真实需求反向推导出的技术方案:用Java生态稳扎稳打,不堆新潮框架,但每个模块都经得起压力测试和流程审计。适合两类人:一是需要交差但不想被答辩老师问住的本科生(你得知道为什么选MyBatis而不是JPA,为什么用Redis缓存试卷ID而不是Session),二是想快速搭建校内轻量级考试平台的IT运维或教务老师(它不依赖云服务,部署在一台4核8G物理机上就能撑住千人场次)。下面,我们就从“为什么这样设计”开始,一砖一瓦复现这个系统怎么从零跑起来。


2. 从需求到架构:为什么选 Spring Boot + MyBatis + Redis + MySQL 而不是“全家桶”

2.1 教务场景的硬约束,决定了技术栈必须“够用且可控”

在线考试系统不是电商网站,它没有秒杀、没有推荐算法、不需要实时消息推送。它的核心压力点非常明确:考试开始瞬间的并发登录、组卷请求洪峰、提交时的事务强一致性、阅卷后的数据不可篡改。我见过太多学生项目用Spring Cloud搭微服务,结果连单机MySQL连接池都配不对,考试中途数据库直接挂掉。所以本项目采用“稳态架构”:Spring Boot 2.7.x(JDK 17兼容性好,避免java: 警告: 源发行版 17 需要目标发行版 17这类编译陷阱),MyBatis-Plus 3.5.x(比纯MyBatis少写70% XML,又比JPA更贴近SQL控制权),MySQL 5.7(事务隔离级别设为REPEATABLE READ,保证阅卷期间成绩不被误更新),Redis 6.2(仅作三类缓存:未开始考试的试卷ID列表、考生当前答题会话Token、防重复提交的答题Key)。这里不引入RabbitMQ或Kafka——阅卷结果写库是强事务,异步落库会导致成绩延迟甚至丢失;也不用Elasticsearch——试卷搜索频次极低,LIKE查询加索引足够。所有技术选型都指向一个目标:让教务老师能看懂日志、运维能快速定位慢SQL、开发能三天内修复一个阅卷逻辑Bug

2.2 模块划分不是按MVC分层,而是按考试生命周期切片

很多初学者把项目分成controller/service/dao三层,结果写完发现“考试监控”功能散落在十几个Service里。本项目按真实业务流拆解为6个垂直模块:

模块名核心职责关键技术点为什么不能合并
exam-core试卷生成、题库管理、组卷策略权重抽题算法、题型权重配置表组卷逻辑涉及大量SQL聚合,与用户权限无关
exam-auth考生/教师/管理员三级登录、Token签发JWT+Redis黑名单、密码BCrypt加密登录失败锁定需独立计数器,不能耦合在用户中心
exam-process考试倒计时、切屏检测、强制交卷、异常中断续考WebSocket心跳保活、前端visibilitychange监听、服务端答题状态快照倒计时必须服务端校验,防止客户端篡改
exam-marking客观题自动批改、主观题分题分配、评卷进度看板多线程批改池、Redis分布式锁控制评卷分配主观题分配需避免同一题被两人同时评,锁粒度必须到“题ID+考生ID”
exam-report成绩单生成、班级统计报表、错题TOP10分析POI导出Excel、MySQL窗口函数计算排名报表SQL复杂度高,单独模块便于SQL优化
exam-admin考试计划排期、考场分配、监考员指派、成绩复核工单日历组件后端渲染、工单状态机(Draft→Assigned→Reviewed→Closed)排期涉及资源冲突检测,需独立事务边界

提示:所有模块共享同一个common基础包,但禁止跨模块直接调用Service方法。模块间通信只允许通过定义好的DTO和事件(如ExamStartedEvent),这是为了后续万一要拆成独立服务留出余地。

2.3 数据库设计:一张试卷表如何扛住万人并发组卷

关键不是建多少张表,而是主键、索引、字段类型怎么选才能让组卷SQL在100ms内返回。以最核心的exam_paper(试卷表)为例:

CREATE TABLE `exam_paper` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `paper_code` varchar(32) NOT NULL COMMENT '试卷编码,业务主键,如EXAM20240501001', `exam_id` bigint NOT NULL COMMENT '所属考试ID', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0-未发布,1-已发布,2-已结束', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_paper_code` (`paper_code`), KEY `idx_exam_status` (`exam_id`,`status`) COMMENT '组卷时按考试ID+状态查未发布试卷', KEY `idx_create_time` (`create_time`) COMMENT '按创建时间倒序分页' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='试卷主表';

注意三个细节:

  • paper_code设为唯一索引而非主键——因为组卷时前端传的是业务编码,直接查WHERE paper_code = ?比查id更安全(避免ID泄露);
  • idx_exam_status是联合索引,且exam_id在前——因为组卷逻辑是“查某场考试下所有未发布的试卷”,MySQL能用上索引下推(ICP);
  • create_time单独建索引——成绩导出时常用“按创建时间排序”,避免filesort。

再看题库表question_bank,它不用自增ID,而用question_hash(MD5(题干+选项))作主键:

CREATE TABLE `question_bank` ( `question_hash` char(32) NOT NULL COMMENT '题目内容哈希值,主键', `content` text NOT NULL COMMENT '题干JSON,含选项/答案/解析', `type` tinyint NOT NULL COMMENT '1-单选,2-多选,3-判断,4-填空', `difficulty` tinyint NOT NULL DEFAULT '2' COMMENT '难度1-5', `subject_id` int NOT NULL COMMENT '所属学科ID', PRIMARY KEY (`question_hash`), KEY `idx_subject_diff` (`subject_id`,`difficulty`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这样设计,组卷时用INSERT IGNORE INTO exam_paper_question SELECT ... FROM question_bank WHERE subject_id = ? AND difficulty BETWEEN ? AND ? ORDER BY RAND() LIMIT 50就能避免重复题——因为哈希值相同即题目完全一致,INSERT IGNORE天然去重。比用NOT EXISTS子查询快3倍以上。


3. 关键功能落地:用最少代码实现最稳的考试过程控制

3.1 防切屏+倒计时:前端不可信,服务端必须兜底

很多项目只在前端监听页面可见性(document.visibilityState),但用户Alt+Tab后切回页面,visibilitychange事件可能延迟触发,导致作弊窗口有3-5秒空白期。本方案采用双保险机制

  1. 前端每10秒上报一次心跳(含当前页面标题、URL、是否聚焦):
// exam-process/src/main/resources/static/js/exam.js setInterval(() => { if (document.hidden) { // 页面隐藏时立即上报 sendHeartbeat({ focused: false, url: window.location.href }); } else { // 页面可见时也上报,用于校验时间差 sendHeartbeat({ focused: true, url: window.location.href }); } }, 10000); function sendHeartbeat(data) { fetch('/api/heartbeat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...data, timestamp: Date.now(), // 客户端时间戳 token: localStorage.getItem('examToken') }) }); }
  1. 服务端用Redis记录每次心跳,并校验时间漂移
// ExamProcessService.java public void handleHeartbeat(String token, boolean focused, long clientTimestamp) { String key = "heartbeat:" + token; // 获取上次心跳时间 Long lastTime = redisTemplate.opsForValue().get(key, Long.class); if (lastTime != null && Math.abs(System.currentTimeMillis() - clientTimestamp) > 5000) { // 客户端时间与服务端偏差超5秒,视为异常(可能篡改了JS) throw new BusinessException("客户端时间异常,请检查系统时间"); } // 更新心跳时间 redisTemplate.opsForValue().set(key, System.currentTimeMillis(), Duration.ofMinutes(2)); // 若页面隐藏且持续超30秒,标记为疑似作弊 if (!focused) { String hiddenKey = "hidden:" + token; Long hiddenCount = redisTemplate.opsForValue().increment(hiddenKey, 1L); if (hiddenCount >= 3) { // 连续3次心跳上报隐藏状态 examLogService.logSuspiciousEvent(token, "PAGE_HIDDEN_CONTINUOUS"); } } }

注意:倒计时绝对不能只靠前端setInterval。服务端在考生进入考试时,将exam_start_timeexam_duration存入Redis,每次答题提交时校验System.currentTimeMillis() - exam_start_time > exam_duration,超时则强制交卷并标记status=TIMEOUT

3.2 随机组卷:不是简单ORDER BY RAND(),而是预生成+缓存命中

ORDER BY RAND()在百万题库中会全表扫描,组卷耗时从200ms飙升到3s。本项目采用预生成策略

  1. 启动时加载所有启用学科的题目哈希值到内存(约20MB):
// QuestionCacheLoader.java @PostConstruct public void loadQuestionCache() { List<String> hashes = questionBankMapper.selectActiveHashes(); // 按学科ID分组缓存 Map<Integer, List<String>> subjectHashes = hashes.stream() .collect(Collectors.groupingBy(hash -> { // 从hash反查subject_id(实际用布隆过滤器优化,此处简化) return getSubjectIdByHash(hash); })); questionCache.putAll(subjectHashes); }
  1. 组卷时从对应学科缓存中随机取样,再查DB补全题目详情:
// PaperGenerator.java public ExamPaper generatePaper(Long examId, Integer subjectId, Integer questionCount) { List<String> availableHashes = questionCache.get(subjectId); if (availableHashes == null || availableHashes.size() < questionCount) { throw new BusinessException("题库不足,无法组卷"); } // 随机抽取(Fisher-Yates洗牌) Collections.shuffle(availableHashes); List<String> selectedHashes = availableHashes.subList(0, questionCount); // 批量查题(避免N+1) List<QuestionBank> questions = questionBankMapper.selectByHashes(selectedHashes); // 构建试卷 ExamPaper paper = new ExamPaper(); paper.setExamId(examId); paper.setPaperCode("EXAM" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))); paper.setStatus(0); // 未发布 // 保存试卷-题目关系 List<ExamPaperQuestion> paperQuestions = questions.stream() .map(q -> { ExamPaperQuestion pq = new ExamPaperQuestion(); pq.setPaperId(paper.getId()); pq.setQuestionHash(q.getQuestionHash()); pq.setSortOrder(questions.indexOf(q) + 1); return pq; }).collect(Collectors.toList()); examPaperQuestionMapper.insertBatch(paperQuestions); return paper; }

这样组卷时间稳定在80ms以内,且支持秒级扩容——增加题库只需刷新缓存,无需改SQL。

3.3 主观题分配:用Redis分布式锁避免“同一道题被两个老师同时评”

主观题评卷最怕并发冲突。常见错误是用synchronized锁方法,但在集群环境下完全无效。本方案用Redis原子操作实现:

// MarkingAssignmentService.java public MarkingTask assignTask(Long questionId, Long studentId) { String lockKey = "marking_lock:" + questionId + ":" + studentId; String lockValue = UUID.randomUUID().toString(); // 尝试获取锁(3秒过期,避免死锁) Boolean isLocked = redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(3)); if (!isLocked) { throw new BusinessException("题目正在被其他老师评阅,请稍候"); } try { // 检查该题是否已被分配 MarkingTask existing = markingTaskMapper.selectByQuestionAndStudent(questionId, studentId); if (existing != null) { return existing; // 已存在,直接返回 } // 创建新任务 MarkingTask task = new MarkingTask(); task.setQuestionId(questionId); task.setStudentId(studentId); task.setAssignTime(new Date()); task.setStatus(0); // 0-待评阅 markingTaskMapper.insert(task); return task; } finally { // 释放锁(Lua脚本保证原子性) String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), lockValue); } }

关键点:锁Key包含questionId+studentId,粒度最小;Lua脚本释放锁,防止A获取锁后崩溃导致锁永远不释放。


4. 避坑指南:那些让答辩老师当场皱眉的5个致命细节

4.1 现象:考试开始后,部分考生看到“试卷加载失败”,但日志里没有任何ERROR

原因:前端请求试卷接口时,后端用@Transactional包裹了整个组卷逻辑,而组卷过程中调用questionBankMapper.selectByHashes()查题时,MySQL连接池耗尽(默认HikariCP最大连接数20),导致后续请求全部阻塞。
解决

  • 将组卷逻辑拆分为两阶段:第一阶段只生成试卷ID和题目哈希列表(无事务),第二阶段异步填充题目详情(带事务);
  • HikariCP配置调大:spring.datasource.hikari.maximum-pool-size=50,并设置connection-timeout=30000
  • application.yml中添加健康检查:management.endpoint.health.show-details=always,部署后立刻访问/actuator/health确认DB连接正常。

4.2 现象:导出Excel成绩单时,服务器CPU飙升到100%,导出耗时超过2分钟

原因:用Apache POI直接new XSSFWorkbook()生成Excel,内存占用随行数线性增长,1万行数据吃掉1.2GB堆内存,触发频繁GC。
解决

  • 改用SXSSFWorkbook(流式写入):
// ExamReportService.java public void exportScoreReport(Long examId, HttpServletResponse response) { SXSSFWorkbook workbook = new SXSSFWorkbook(1000); // 每1000行刷盘一次 Sheet sheet = workbook.createSheet("成绩单"); // 写入表头... List<ScoreRecord> records = scoreMapper.selectByExamId(examId); for (int i = 0; i < records.size(); i++) { Row row = sheet.createRow(i + 1); Cell cell = row.createCell(0); cell.setCellValue(records.get(i).getStudentName()); // ...其他列 } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=scores.xlsx"); workbook.write(response.getOutputStream()); workbook.dispose(); // 必须调用,否则临时文件不清理 }
  • JVM启动参数加-XX:+UseG1GC -Xmx2g -Xms2g,避免Minor GC频繁。

4.3 现象:教师登录后,点击“监考监控”页面显示空白,浏览器Console报Failed to fetch

原因:前端Vue项目打包后静态资源放在src/main/resources/static,但Spring Boot默认静态路径是/static/**,而监考页JS文件路径写成了/monitor/app.js,实际应为/static/monitor/app.js
解决

  • Vue CLI构建时配置vue.config.js
module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/static/' : '/', outputDir: 'src/main/resources/static' }
  • 后端Controller返回监考页时,用return "forward:/static/monitor/index.html";而非return "monitor/index"

4.4 现象:考生交卷后,成绩页面显示“0分”,但数据库里score字段是正确数值

原因Score实体类中score字段用Integer类型,而MySQL中该列为DECIMAL(5,2),MyBatis-Plus默认映射为BigDecimal,前端接收时未做类型转换,JSON序列化后变成字符串"85.50",JavaScript解析为NaN
解决

  • 实体类字段改为BigDecimal,并添加JSON序列化注解:
public class Score { @TableField("score") @JsonSerialize(using = BigDecimalToStringSerializer.class) private BigDecimal score; }
  • 自定义序列化器:
public class BigDecimalToStringSerializer extends JsonSerializer<BigDecimal> { @Override public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(value.setScale(2, RoundingMode.HALF_UP).toString()); } }

4.5 现象:部署到Linux服务器后,考试倒计时总是比实际快1小时

原因:服务器时区为UTC,而Java应用未指定时区,LocalDateTime.now()返回UTC时间,但前端JS的new Date()用的是浏览器本地时区(如CST),导致时间差。
解决

  • 启动脚本中强制指定时区:
java -Duser.timezone=Asia/Shanghai -jar exam-system.jar
  • 或在application.yml中配置:
spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss
  • 数据库连接URL加时区参数:jdbc:mysql://localhost:3306/exam?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8

5. 项目报告与答辩PPT:如何把技术细节转化成评委听得懂的价值点

5.1 项目报告不是流水账,而是讲清“为什么选这个方案”的决策链

很多同学的项目报告第一章就写“系统采用B/S架构”,评委看到就想翻页。真正有效的写法是用问题驱动叙述。例如:

问题:传统纸质考试阅卷周期长(平均5天),且错题统计需人工汇总。
现有方案缺陷:某商用系统虽支持自动批改,但主观题分配依赖人工指派,300份试卷需2小时完成分发。
本方案创新点:提出“题-生二维锁”分配算法(见3.3节),将主观题分配耗时从120分钟降至8.3秒(实测数据),且支持动态增减评卷教师——新增教师上线后,系统自动将其加入待分配队列,无需重启服务。
验证方式:使用JMeter模拟50并发教师登录,执行1000次分配请求,99%响应时间<15ms,错误率0%。

所有技术描述都要绑定具体指标:不是“用了Redis”,而是“Redis缓存使组卷QPS从12提升至217”;不是“做了防切屏”,而是“双心跳机制将页面隐藏漏检率从37%降至0.2%”。

5.2 开题报告要预判评委最可能问的3个问题,并提前埋好答案

开题报告不是承诺书,而是风险预案。我在答辩时被问最多的三个问题,其实在开题报告“可行性分析”章节就已给出答案:

评委可能问开题报告对应段落回答要点
“你们怎么保证考试期间系统不宕机?”4.2节“容灾设计”“采用主从分离:读操作走从库(配置@DS('slave')),写操作走主库;Redis部署哨兵模式,故障转移时间<3秒;所有核心接口添加熔断(Sentinel),当错误率>50%时自动降级为‘缓存试卷ID+本地题库’模式,保障基本考试功能”
“主观题评卷公平性怎么保障?”3.3节“评卷质量控制”“系统强制要求每道题至少2人评阅,分数差值>15%时触发第三评;所有评卷操作留痕(谁、何时、给多少分),支持按‘题ID+考生ID’回溯完整评卷链路”
“代码里没看到单元测试,怎么证明逻辑正确?”5.1节“质量保障”“对组卷、批改、导出三大核心模块编写JUnit5测试,覆盖率82%(报告附Coveralls链接);特别针对‘时间溢出’场景,用@Test(expected = BusinessException.class)验证超时强制交卷逻辑”

注意:开题报告里的“技术路线图”不要画UML时序图,而要用甘特图标出每个模块的交付物(如“4月10日前完成Redis分布式锁实现并输出压测报告”),让评委一眼看到你管住了过程。

5.3 答辩PPT:一页讲透一个技术点,拒绝文字堆砌

答辩PPT不是Word稿放大版。我的经验是:每页只放1个结论+1张图+1行数据。例如讲防切屏机制:

  • 标题页双心跳机制:让页面隐藏无所遁形
  • :左侧画前端心跳上报流程(JS → API),右侧画服务端校验逻辑(Redis时间戳比对 → 异常标记)
  • 数据:底部加一行红字:“实测页面隐藏检测准确率99.8%,漏检窗口<200ms”

再比如讲组卷性能优化:

  • 标题页组卷提速15倍:从ORDER BY RAND()到内存采样
  • :柱状图对比两种方案耗时(旧方案3200ms vs 新方案210ms)
  • 数据:“题库120万题时,QPS从12→217,TP99<100ms”

最后一页不要写“谢谢聆听”,而要写:“本系统已在XX学院2023级期末考中实际运行,支撑32场考试、11762人次,0生产事故。”——用事实代替客套。


6. 从“能跑”到“真用”:部署上线前必须做的5项生产级检查

6.1 数据库连接池:别让HikariCP在半夜悄悄“自杀”

很多项目本地调试一切正常,一上生产就报HikariPool-1 - Connection is not available, request timed out after 30000ms。这不是连接数不够,而是连接泄漏。HikariCP默认connection-timeout=30000,但若某次查询因网络抖动超时,连接未被归还,池子里的连接就会越来越少。必须做两件事:

  1. 开启连接泄漏检测(开发环境可关,生产必须开):
spring: datasource: hikari: leak-detection-threshold: 60000 # 60秒未归还即告警 keepalive-time: 30000 validation-timeout: 3000
  1. 在所有DAO方法后加连接关闭日志(MyBatis-Plus不自动关,需手动):
// BaseMapperWrapper.java public class BaseMapperWrapper<T> extends BaseMapper<T> { @Override public int insert(T entity) { long start = System.currentTimeMillis(); int result = super.insert(entity); log.debug("insert {} cost {}ms", entity.getClass().getSimpleName(), System.currentTimeMillis() - start); return result; } }

然后在日志里搜cost > 5000,找到慢SQL优化。

6.2 文件上传:考试附件(如作文图片)必须走独立存储

项目里常把考生上传的图片存到src/main/resources/static/upload/,这在开发时没问题,但生产部署用java -jar方式运行时,static目录是jar包内嵌的,无法写入。必须改用外部路径+nginx代理

  1. 配置上传路径:
# application.yml exam: upload: path: /data/exam/uploads/ # Linux绝对路径
  1. 创建目录并赋权:
mkdir -p /data/exam/uploads chmod 755 /data/exam/uploads chown -R springboot:springboot /data/exam/uploads
  1. Nginx配置反向代理:
location /upload/ { alias /data/exam/uploads/; expires 1h; add_header Cache-Control "public, max-age=3600"; }

这样前端上传时POST /api/upload,后端保存到/data/exam/uploads/xxx.jpg,前端访问时用https://yourdomain.com/upload/xxx.jpg,既安全又高效。

6.3 日志分级:让运维一眼看出是“考生交卷”还是“系统崩溃”

默认的logback.xml会把所有INFO日志混在一起,出问题时翻半小时找不到关键行。必须按业务域切分:

<!-- logback-spring.xml --> <appender name="EXAM_PROCESS" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/exam-process.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/exam-process.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="com.exam.process" level="INFO" additivity="false"> <appender-ref ref="EXAM_PROCESS"/> </logger>

然后在代码里打日志:

// ExamProcessService.java private static final Logger logger = LoggerFactory.getLogger(ExamProcessService.class); public void submitAnswer(Long paperId, AnswerDTO answer) { logger.info("考生{}提交试卷{},共{}道题", SecurityUtils.getCurrentUserId(), paperId, answer.getAnswers().size()); // ...业务逻辑 }

这样运维查问题,直接grep "提交试卷" logs/exam-process.log,3秒定位。

6.4 安全加固:3个必须改掉的“教学默认值”

学生项目常保留这些危险配置,上线前务必检查:

风险点默认值生产必须改成
Actuator端点management.endpoints.web.exposure.include=*management.endpoints.web.exposure.include=health,info,metrics,prometheus(禁用env,beans,shutdown
数据库密码spring.datasource.password=123456改用Jasypt加密:spring.datasource.password=ENC(XXXXX),启动时加--jasypt.encryptor.password=your-secret
JWT密钥jwt.secret=abc123改为32位随机字符串:openssl rand -base64 32生成,存入环境变量

提示:用mvn compile -Dmaven.test.skip=true打包前,先运行find . -name "*.yml" -o -name "*.properties" | xargs grep -l "123456\|abc123\|exposure.include=\*",确保没漏改。

6.5 压测验证:用真实数据跑一遍,别信“理论上能扛”

最后一步,也是最容易跳过的一步:用生产数据量压测。别只测“10个用户”,要测“1000个并发考生同时点击‘开始考试’”。我用JMeter做了三轮压测:

  1. 单接口压测/api/exam/start(组卷接口),线程数100,循环10次 → 发现MySQL CPU达95%,加idx_exam_status索引后降至42%;
  2. 链路压测:模拟考生完整流程(登录→进入考试→答题→交卷),线程数200,Ramp-up 60秒 → 发现Redis内存涨到4GB,调整maxmemory-policy volatile-lru后稳定在1.2GB;
  3. 混合压测:200考生考试 + 5监考老师实时刷新监控页 + 2教务老师导出报表 → 确认各模块QPS均在阈值内(组卷217、交卷189、监控刷新83、导出12)。

压测报告里必须写明:“在4核8G服务器上,本系统可持续支撑200并发考试,平均响应时间<800ms,错误率0%”。没有这个数据,所有技术描述都是空中楼阁。


做到这一步,你的“基于Java的在线考试管理项目”就不再是课程设计作业,而是一个能真实交付的工程产品。我带过三届毕业设计,最常对学生说的一句话是:“代码能跑通只是及格线,让教务老师愿意用、运维同事敢上线、答辩老师挑不出硬伤,才算真正做完。”这个项目里没有黑科技,全是老老实实用Java生态解决真实问题的笨功夫——而恰恰是这些“笨功夫”,才是企业最看重的工程能力。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 11:20:47

一直英语原理详解:面试必问的底层逻辑拆解

一直英语原理详解:面试必问的底层逻辑拆解 配置环境就卡半天?别急着骂娘。很多开发者在搭建“一直英语”这类本地化语言处理或特定业务逻辑的Demo时,往往卡在依赖冲突或路径解析上。这不仅仅是环境问题,更是面试必问的底层原理考察点。…

作者头像 李华
网站建设 2026/9/23 11:20:41

植物CNN识别系统落地全链路:从解压失败到Grad-CAM可解释推理

简介&#xff1a;这是一套完整的Python植物识别系统开发资源&#xff0c;面向人工智能初学者、计算机视觉实践者及高校课程设计学生&#xff0c;聚焦基于深度学习的植物图像分类任务。资源包含CNN与MobileNet双模型实现&#xff0c;配套训练代码、测试脚本、PyQt5图形界面及可视…

作者头像 李华
网站建设 2026/9/23 11:20:43

周记写什么?从入门到精通,3个维度搞定技术复盘

周记写什么?从入门到精通,3个维度搞定技术复盘 代码复制过来直接报错,环境配置半天没反应,这时候你盯着屏幕发呆,不知道从哪开始调。别慌,这种“复制粘贴综合症”是每个开发者从入门到精通路上必过的坎。…

作者头像 李华
网站建设 2026/9/23 11:20:34

星际争霸2神族战术避坑指南:新手必看的实战代码逻辑

星际争霸2神族战术避坑指南:新手必看的实战代码逻辑 刚拿到一套所谓的“星际争霸2神族战术”自动化脚本,直接运行却报了一堆错?别慌,这种“复制来的代码跑不通不知道怎么调”的情况,在咱们技术圈太常见了。很多兄弟以为星际争霸2神族战术就是点点鼠标,其实背后全是状态机和逻辑判断。今天这篇避坑指南,不整虚的,…

作者头像 李华
网站建设 2026/9/23 11:20:30

三星怎么截屏原理详解

三星怎么截屏:3个底层原理助你面试突围,新手避坑指南 面试被问“三星手机截屏底层实现”答不上来,简历直接进回收站。别笑,这题真考过。大厂安卓组喜欢用这种看似生活化、实则硬核的问题,筛选懂系统机制的人。新手避坑第一步,就是别把截屏当普通拍照,它是图形管线的一次快照。 考点梳理…

作者头像 李华
网站建设 2026/9/23 11:20:05

宏基4752g驱动避坑指南:3个真实案例搞定新手调试难题

宏基4752g驱动避坑指南:3个真实案例搞定新手调试难题 复制来的代码跑不通,报错信息一堆,完全不知道从哪下手调?这是无数新手在接触旧机型或特定环境时的噩梦。很多教程只给最终结果,却忽略了中间那些让人抓狂的兼容性问题。尤其是像宏基4752g这种老款笔记本,其驱动环境与现在的Windows…

作者头像 李华