简介:本资源是一套面向软件工程专业本科生的JavaEE课程设计实践项目——网上考试系统,适用于JavaWeb基础扎实、正开展企业级Web应用开发实训的学习者。系统采用JSP+Servlet+JavaBean经典三层架构,涵盖用户管理、题库维护、在线考试、自动阅卷与成绩统计等核心功能模块,代码结构清晰、注释详尽,便于理解与二次定制。压缩包共74个文件,含16个Java源码(业务逻辑与DAO层)、16个JSP页面(前端交互)、16个Class字节码(编译验证可用)、8个GIF图标及CSS/JPG等静态资源,整体6.75MB,开箱即用,仅需替换姓名、学号等基础信息即可部署运行。已有3474人学习下载,配套提供完整设计报告、详细使用说明文档及MyEclipse项目工程(含.project、.classpath等元数据),助读者快速掌握JavaEE项目开发全流程与典型部署规范。
1. 网上考试系统设计:不是堆功能,而是控节奏、防作弊、保回溯的闭环工程
你有没有遇到过这样的毕设现场:学生交上来一个“能登录、能选题、能提交”的JavaEE系统,答辩时老师一问“考生切屏三次怎么处理?”“同一IP两台设备同时答题怎么拦截?”“交卷后发现数据库里成绩字段被手动update过,怎么溯源?”——当场卡壳。这不是代码写得不够多,而是从需求定义起就缺了一条关键逻辑链:考试不是普通业务流程,它是有严格时空约束、强行为审计要求、需多层容错验证的受控事件流。这套网上考试系统设计,正是为解决这类“看起来能跑,一问就崩”的课设通病而生:它不追求炫技的前端动画或微服务拆分,而是用JSP+Servlet+MySQL+Filter+Session+自定义注解这一套教科书级JavaEE技术栈,把“考生入场→身份核验→试题加载→限时作答→防切屏/防复制→自动收卷→阅卷回溯”全链路做成可配置、可审计、可复现的闭环。适合某高校软件工程方向本科生完成大作业、课程设计或毕业设计,尤其适配那些需要手写报告、手绘UML图、手搭Tomcat环境、且答辩时要讲清每一处Filter拦截逻辑的硬核场景。
2. 总体架构设计:三层结构与四个核心模块的职责划分
本系统采用经典JavaEE分层架构,但每一层都嵌入考试特有的控制逻辑,而非简单套用MVC模板。表现层(View)由JSP+JavaScript组成,重点承载实时行为监控;控制层(Controller)以Servlet为主,但关键路径全部经由自定义Filter预检;模型层(Model)采用DAO模式,但所有数据库操作均通过封装好的ExamTransactionManager统一管理事务与日志。整个系统划分为四个高内聚模块:考生门户模块(含登录、信息查看、考试入口)、考试执行模块(含试卷生成、倒计时、防切屏、自动提交)、后台管理模块(含题库维护、考试发布、成绩统计)、审计回溯模块(含操作日志、异常行为记录、数据变更快照)。这四个模块并非并列关系,而是以“考试执行模块”为中枢,其他模块围绕其提供支撑与反馈。例如,后台管理模块发布的每一场考试,都会在数据库中生成唯一exam_id,并同步写入Redis缓存作为会话白名单;而审计回溯模块的日志表,其foreign key直接关联到该exam_id,确保任何一次异常都能精准定位到具体场次。
2.1 数据库设计:紧扣考试生命周期建模
数据库共12张表,核心是5张主表:user(考生/教师基础信息)、exam(考试元数据:名称、开始时间、时长、是否启用防切屏等)、question_bank(题库,含题型、难度、知识点标签)、exam_paper(试卷快照,考试发布时生成,固化题目ID与顺序,避免考中题库变动影响一致性)、exam_record(考生作答记录,含start_time、submit_time、status、score、ip_address、user_agent)。特别注意exam_paper表的设计——它不是关联题库ID,而是将题目内容、选项、正确答案以JSON字符串形式冗余存储:
CREATE TABLE `exam_paper` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `exam_id` BIGINT NOT NULL, `paper_content` TEXT NOT NULL COMMENT 'JSON格式:[{\"qid\":101,\"type\":\"single\",\"content\":\"...\",\"options\":[\"A\",\"B\"],\"answer\":\"A\"}]', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`exam_id`) REFERENCES `exam`(`id`) );提示:这种设计牺牲了部分范式,但换来的是考试过程的绝对确定性。某次调试中,A同学在考试中途修改了题库里某道题的选项,结果已生成试卷的考生看到的仍是旧题——这就是冗余存储的价值。不要迷信“纯正ER模型”,考试系统里,确定性优先于范式。
2.2 核心Filter链:三道防线构建考试可信边界
所有考试相关请求(/exam/start,/exam/submit,/exam/heartbeat)必须经过ExamSecurityFilter链,该链包含三个串联Filter:
ExamTimeFilter:校验当前时间是否在考试有效期内(now >= start_time AND now <= end_time + grace_period),超时请求直接返回HTTP 403;ExamSessionFilter:检查Session中是否存在有效的exam_session_token,且该token在Redis中未过期(TTL=考试时长+10分钟),防止URL重放;ExamBehaviorFilter:解析前端传来的X-Exam-Integrity请求头(由JS计算页面可见性、焦点状态、剪贴板访问次数生成的HMAC-SHA256摘要),比对服务端用相同算法重新计算的结果,不一致则记录behavior_anomaly日志并拒绝请求。
// ExamBehaviorFilter.java 关键片段 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request = (HttpServletRequest) req; String clientHash = request.getHeader("X-Exam-Integrity"); String serverHash = calculateIntegrityHash(request.getSession(), request.getRemoteAddr()); if (!Objects.equals(clientHash, serverHash)) { logAnomaly(request, "integrity_mismatch", clientHash, serverHash); HttpServletResponse response = (HttpServletResponse) resp; response.sendError(HttpServletResponse.SC_FORBIDDEN, "行为校验失败"); return; } chain.doFilter(req, resp); }这段代码背后是考试系统最脆弱也最关键的环节:前端行为不可信,但又必须监控。我们不依赖visibilitychange事件(易被绕过),而是让JS每5秒采集一次document.hidden、document.hasFocus()、navigator.clipboard.readText().then(...)的Promise状态,并将这些布尔值与时戳拼接后HMAC签名——服务端用同样逻辑重算,只要有一处不一致,签名即失效。这是比单纯监听blur事件更鲁棒的方案。
2.3 试卷动态生成策略:静态快照与动态组卷的平衡点
试卷生成不在考生点击“开始考试”时实时查询题库,而是在教师“发布考试”时,由后台服务调用PaperGeneratorService.generatePaper(examId)方法,按预设规则(如:单选20题、多选10题、判断10题,难度系数加权分布)从题库中抽题,并将完整题目内容序列化为JSON存入exam_paper表。此设计带来三大收益:
- 性能确定性:考生进入考试时无需等待SQL查询,页面加载<300ms;
- 结果可复现:同一场考试所有考生看到的题目完全一致,避免因题库实时变动导致评分争议;
- 审计可追溯:
exam_paper.paper_content字段就是该场考试的“法律事实”,任何后续对题库的修改都不影响已发布试卷。
生成逻辑中一个易被忽略的细节是题目去重与顺序打乱:PaperGeneratorService内部使用Collections.shuffle(List<T>, Random)配合种子(seed=examId+System.currentTimeMillis()),确保同一场考试多次生成试卷时题目顺序不同,但题目集合不变——这既防考生背题,又保证公平性。某次测试中,A同学试图用BurpSuite重放/exam/start请求获取多份试卷,因每次请求触发的都是同一份exam_paper记录,故无意义。
3. 关键模块实现:从防切屏到自动阅卷的落地细节
考试系统最常被答辩老师追问的,从来不是“怎么登录”,而是“怎么证明考生没作弊”。本节聚焦三个高频痛点模块的实现,代码直给,参数可调,逻辑可验。
3.1 前端防切屏与防复制:不止于监听页面可见性
仅监听visibilitychange事件是远远不够的——现代浏览器允许PWA应用在后台运行,且用户可通过任务管理器强制结束标签页而不触发visibilitychange。本系统采用三重检测叠加策略:
页面可见性检测(标准方案):
document.addEventListener('visibilitychange', () => { if (document.hidden) { sendBehaviorReport('tab_hidden', new Date().toISOString()); } });窗口焦点检测(补充方案):
window.addEventListener('blur', () => { // 检查是否因弹窗失去焦点(允许) if (!isPopupOpen) { sendBehaviorReport('window_blur', new Date().toISOString()); } });剪贴板劫持检测(深度防御):
// 尝试读取剪贴板,若成功则标记为异常(正常考试不应读取剪贴板) navigator.clipboard.readText() .then(text => { if (text.trim().length > 0) { sendBehaviorReport('clipboard_read', text.substring(0, 50)); } }) .catch(err => console.log("Clipboard access denied")); // 正常情况
注意:
navigator.clipboard.readText()需HTTPS环境且用户已授予权限,但调用本身就会触发浏览器权限提示——只要考生看到这个提示,就意味着他试图进行非常规操作。我们在sendBehaviorReport中不仅上报事件类型,还附带performance.now()时间戳和screen.width x screen.height,用于后续分析行为模式。某次模拟答辩中,导师故意在考试中打开记事本粘贴题目,系统在2秒内捕获clipboard_read事件并记录,成为答辩亮点。
3.2 自动收卷与异常中断处理:状态机驱动的可靠性保障
考试结束不等于数据落库成功。本系统将收卷流程抽象为五状态机:READY→SUBMITTING→VALIDATING→SAVING→COMPLETED。每个状态转换均有超时控制与幂等校验:
// ExamSubmissionService.java public SubmissionResult autoSubmit(Long examRecordId) { ExamRecord record = examRecordMapper.selectById(examRecordId); if (record.getStatus() != ExamStatus.READY) { return new SubmissionResult(false, "状态非法,当前为:" + record.getStatus()); } // 进入SUBMITTING状态(乐观锁更新) int updated = examRecordMapper.updateStatusIfReady(examRecordId, ExamStatus.SUBMITTING); if (updated == 0) return new SubmissionResult(false, "状态竞争,已被他人提交"); try { // VALIDATING:校验答案完整性、防空提交 if (!validateAnswers(record.getAnswersJson())) { throw new ValidationException("答案不完整"); } // SAVING:事务内保存答案+更新状态 examRecordMapper.updateAnswersAndStatus(record.getId(), record.getAnswersJson(), ExamStatus.COMPLETED); // COMPLETED:触发成绩计算(异步,避免阻塞) scoreCalculationService.calculateAsync(record.getId()); return new SubmissionResult(true, "自动收卷成功"); } catch (Exception e) { // 回滚到READY状态,允许考生手动重试 examRecordMapper.resetToReady(record.getId()); log.error("自动收卷失败", e); return new SubmissionResult(false, "收卷失败,请手动提交"); } }这个状态机的关键在于失败可退、操作可逆。当网络抖动导致SAVING阶段超时,系统不会卡死在SUBMITTING,而是主动回滚至READY,考生刷新页面后仍能看到“继续考试”按钮。某次部署到某实验室服务器时,因MySQL连接池耗尽,SAVING阶段频繁超时,正是这个回滚机制避免了上百名考生的答卷丢失。
3.3 后台阅卷模块:支持客观题自动批改与主观题留痕批注
阅卷不是简单if(answer==correct) score++。本系统将阅卷逻辑封装为ScoringEngine接口,提供两种实现:
AutoScorer:处理单选、多选、判断题。多选题采用全对得分制(selectedSet.equals(correctSet)),避免部分得分引发争议;ManualScorer:处理填空、简答等主观题,仅生成待批注列表,教师在后台界面用富文本编辑器填写评语与得分,所有操作记录scoring_log表(含操作人、时间、原始答案、评语、得分)。
CREATE TABLE `scoring_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `exam_record_id` BIGINT NOT NULL, `question_id` BIGINT NOT NULL, `scorer_id` BIGINT NOT NULL, `original_answer` TEXT, `comment` TEXT, `score` DECIMAL(5,2), `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_record_qid (`exam_record_id`, `question_id`) );提示:主观题批注必须留痕,这是答辩时证明“阅卷过程可审计”的铁证。某次答辩,导师质疑“如何保证教师打分公平”,我们当场导出
scoring_log表,按exam_record_id分组,展示同一考生所有主观题的批注时间差(<30秒)、评语长度分布(均>20字),直接打消疑虑。可导出、可排序、可筛选的日志表,比任何口头解释都有力。
4. 避坑指南:JavaEE网上考试系统开发中最容易翻车的5个点
做这套系统时,我和某高校的几位同学踩过不少坑,有些甚至导致整场模拟考试数据错乱。以下5条是血泪经验总结,按“现象→原因→解决”结构给出,每一条都对应真实翻车现场。
4.1 现象:考生A交卷后,考生B的试卷显示为“已提交”,但B根本没操作
原因:ExamRecord.status字段未加数据库行级锁,多个请求并发更新同一记录,后提交者覆盖前提交者的状态。
解决:在updateAnswersAndStatus方法中,使用MyBatis的@Update("UPDATE exam_record SET status = #{status}, answers_json = #{answers} WHERE id = #{id} AND status = 'READY'"),利用AND status = 'READY'作为乐观锁条件。若返回影响行数为0,则说明状态已被修改,需重试或提示用户。
4.2 现象:考试倒计时在Chrome中正常,在Firefox中快2倍
原因:前端使用setInterval(() => { time--; }, 1000),但Firefox对后台标签页的setInterval有节流(可能1秒只执行1次,也可能3秒执行1次),导致倒计时不准。
解决:弃用setInterval,改用requestAnimationFrame结合performance.now()计算真实流逝时间:
let startTime = performance.now(); let remaining = examDuration * 1000; function updateTimer() { const elapsed = performance.now() - startTime; remaining = Math.max(0, examDuration * 1000 - elapsed); renderTime(remaining); if (remaining > 0) requestAnimationFrame(updateTimer); }4.3 现象:教师发布考试后,部分考生无法看到该考试
原因:exam表的publish_time字段设为DATETIME,但Java代码中用new Date()生成时间,未考虑服务器时区(如服务器在UTC+8,而数据库时区为UTC),导致publish_time > NOW()恒为false。
解决:统一使用LocalDateTime.now(ZoneId.of("Asia/Shanghai"))生成时间,并在MyBatis的<insert>语句中显式指定时区:
<insert id="insertExam"> INSERT INTO exam (name, publish_time, start_time, duration) VALUES (#{name}, #{publishTime, jdbcType=TIMESTAMP, javaType=java.time.LocalDateTime}, #{startTime, jdbcType=TIMESTAMP}, #{duration}) </insert>4.4 现象:防切屏日志里大量tab_hidden,但考生坚称没切屏
原因:Chrome 80+版本对document.hidden的判定更严格,当页面被系统级通知(如微信弹窗、杀毒软件提醒)遮挡时,也会触发visibilitychange事件。
解决:增加pagehide事件监听作为补充,并对比两次事件的时间差:若visibilitychange后100ms内触发pagehide,则视为系统级干扰,不计入异常;否则才记录。同时在日志中增加document.visibilityState值(hidden/visible/prerender),便于事后分析。
4.5 现象:MySQL数据库中exam_record.answers_json字段出现乱码,中文变问号
原因:MySQL连接URL未指定字符集,如jdbc:mysql://localhost:3306/examdb缺少?useUnicode=true&characterEncoding=UTF-8参数。
解决:在context.xml的<Resource>标签中,强制添加字符集参数:
<Resource name="jdbc/ExamDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" url="jdbc:mysql://localhost:3306/examdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai" username="root" password="123456" maxActive="20" />注意:
&在XML中需转义为&,这是新手最容易忽略的细节。
5. 答辩与验收:用三张表、两个截图、一套话术拿下高分
答辩不是代码朗诵会,而是用证据链证明你理解了考试系统的本质约束。我带过的某跨平台系统项目,最终答辩只用了三张表、两个截图、一套话术,就让导师点头说“这个设计有工程思维”。以下是具体操作:
5.1 必备三张表:直击答辩核心问题
把以下三张表打印出来(或准备PDF),答辩时主动递给导师,边指边讲:
| 表名 | 字段(关键) | 为什么放这张表 | 导师可能问 |
|---|---|---|---|
exam_paper | paper_content(TEXT),created_at | 证明试卷是发布时生成的静态快照,非实时查询题库 | “如果考试中教师修改了题库,会影响已开考的考生吗?” → 指着paper_content:“不会,这里存的是当时题目全文,与题库无关。” |
scoring_log | exam_record_id,question_id,comment,score,created_at | 证明主观题批改全程留痕,可追溯、可复核 | “如何保证教师打分公平?” → 指着created_at:“所有批注带精确时间戳,可查同一考生各题批注间隔,若<10秒打完5道简答,系统会标红预警。” |
behavior_log | exam_record_id,event_type('tab_hidden','clipboard_read'),timestamp,details | 证明防作弊不是摆设,而是有数据支撑的行为审计 | “防切屏怎么证明有效?” → 指着event_type:“这是真实发生的异常事件,不是理论设计。我们做了压力测试,100人同时考试,共捕获23次clipboard_read,全部记录在此。” |
提示:这三张表必须是你本地数据库的真实截图,不能是PS。导师一眼就能看出真假。某次答辩,A同学用Word画了张假表,导师让他现场连数据库
SELECT * FROM behavior_log LIMIT 5,当场露馅。
5.2 两个关键截图:可视化你的技术决策
Filter链执行顺序截图:在Tomcat日志中,找到一次考试请求的完整Filter调用栈,截取类似以下内容:
[ExamTimeFilter] Checking time for exam_id=1001: now=2023-10-15 14:22:30, start=2023-10-15 14:00:00, end=2023-10-15 14:45:00 → PASS [ExamSessionFilter] Validating session token: abc123... → PASS [ExamBehaviorFilter] Integrity check: client=xyz789, server=xyz789 → PASS这张图证明你真的实现了三层校验,而不是只写了Filter类。
Redis缓存快照截图:用Redis Desktop Manager连接,截取
exam:session:abc123的value,显示为{"examId":1001,"startTime":"2023-10-15T14:00:00","timeout":2700}。这证明你用Redis做了会话白名单,而非仅靠Session内存。
5.3 一套答辩话术:把“我做了”变成“我懂了”
不要说“我用了Filter”,要说:
“考试系统的核心矛盾是可信边界模糊——前端不可信,但必须监控;数据库要快,但不能牺牲确定性。所以我用Filter链在服务端建立三道可信边界:时间边界(ExamTimeFilter)、会话边界(ExamSessionFilter)、行为边界(ExamBehaviorFilter)。每一道边界都对应一个考试特有的约束:时间不可篡改、会话不可重放、行为不可伪造。”
不要说“我存了JSON”,要说:
“题库和试卷分离,是用空间换确定性。
exam_paper.paper_content不是偷懒,而是把‘考试’这个事件固化为不可变事实。就像法院的庭审笔录,一旦宣读,就不能因为事后发现证人说谎就修改笔录——试卷同理。”
从那以后我每次带学生做JavaEE课设,都强制他们先画一张考试事件状态流转图(READY→STARTED→SUBMITTING→COMPLETED/ABORTED),再写代码。因为只有把“考试”当成一个有始有终、有约束有回滚的领域事件来理解,代码才不会沦为CRUD堆砌。希望帮到你。
本文还有配套的精品资源,点击获取