☰
基于Java Web的在线考试系统:从JSP+Servlet到Spring Boot改造实践
2026/11/1 1:09:32 网站建设 项目流程

简介:这是一套面向Java Web学习者的在线考试系统完整项目,整合源码与数据库脚本,适合课程设计、毕业设计或框架入门实战。项目基于IDEA开发,运行在MySQL、JDK、Tomcat环境下,内置管理员、教师、学生三类账号,便于体验不同角色的核心流程。压缩包共566个文件,约3.2MB,以Java源码、XML配置、HTML页面、JavaScript交互、CSS/LESS样式以及SQL数据库脚本等类型为主,并附带少量图片、字体资源,目录结构完整,导入开发工具后即可对照学习。系统涵盖用户管理、试题维护、在线考试、成绩统计等常见考试业务模块,Controller、Entity、Mapper等分层结构清晰,适合快速理解Java Web项目组织方式。已有902人浏览学习,对有Java基础、希望获取可运行项目作为参考的读者具有较高实用价值。

1. 这套基于Java web的在线考试系统:为什么老技术栈现在还能拿来跑业务

拿到这份“基于Java web的在线考试系统(源码+数据库).zip”,你先别急着双击开跑。这类压缩包在Java课程设计案例源码里几乎是最常见的一种选题:学生端选卷答题、教师端组卷判分、管理员管账号,主流程完整,前端没有复杂框架,后端是JSP+Servlet+JDBC直连MySQL。对两类人很有用:一类是要在两周内交付一个“能点能跑”的Java web项目,另一类是接手了公司内部培训考核需求,想先拿现成系统当底座改功能。它不能直接上线当生产考试系统,但能让你在最短时间内看明白一场在线考试在代码里到底怎么流转——这比空读Java基础八股文有用得多。

2. 把项目拆开看:JSP+Servlet+JDBC经典分层,以及四张核心表怎么串起一场考试

拿到源码不要先开IDE,先看分层和表结构。在线考试系统的业务主流程是“建卷→答题→判分→出成绩”,这几件事分别落在不同的包和Servlet里,理解它们的关系比背代码重要。

2.1 为什么这套源码是“能跑通的原型”而不是生产系统

打开源码你会发现,包名和类名通常长这样:entity、dao、servlet、web、util,典型的JSP做展示、Servlet做控制、DAO用JDBC做增删改查。数据库连接写到properties文件里,JSP页面之间用forward和redirect跳转。没有Spring,没有Maven,甚至可能没有接口定义。

这套结构放到今天的生产环境里有很多明显短板,但它有一个不可替代的优点:分层足够直白,断点容易打,任何一层出问题都能快速定位。

层次常见文件职责改造方向
展示层student/exam.jsp、teacher/paper_list.jsp渲染页面、收集表单换Thymeleaf或前后端分离
控制层ExamServlet、LoginServlet接收请求、调DAO、跳转拆成Spring MVC Controller
数据层QuestionDAO、ScoreDAOJDBC增删改查、拼SQL换MyBatis或JPA
公共部分DBUtil、BaseServlet连库、编码、分页换成连接池和Spring容器

我一般建议把这类源码当“业务原型”用:表结构、流程判断、角色边界都值得参考,但Servlet里写业务、JSP里写Java代码的部分要重写。

2.2 四张核心表:试卷、试题、答题记录、成绩怎么串起一次考试

不管项目里表名带不带前缀,在线考试系统的核心数据模型翻来覆去就这么几张。只要你先建好这四张表,再回看代码,基本不会迷路。

-- 试卷表:定义一次考试的基本信息 CREATE TABLE t_paper ( id INT PRIMARY KEY AUTO_INCREMENT, paper_name VARCHAR(100) NOT NULL, duration INT NOT NULL DEFAULT 60, -- 考试时长,单位分钟 total_score INT NOT NULL DEFAULT 100, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 试题表:挂在某份试卷下的具体题目 CREATE TABLE t_question ( id INT PRIMARY KEY AUTO_INCREMENT, paper_id INT NOT NULL, -- 外键,指向t_paper qtype VARCHAR(20) NOT NULL, -- single/multiple/truefalse content VARCHAR(500) NOT NULL, -- 题干 option_a VARCHAR(200), option_b VARCHAR(200), option_c VARCHAR(200), option_d VARCHAR(200), answer VARCHAR(50) NOT NULL, -- 单选存A;多选存A,B,C;判断存TRUE/FALSE score INT NOT NULL DEFAULT 5, KEY idx_paper_id (paper_id) );

成绩表相对简单:

CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, paper_id INT NOT NULL, score DECIMAL(5,1) NOT NULL, start_time DATETIME, -- 进入考试页的时间 submit_time DATETIME, -- 实际交卷时间 KEY idx_student_paper (student_id, paper_id) );

