简介:本资源是一套完整的基于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秒空白期。本方案采用双保险机制:
- 前端每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') }) }); }- 服务端用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_time和exam_duration存入Redis,每次答题提交时校验System.currentTimeMillis() - exam_start_time > exam_duration,超时则强制交卷并标记status=TIMEOUT。
3.2 随机组卷:不是简单ORDER BY RAND(),而是预生成+缓存命中
ORDER BY RAND()在百万题库中会全表扫描,组卷耗时从200ms飙升到3s。本项目采用预生成策略:
- 启动时加载所有启用学科的题目哈希值到内存(约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); }- 组卷时从对应学科缓存中随机取样,再查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,但若某次查询因网络抖动超时,连接未被归还,池子里的连接就会越来越少。必须做两件事:
- 开启连接泄漏检测(开发环境可关,生产必须开):
spring: datasource: hikari: leak-detection-threshold: 60000 # 60秒未归还即告警 keepalive-time: 30000 validation-timeout: 3000- 在所有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代理:
- 配置上传路径:
# application.yml exam: upload: path: /data/exam/uploads/ # Linux绝对路径- 创建目录并赋权:
mkdir -p /data/exam/uploads chmod 755 /data/exam/uploads chown -R springboot:springboot /data/exam/uploads- 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做了三轮压测:
- 单接口压测:
/api/exam/start(组卷接口),线程数100,循环10次 → 发现MySQL CPU达95%,加idx_exam_status索引后降至42%; - 链路压测:模拟考生完整流程(登录→进入考试→答题→交卷),线程数200,Ramp-up 60秒 → 发现Redis内存涨到4GB,调整
maxmemory-policy volatile-lru后稳定在1.2GB; - 混合压测:200考生考试 + 5监考老师实时刷新监控页 + 2教务老师导出报表 → 确认各模块QPS均在阈值内(组卷217、交卷189、监控刷新83、导出12)。
压测报告里必须写明:“在4核8G服务器上,本系统可持续支撑200并发考试,平均响应时间<800ms,错误率0%”。没有这个数据,所有技术描述都是空中楼阁。
做到这一步,你的“基于Java的在线考试管理项目”就不再是课程设计作业,而是一个能真实交付的工程产品。我带过三届毕业设计,最常对学生说的一句话是:“代码能跑通只是及格线,让教务老师愿意用、运维同事敢上线、答辩老师挑不出硬伤,才算真正做完。”这个项目里没有黑科技,全是老老实实用Java生态解决真实问题的笨功夫——而恰恰是这些“笨功夫”,才是企业最看重的工程能力。希望帮到你。
本文还有配套的精品资源,点击获取