简介:这份Java在线考试系统源码包面向Java Web初学者与需要二次开发的开发者,提供一套可直接运行的在线测试与评估系统实现,帮助理解从用户请求处理到数据库交互的完整链路。压缩包共255个文件,约4.54MB,以gif、jpg等图片素材和java、class源码为主体,辅以xml配置、jsp页面、jar依赖及sql建库脚本,覆盖后端服务、前端界面与数据库设计。系统基于Java语言,结合Spring、Hibernate等框架与MySQL数据库,包含用户管理、考试管理、题库维护、考试流程监控、自动评分、成绩展示与报表统计等核心模块,并预留二次开发空间。目前已有914人学习下载。通过研读源码,读者可掌握DAO与Service分层、Action请求分发、Hibernate会话管理等实现思路,理解防SQL注入、XSS等安全考量,适合作为课程设计、毕业设计或功能扩展的参考项目。
1. 从一份 Java 在线考试系统源码说起:它能帮你省掉多少重复造轮子的时间
如果你正在做课程设计、毕业项目,或者公司内部需要一个能快速跑起来的考核工具,那这份 Java 在线考试系统源码大概率能让你少熬几个通宵。它解决的核心问题很具体:把“出题、组卷、考试、判分、成绩管理”这条链路完整地跑通,而不是丢给你一堆零散的 CRUD 代码让你自己拼。我见过太多号称“在线考试”的项目,打开一看只有一张试卷表和一个提交按钮,判分逻辑全靠前端糊弄,这种代码拿来做二次开发基本等于重写。这份源码的价值在于它把考试业务里最容易翻车的几个环节——随机组卷、倒计时控制、客观题自动判分、主观题人工阅卷——都做成了可运行的模块。适合谁?适合手里有 Java Web 基础、想找一个结构完整、能直接部署起来看效果、再按自己需求改的开发者。新手能照着跑通,熟手能直接扒它的组卷算法和判分逻辑用到自己项目里。
2. 技术栈与目录结构:拿到源码先看这三层
2.1 技术选型:为什么是 Spring Boot + MyBatis + Vue
这份源码采用的是前后端分离架构,后端 Spring Boot 打底,MyBatis 做持久层,前端 Vue 配合 Element UI。这个组合在在线考试这类中小型系统里几乎是标配,原因很实在:考试系统本质上是“读多写少、并发集中在开考瞬间”的场景,Spring Boot 的轻量级容器和 MyBatis 的手写 SQL 控制力,刚好能应对组卷时复杂的多表关联查询。数据库用 MySQL,缓存层留了 Redis 的接口但默认没强依赖,这点对新手很友好——你本地没装 Redis 也能先把项目跑起来。
常见做法是后端按controller / service / mapper / entity四层分包,前端按views / api / router / store组织。这份源码基本遵循了这个约定,没有搞什么花哨的 DDD 分层,对二次开发来说反而是好事,你找代码不用先画一张架构图。
2.2 目录结构:从入口到业务模块的物理映射
拿到压缩包解压后,你会看到类似下面这样的结构。我把它整理成表格,方便你对照自己的项目:
| 目录/文件 | 作用 | 二次开发关注点 |
|---|---|---|
exam-backend/ | 后端 Spring Boot 工程 | 核心业务逻辑都在这里 |
exam-backend/src/main/java/com/exam/controller/ | 接口层 | 新增考试类型时从这里加路由 |
exam-backend/src/main/java/com/exam/service/ | 业务逻辑层 | 组卷算法、判分逻辑在这层 |
exam-backend/src/main/java/com/exam/mapper/ | MyBatis 映射接口 | 改 SQL 或加自定义查询 |
exam-backend/src/main/resources/mapper/ | XML 映射文件 | 复杂查询的 SQL 写在这 |
exam-backend/src/main/resources/application.yml | 配置文件 | 数据库连接、端口、文件上传路径 |
exam-frontend/ | Vue 前端工程 | 页面交互和路由 |
exam-frontend/src/views/ | 页面组件 | 考试页、管理页、成绩页 |
exam-frontend/src/api/ | 接口封装 | 后端地址变了改这里 |
sql/exam_init.sql | 数据库初始化脚本 | 建库建表加初始数据 |
这个结构清晰的地方在于,后端和前端完全解耦,你可以只拿后端做接口服务,前端自己用 React 重写;也可以只改前端,后端一行不动。我一般拿到这种项目,第一件事是打开application.yml看数据库配置和端口,第二件事是跑一遍exam_init.sql,第三件事才是启动项目看报错。
2.3 数据库设计:五张核心表撑起整个考试流程
考试系统的数据库设计直接决定了你后面改功能的难度。这份源码里最核心的是五张表:user(用户)、question(题库)、exam(考试)、exam_question(试卷题目关联)、exam_record(考试记录)。其中exam_question这张关联表是关键,它记录了每场考试抽了哪些题、每题多少分、题目顺序是什么。没有这张表,随机组卷就没法做到“同一场考试不同学生题目顺序不同但难度一致”。
-- 试卷题目关联表的核心字段 CREATE TABLE exam_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL COMMENT '考试ID', question_id BIGINT NOT NULL COMMENT '题目ID', score INT DEFAULT 0 COMMENT '该题在本场考试中的分值', sort_order INT DEFAULT 0 COMMENT '题目顺序', INDEX idx_exam_id (exam_id) ) COMMENT '试卷题目关联表';这段 SQL 里score字段允许每场考试单独设置分值,而不是写死在题库里,这个设计很实用。比如同一道选择题,在随堂测验里值 2 分,在期末考试里值 5 分,改的是关联表而不是题库表。sort_order控制题目展示顺序,配合随机组卷时打乱顺序用。索引加在exam_id上,因为查试卷内容时都是按考试 ID 捞全部题目。
3. 核心模块拆解:组卷、判分、倒计时三条链路
3.1 随机组卷:从题库按题型和难度抽题
组卷是在线考试系统里最容易被做烂的模块。很多项目就是SELECT * FROM question ORDER BY RAND() LIMIT 10,这种写法在数据量小的时候没问题,一旦题库上千条,性能直接崩。这份源码的做法是按题型分组抽题,再按难度系数筛选,最后写入关联表。
// 组卷服务核心逻辑(简化版) public ExamPaper generatePaper(Long examId, PaperRule rule) { // 1. 按题型分组抽取题目ID List<Long> singleChoiceIds = questionMapper.selectRandomByType( rule.getSingleChoiceCount(), "SINGLE_CHOICE", rule.getDifficulty() ); List<Long> multiChoiceIds = questionMapper.selectRandomByType( rule.getMultiChoiceCount(), "MULTI_CHOICE", rule.getDifficulty() ); // 2. 合并题目列表并打乱顺序 List<Long> allQuestionIds = new ArrayList<>(); allQuestionIds.addAll(singleChoiceIds); allQuestionIds.addAll(multiChoiceIds); Collections.shuffle(allQuestionIds); // 3. 写入试卷题目关联表,记录分值和顺序 for (int i = 0; i < allQuestionIds.size(); i++) { ExamQuestion eq = new ExamQuestion(); eq.setExamId(examId); eq.setQuestionId(allQuestionIds.get(i)); eq.setScore(rule.getScorePerQuestion()); eq.setSortOrder(i); examQuestionMapper.insert(eq); } return buildPaperDetail(examId); }这段代码的关键在selectRandomByType这个 Mapper 方法,它对应的 SQL 不是简单的ORDER BY RAND(),常见做法是用WHERE type = ? AND difficulty = ? ORDER BY RAND() LIMIT ?,但在数据量大时会改成基于 ID 范围的随机抽样。Collections.shuffle保证题目顺序随机,防止学生之间对答案。参数rule里封装了各题型数量和难度系数,这些值来自考试配置表,改组卷规则不用动代码,改配置就行。
3.2 自动判分:客观题比对与主观题标记
判分逻辑分两条路走:客观题(单选、多选、判断)自动比对答案,主观题(简答、论述)标记为待阅卷。自动判分的坑在于多选题的答案比对——学生选的选项顺序可能和标准答案不一样,不能直接用字符串相等判断。
// 客观题判分逻辑 public int autoJudge(Question question, String studentAnswer) { if (studentAnswer == null || studentAnswer.trim().isEmpty()) { return 0; // 未作答得0分 } String standardAnswer = question.getAnswer().trim(); String stuAns = studentAnswer.trim(); switch (question.getType()) { case "SINGLE_CHOICE": case "JUDGE": // 单选和判断直接比对 return standardAnswer.equalsIgnoreCase(stuAns) ? question.getScore() : 0; case "MULTI_CHOICE": // 多选:排序后比对,避免顺序影响 List<String> stdList = Arrays.asList(standardAnswer.split(",")); List<String> stuList = Arrays.asList(stuAns.split(",")); Collections.sort(stdList); Collections.sort(stuList); return stdList.equals(stuList) ? question.getScore() : 0; default: return -1; // 主观题返回-1,标记待阅卷 } }多选的处理是先按逗号拆成列表,各自排序后再比对。这里有个细节:如果学生少选了一个选项,按严格判分是 0 分,但有些考试规则允许“少选得一半分”。这份源码默认是严格判分,你要改成半对半错,就在MULTI_CHOICE分支里加一个交集计算。返回-1表示主观题,后续阅卷接口会捞这些记录让老师打分。
3.3 倒计时与自动交卷:前端计时 + 后端兜底
考试系统的倒计时不能只靠前端。学生把浏览器标签页切走、或者用开发者工具改倒计时变量,前端计时就失效了。这份源码的做法是前端展示倒计时,后端在考试记录里存start_time和duration,交卷时校验是否超时。
// 前端倒计时组件核心逻辑 data() { return { remainSeconds: 0, timer: null }; }, mounted() { // 从后端获取剩余时间,而不是直接用考试时长 this.fetchRemainTime().then(seconds => { this.remainSeconds = seconds; this.startCountdown(); }); }, methods: { startCountdown() { this.timer = setInterval(() => { if (this.remainSeconds <= 0) { clearInterval(this.timer); this.autoSubmit(); // 时间到自动交卷 return; } this.remainSeconds--; }, 1000); }, autoSubmit() { this.$confirm('考试时间已到,系统将自动交卷', '提示', { confirmButtonText: '确定', showCancelButton: false }).then(() => { this.submitExam(); }); } }前端从后端拿剩余时间而不是本地计算,这样即使用户刷新页面,倒计时也能接上。后端在交卷接口里会再校验一次start_time + duration是否小于当前时间,如果超时则强制交卷并标记异常。这个双重保险是考试系统的标配,少了后端校验,前端倒计时就是个摆设。
4. 本地跑起来:从数据库初始化到前后端联调
4.1 环境准备与数据库初始化
跑这个项目需要 JDK 8 或 11、Maven 3.6+、MySQL 5.7 或 8.0、Node.js 14+。我一般会先确认 JDK 版本,因为 Spring Boot 2.x 和 3.x 对 JDK 要求不同,这份源码用的是 Spring Boot 2.7,JDK 8 就能跑。
# 1. 创建数据库并导入初始数据 mysql -u root -p -e "CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p exam_system < sql/exam_init.sql # 2. 修改后端数据库配置 # 编辑 exam-backend/src/main/resources/application.yml # 把 username 和 password 改成你自己的 # 3. 启动后端 cd exam-backend mvn clean package -DskipTests java -jar target/exam-backend-1.0.0.jar # 4. 启动前端 cd exam-frontend npm install npm run serve导入 SQL 的时候注意字符集,utf8mb4才能存 emoji 和特殊符号,考试题目里如果有数学公式符号,用utf8可能会乱码。后端启动后默认端口 8080,前端开发服务器默认 8081,跨域问题在application.yml里配了cors允许本地访问。
4.2 配置文件里最容易改错的三个参数
application.yml里有三个参数我每次都会检查:数据库连接 URL 的时区参数、文件上传路径、JWT 密钥。时区参数不写serverTimezone=Asia/Shanghai,考试记录的创建时间会差 8 小时,倒计时校验直接出错。文件上传路径默认是相对路径./upload,部署到服务器上要改成绝对路径,否则重启后文件找不到。JWT 密钥默认是个固定字符串,上线前必须换掉,不然任何人都能伪造 token。
spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password servlet: multipart: max-file-size: 10MB max-request-size: 10MB exam: upload-path: /data/exam/upload/ jwt-secret: change-this-to-a-random-string exam-duration-default: 60exam-duration-default是默认考试时长,单位分钟。这个值在创建考试时会被覆盖,但作为兜底配置留着。max-file-size限制上传文件大小,题库支持 Excel 批量导入,10MB 大概能放几千道题。
4.3 前后端联调:接口地址与跨域处理
前端在src/api/request.js里封装了 axios 实例,baseURL指向后端地址。开发环境下vue.config.js里配了代理,把/api开头的请求转发到localhost:8080,这样前端代码里写/api/exam/list就行,不用写完整域名。
// vue.config.js 代理配置 module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };pathRewrite把/api前缀去掉,因为后端接口本身没有这个前缀。如果你后端加了 context-path,这里要相应调整。联调时如果遇到 404,先看浏览器 Network 面板里请求的实际 URL 是什么,再对比后端@RequestMapping的路径,十有八九是前缀没对上。
5. 避坑与常见问题:部署和二次开发中容易翻车的地方
5.1 考试提交后成绩为 0 或判分异常
现象:学生交卷后,客观题得分全是 0,但答案明明是对的。
原因:最常见的是答案字段里有多余空格或换行。题库导入时 Excel 单元格里带了不可见字符,标准答案存成了"A ",学生选的是"A",字符串比对失败。
解决:在判分逻辑里统一做trim()处理,导入题库时也加一道清洗。如果用的是 MySQL,可以在查询时用TRIM()函数。我一般会在autoJudge方法入口加一行studentAnswer = studentAnswer.trim(),标准答案在入库前也 trim 一遍。
5.2 倒计时结束后自动交卷失败
现象:倒计时归零,弹出了自动交卷提示,但点击确定后接口报错,试卷没交上去。
原因:自动交卷时用户可能已经断网或者 token 过期。前端autoSubmit直接调submitExam,没有处理 token 失效的情况。
解决:在 axios 拦截器里统一处理 401,token 过期时跳转登录页并提示“登录已过期,请重新登录后查看考试记录”。同时后端在交卷接口里对超时交卷做特殊标记,允许在考试结束后 5 分钟内补交。
5.3 随机组卷时题目重复出现
现象:同一场考试里,同一个学生抽到了两道一模一样的题。
原因:selectRandomByType在并发情况下可能返回重复 ID,或者题库里本身有重复题目但 ID 不同。
解决:在 Java 层用Set去重,抽题后检查数量是否够,不够就补抽。更彻底的做法是在题库表加唯一索引,限制题干内容的 hash 值唯一。我一般会在组卷服务里加一个while循环,直到抽到的题目 ID 集合大小等于需求数量才跳出。
5.4 前端打包后刷新页面 404
现象:npm run build之后部署到 Nginx,首页能打开,但刷新考试页或成绩页就报 404。
原因:Vue Router 默认是 history 模式,Nginx 没有配置 fallback 到index.html。
解决:在 Nginx 配置里加try_files $uri $uri/ /index.html;。如果不想改 Nginx,就把 Router 模式改成 hash 模式,URL 里带#,但体验差一些。生产环境建议用 history 模式加 Nginx 配置。
5.5 数据库连接池耗尽导致考试卡顿
现象:多人同时开考时,页面加载缓慢,后端日志出现Connection is not available。
原因:默认 HikariCP 连接池最大连接数是 10,考试开考瞬间并发查试卷,连接不够用。
解决:在application.yml里调大maximum-pool-size,同时给试卷查询加缓存。考试试卷在开考前就生成好了,完全可以缓存到 Redis 里,学生开考时直接读缓存而不是查库。
spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 300006. 二次开发改造:从能跑到好用,我踩过的三个坑
这份源码跑通之后,你大概率会想改点东西。我拿它做过两次二次开发,一次是加防作弊,一次是接第三方判分引擎,踩过的坑比跑通的过程还多。
第一个改造点是防作弊。原版只做了倒计时和自动交卷,没有切屏检测。我一开始想在前端监听visibilitychange事件,切屏超过 3 次就强制交卷。代码写起来简单,但测试时发现学生用双屏或者手机查答案,浏览器根本不会触发这个事件。后来改成前后端配合:前端记录切屏次数上报,后端在考试记录里存switch_count字段,交卷时如果超过阈值就标记异常,由老师决定是否作废成绩。这个方案不完美,但比纯前端检测靠谱。
第二个改造点是主观题判分。原版是老师逐份阅卷,效率太低。我接了一个简单的关键词匹配判分:老师给每道简答题设置若干关键词和分值权重,学生答案里命中关键词就得分。听起来很粗糙,但对于“写出下列名词的定义”这类题目,准确率能到七八成。实现时在Question实体里加了一个keywords字段,存 JSON 格式的关键词列表,判分时遍历匹配。
// 主观题关键词判分(简化版) public int judgeSubjective(Question question, String studentAnswer) { if (question.getKeywords() == null || studentAnswer == null) { return -1; // 没有关键词配置,转人工 } List<Keyword> keywords = JSON.parseArray(question.getKeywords(), Keyword.class); int totalScore = 0; for (Keyword kw : keywords) { if (studentAnswer.contains(kw.getWord())) { totalScore += kw.getScore(); } } return Math.min(totalScore, question.getScore()); // 不超过题目满分 }第三个改造点是题库导入。原版只支持手动录题,我加了一个 Excel 批量导入功能。坑在于 Excel 的列映射——不同老师做的模板列顺序不一样,有的把答案放在第 3 列,有的放在第 5 列。后来改成用列名匹配而不是列索引,模板第一行必须是固定的表头,代码里按表头名找列。这个改动让导入成功率从六成提到了九成以上。
从那以后我每次改这类项目,都强制走一遍“改配置 → 跑单测 → 本地联调 → 压测开考并发”的流程,不再直接改完就部署。尤其是考试系统,开考瞬间的并发比你想象的高,一个连接池配置没改对,全班考试就卡住了。希望这些经验能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取