这里有一个关键设计你要留意:题目表存的是answer字段,不是correct_answer_id。这种方式的好处是单选、多选、判断都能用字符串统一存;坑在于多选的答案顺序如果不做标准化,判分时会出现“A,C”和“C,A”不匹配的问题,后面的判分逻辑我会专门讲。

2.3 学生、教师、管理员三个角色,入口和服务落在哪些模块

源码里通常有三个Servlet前缀,分别对应三个角色。学生入口是“选试卷→进考试→交卷→看成绩”,教师入口是“维护题库→手动组卷→人工调分”,管理员入口是“用户管理→考试时段配置→数据统计”。

这三个角色在代码里的控制方式比较粗暴:登录成功后把用户实体放进Session,然后每个页面顶部判断角色。常见做法是写一个BaseServlet,在doGet和doPost里统一做登录校验,再按角色白名单过滤请求。

// 角色过滤的常见写法:在BaseServlet里统一处理 String role = (String) session.getAttribute("role"); if (role == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } if (!allowedRoles.contains(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; }

这个写法的好处是简单直观,坏处是权限全在Servlet里重复判断,改一处漏一处。如果后面要往生产方向改,建议把角色和URL的映射关系抽出来,做成配置表或者注解,而不是散落在每个Servlet开头。

3. 从压缩包到浏览器能点:导入数据库、部署Tomcat、跑通一场模拟考试

这章的目标只有一个:让你在半小时内让系统在浏览器里能点起来。很多人卡在这一步,大部分不是因为代码有错,而是环境版本选得不对。

3.1 环境选型:JDK 8、Tomcat 8.5、MySQL 5.7 的保守搭配

老项目最怕新环境。JSP+Servlet这套东西,JDK 8搭配Tomcat 8.5是兼容性最稳的组合;MySQL用5.7,编码和驱动都没什么幺蛾子。如果你机器上装了MySQL 8.0,也不是不能用,但驱动类名要改,后面避坑章节会单独说。

组件推荐版本原因
JDK1.8老代码不依赖新语法,避免编译报错
Tomcat8.5.x支持Servlet 3.1,javax包名配对
MySQL5.7.xutf8和驱动行为最传统,少踩坑
MySQL驱动5.1.x类名是com.mysql.jdbc.Driver,和代码里一致

3.2 把SQL脚本灌进MySQL:source命令和必须改的三处连接配置

压缩包里通常带一个.sql文件,文件名一般是exam_db.sql或者exam_system.sql。先打开看一眼,里面如果有CREATE DATABASE,就在MySQL命令行里直接执行;如果没有,先手动建库再导入。

# 进入MySQL命令行,注意指定默认字符集 mysql -u root -p --default-character-set=utf8 -- 在MySQL命令行内执行 source /path/to/exam_db.sql;

导入之后立刻验证一下:

SHOW TABLES; SELECT COUNT(*) FROM t_question;

如果表都出来了,但中文是乱码,先不要动代码,在MySQL里执行ALTER DATABASE exam_db CHARACTER SET utf8mb4;,再重新导入一遍。

连接配置通常藏在三个地方:src/db.properties、WEB-INF/classes/jdbc.properties、META-INF/context.xml。不管它在哪个文件里,你要改的永远是下面这几行:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/exam_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的数据库密码

注意几点:localhost改成你的数据库实际地址;3306是MySQL默认端口,如果你改过要同步;characterEncoding=utf-8这一行建议保留,它能解决80%的中文乱码问题;serverTimezone=Asia/Shanghai是MySQL 8.0驱动才需要的参数,5.7可加可不加。

3.3 部署到Tomcat:war包和源码目录,哪一种更适合拿来改

解压压缩包后,先看目录结构。如果是war包,直接丢到Tomcat的webapps目录下,启动Tomcat就会自动解压。如果你是拿源码改,建议用Eclipse IDE for Enterprise Java 或 IntelliJ IDEA直接导入,配好Tomcat运行环境后,用“Add Framework Support”识别成Web项目。

典型的源码目录长这样:

exam-system/ ├── src/ │ ├── com/exam/entity/ │ ├── com/exam/dao/ │ ├── com/exam/servlet/ │ └── com/exam/util/ ├── WebContent/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ │ ├── student/ │ ├── teacher/ │ ├── admin/ │ └── login.jsp └── exam_db.sql

部署时重点看web.xml,它决定了你的访问路径。常见配置是:

<welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list>

启动Tomcat后,浏览器访问http://localhost:8080/exam-system/,如果能跳到登录页,说明部署成功。这里最容易犯的错是把项目名搞错——访问路径是WebContext名字,不是磁盘目录名。

3.4 最小启动验证:注册一个考生账号,走完一场考试的完整链路

页面能开不代表流程通,我建议你按真实考生的操作路径完整走一遍:注册账号→登录→进考试列表→选一份试卷→答几道题→交卷→看成绩。这个过程能暴露90%的问题。

# 用curl做个最简单的登录验证,-c保存会话Cookie curl -c cookies.txt \ -d "username=test001&password=123456" \ http://localhost:8080/exam-system/student/login.jsp # 查看返回状态,200说明登录请求被处理了 # 再用会话Cookie请求考试列表页 curl -b cookies.txt http://localhost:8080/exam-system/student/exam_list.jsp

浏览器里能看到试卷列表后,点进一场考试,确认下面三件事:倒计时在走、答题后能保存、交卷后成绩能出来。这三件事全通,这个系统就算跑起来了。如果有一步断掉,优先看Tomcat日志——老项目的错误几乎都会打在catalina.out或localhost.log里,比看控制台可靠。

4. 考试核心逻辑的三处硬骨头:组卷、计时、自动判分怎么改

系统能跑起来之后,真正需要你动手改的就是组卷、计时、判分这三块。这三块也是面试里最常被追问的地方。

4.1 手动组卷和随机组卷:“试卷引用”和“试卷快照”不是一回事

打开QuestionDAO,最常见的手动组卷是这样:按paper_id查试题列表,前端展示出来让你勾选。对应SQL:

-- 手动组卷:按试卷编号取题 SELECT id, content, qtype, answer, score FROM t_question WHERE paper_id = ? ORDER BY id; -- 随机组卷:从题库里抽题 SELECT id, content, qtype, answer, score FROM t_question WHERE qtype = 'single' ORDER BY RAND() LIMIT 20;

这里有一个业务边界你要想清楚:数据库里t_question.paper_id只存了“这张试卷引用了哪些题”,并没有存“考生答题时看到的题目长什么样”。这意味着如果教师在考试结束后改了某道题的内容,历史成绩单里关联的题目也跟着变了。这叫“试卷引用”而不是“试卷快照”。

生产级考试系统一定会在交卷时把题干、选项、答案都冗余存到答题记录表里,保证历史试卷永久定格。改造方案是在答题记录表里增加question_snapshot字段,存JSON或完整文本。如果你只打算自己用,这个坑可以先记着。

4.2 倒计时在浏览器还是在服务端:两种写法的边界

大多数老系统的倒计时是纯前端JS实现的:进入考试页时用setInterval每秒减一,时间到了触发交卷。这种写法最大的毛病是刷新页面就“满血复活”,学生按一下F5就能重置时间。懂点前端的人甚至打开开发者工具改全局变量。

正确的做法是双向校验:前端倒计时只做展示,服务端在Session或数据库里记录真正的开始时间,交卷时按服务端时间判断是否超时。

// 考试开始时间在进入考场的Servlet里写入Session session.setAttribute("examStartTime", System.currentTimeMillis()); // 交卷时校验超时 long startTime = (Long) session.getAttribute("examStartTime"); long elapsed = System.currentTimeMillis() - startTime; int duration = paper.getDuration(); // 分钟 if (elapsed > duration * 60 * 1000L) { // 超时交卷:按实际用时截断,或者直接标记为超时提交 examRecord.setTimeoutFlag(true); }

参数说明:duration是试卷表里的考试时长;超时按分钟算,所以乘60 * 1000L转成毫秒。实践里我一般还会加一个15秒的宽容期,避免网络延迟导致最后一秒交卷被误判为超时。

4.3 自动判分逻辑:单选、多选、判断题的判断藏在哪个DAO

判分逻辑通常在ScoreDAO或者ExamServlet的submitExam方法里。它不复杂,但边界条件特别多。核心代码长这样:

// 遍历考生提交的每一道题 for (SubmittedAnswer item : submittedAnswers) { Question q = questionDAO.findById(item.getQuestionId()); // 没作答直接判错 if (item.getAnswer() == null || item.getAnswer().trim().isEmpty()) { item.setCorrect(false); continue; } // 单选题和判断题:字符串直接比较,忽略大小写 if ("single".equals(q.getQtype()) || "truefalse".equals(q.getQtype())) { item.setCorrect( q.getAnswer().trim().equalsIgnoreCase(item.getAnswer().trim()) ); } // 多选题:必须全部匹配,少选、错选都不给分 else if ("multiple".equals(q.getQtype())) { item.setCorrect( sameAnswerSet(q.getAnswer(), item.getAnswer()) ); } }

sameAnswerSet这个方法是网上很多源码里写得最草率的地方。常见做法是把两边的答案字符串按逗号拆成数组,转成TreeSet再去比较——TreeSet会自动排序,就能解决“A,C”和“C,A”不一致的问题。

多选还有个业务规则要跟需求方确认:少选是得一半分还是不得分?老系统里大多数是“全对才得分”,这个规则决定了判分分支的写法,改起来也最简单。

4.4 交卷时并发写成绩:为什么要在事务里先锁考生记录

交卷这个操作看起来简单,实际最容易出脏数据。学生手快点了两次交卷按钮,或者开着两个标签页同时交,成绩表里就可能出现两条记录。网上很多源码的解决方式是前端按钮置灰,这挡不住懂行的学生。

后端必须在事务里做防重:

// 交卷时先锁定考生在本场考试的状态,防止重复提交 BEGIN; SELECT * FROM t_score WHERE student_id = ? AND paper_id = ? FOR UPDATE; -- 如果查不到记录,才允许插入成绩 INSERT INTO t_score (student_id, paper_id, score, start_time, submit_time) VALUES (?, ?, ?, ?, ?); COMMIT;

FOR UPDATE会把这条考生记录锁住,第二个交卷请求会阻塞到事务提交,然后因为记录已存在而走向“更新成绩”而不是“插入成绩”的分支。注意这里的事务一定要在Service层控制,不能把BEGIN和COMMIT散落在DAO里,不然连接池归还连接时事务状态是乱的,后续请求会莫名报错。

5. 从能跑到敢用的避坑清单:5条翻车记录和解决办法

这套系统跑通不难,但要从“自己电脑上能点”变成“敢给别人用”,中间全是细节坑。下面这5条都是我实际带项目时翻过车的,按“现象→原因→解决”写,你可以直接对照排查。

5.1 数据库中文乱码:库、连接串、JSP页面三级编码没对齐

现象:登录页是中文,注册后用户名在列表里变成“???”或者乱码。

原因:三处编码不一致。第一是数据库本身是latin1,第二是jdbc连接串没带characterEncoding=utf-8,第三是JSP页面没有统一用UTF-8输出。

解决:依次检查三处。先改库:ALTER DATABASE exam_db CHARACTER SET utf8mb4;,把已有表也一起改;再改连接串,加useUnicode=true&characterEncoding=utf-8;最后看JSP页面顶部的contentType是否带charset=UTF-8,把它们统一成一套。改完重启Tomcat,重新插入一条中文数据验证。

5.2 换了JDK版本,JSTL和Servlet版本编译报错

现象:导入项目后javax.servlet的类大面积报错,编译期就过不去。

原因:常见情况是用了Tomcat 10.x——从Tomcat 10开始,Servlet规范从javax改名成jakarta,老代码的包名全部失效。或者本机JDK版本过高,老代码里用了不兼容的过时API。

解决:最省事的是退回Tomcat 9以下版本,配合JDK 8跑老项目。如果你的需求方指定了必须用Tomcat 10,那就只能全局搜索替换包名:把所有javax.servlet换成jakarta.servlet,再把web.xml头部的命名空间改成新版本,工程量大但可行。

5.3 考试途中刷新页面,倒计时和已答题全丢

现象:学生答了一半按F5刷新,倒计时归零,已答题目变空白。

原因:答题状态只存在浏览器端——倒计时在JS变量里,已答选项在页面DOM里,刷新就清空。老系统的经典毛病。

解决:我有两个方案,按成本排序。最低成本方案:把当前答题进度存到sessionStorage,刷新时页面加载完成后回填;正规方案:每切换一题就AJAX把答案存到t_exam_record表,刷新时按student_id+paper_id查回草稿状态。后者不仅能防刷新,还能支持“断网恢复”,企业内网考核建议直接上这个。

5.4 自动判分和教师手工调分,成绩表里到底以谁为准

现象:教师后台给某道题调了分,但学生端查成绩时分数没变,或者变了但总分不对。

原因:源码里有两套逻辑都在改成绩。一套是交卷时的自动判分,写t_score表;另一套是教师调分,要么单独写在t_adjustment表里,要么直接UPDATE分数,但查询时没有把两处合并,导致显示口径不一致。

解决:明确一个原则——成绩只以t_score.score字段为准,手工调分就是直接改这个字段,同时加一个adjust_time字段记录最后修改时间。如果源码里已经建了调分表,要么查询时JOIN合并,要么停用调分表。注意自动判分和手工调分不要同时写,否则后写的人会覆盖前一个人的结果。

5.5 MySQL 8.0 驱动类和权限的兼容坑

现象:启动Tomcat后报ClassNotFoundException: com.mysql.jdbc.Driver。

原因:源码lib目录里放的是MySQL 5.x的驱动jar,但你的数据库是MySQL 8.0。8.0的驱动类名改成了com.mysql.cj.jdbc.Driver,并且连接串必须带serverTimezone参数。

解决:换驱动jar,把mysql-connector-java-5.1.x.jar换成8.0.x版本,同时改properties里的driver类名。另外MySQL 8.0默认的认证插件是caching_sha2_password,老驱动不认,需要在MySQL里给连接用的账号改成mysql_native_password:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

这几条如果按顺序处理完,你的系统基本就具备“敢给别人用”的基础了。

6. 往生产方向迈一步:把DAO换成Spring Boot接口,数据库设计原样保留

如果你打算把这个系统继续做下去,最值得的投资不是优化JSP页面,而是把它迁到Spring Boot上。迁移的切入点很清晰:数据库表结构几乎可以原样保留,真正要重写的是三层——Entity对应用户表、试题表、成绩表,Mapper层替代原来的DAO,Service层把Servlet里的业务判断搬进去。

@RestController @RequestMapping("/api/exam") public class ExamController { private final ExamService examService; public ExamController(ExamService examService) { this.examService = examService; } @PostMapping("/submit") public Result submit(@RequestParam Integer paperId, @RequestBody List<SubmitItem> answers, HttpSession session) { Integer studentId = (Integer) session.getAttribute("studentId"); // 判分和防重的业务逻辑在Service里做,Controller只做参数中转 ScoreVO score = examService.submitExam(studentId, paperId, answers); return Result.success(score); } }

迁移这事我做过不止一次,给你一个排序建议:先迁“交卷判分”这个接口,因为它最独立、逻辑最容易验证;再迁“组卷列表”接口;最后迁“用户登录”和“后台管理”。每次迁完一个接口,就用curl配合数据库比对结果,保证老的JSP页面和新的REST接口同时存在也不冲突。

另一个值得做的小改造是把题库导入做成Excel模板。老系统所有题目都是页面里一条一条录入,教师维护题库的成本极高。新的Spring Boot版本里接一个EasyExcel或POI,支持批量导入、批量更新answer字段,一套模板能覆盖几百道选择题,这个功能做完,教师才真正愿意把旧数据迁进来。

我最初跑通这套系统的时候,满脑子只想换框架,后来才发现最值钱的是那几张表和它们之间的关系。框架迭代快,业务模型才是这个项目里真正能沉淀下来的东西。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询