简介:这是一套面向高校计算机相关专业学生与Java Web初学者、可用于毕业设计或课程设计的在线考试管理系统完整源码包,基于JSP+Servlet技术栈实现,前端采用jQuery与Bootstrap构建页面,后端通过Servlet与JDBC完成业务逻辑与数据库交互。系统分为学生与管理员两类角色:学生可在线答题,答题过程中实时显示倒计时与进度,交卷后可查看错题,错题以红色标记并绿色标注正确答案,便于复习;管理员则负责题目、试卷、用户、角色及系统功能的管理,支持增删改查、关键词搜索与权限分配。资源包共104个文件,包含43个Java源文件、22个JSP页面、12个JAR依赖包,以及CSS、JS、图片等静态资源,另附SQL建库脚本与项目说明文档,压缩包约6.14MB,需在IDEA 2021.3、MySQL 5.7.26、Tomcat 7.0.73与JDK 1.8环境下运行。目前已有43人学习,适合需要完整实战项目参考、快速搭建考试系统原型的读者。
1. 从一份 JSP 毕设源码说起:在线考试管理系统能跑起来的关键在哪
每年毕业季,计算机专业的同学都会面对同一个问题:选题定了,代码从哪来。在线考试管理系统是出现频率极高的题目之一,原因很直接——业务逻辑清晰、功能模块完整、技术栈经典,答辩时也容易讲清楚。但真正拿到一份 JSP + Servlet 的源码包之后,很多人会卡在第一步:环境跑不起来。Tomcat 版本不对、JDK 编译级别不匹配、数据库连不上、页面 404,这些问题比写代码本身更消耗时间。
这份基于 JSP + Servlet 的在线考试管理系统,前端用 JSP、jQuery 和 Bootstrap,后端用 Servlet + JDBC,数据库是 MySQL。功能覆盖了学生在线答题、查看错题,以及管理员的题目管理、试卷管理、用户管理、角色权限分配。它不是一个玩具 Demo,而是一套有完整权限体系和业务闭环的课程设计级系统。适合正在做毕设、需要一套可运行、可讲解、可二次开发的 Java Web 项目的同学,也适合想回顾传统 Servlet 开发模式的后端初学者。
2. 环境搭建与数据库初始化:把项目从压缩包跑到登录页
2.1 四个版本号的硬约束
这份源码对环境版本有明确要求,不是随便装一个就能跑。原文给出的组合是 IntelliJ IDEA 2021.3、MySQL 5.7.26、Tomcat 7.0.73、JDK 1.8。这四个版本号里,最容易翻车的是 Tomcat 和 JDK 的搭配。
Tomcat 7 对应的是 Servlet 3.0 规范,而 Tomcat 8 以上是 Servlet 3.1+。如果项目 web.xml 里声明的是 3.0,用 Tomcat 9 部署通常也能跑,但有些老项目的 JSP 页面用了已废弃的标签库或 API,在高版本 Tomcat 上会直接报 500。JDK 方面,JDK 1.8 是底线,JDK 11 以上编译时可能遇到javax.servlet包找不到的问题——因为 Jakarta EE 9 之后包名已经从javax.*改成了jakarta.*。所以如果你本地装的是 JDK 17,要么切回 1.8,要么在 IDEA 里把项目 SDK 和语言级别都改成 8。
MySQL 5.7 和 8.0 的差异主要在驱动类和连接 URL 上。5.7 用com.mysql.jdbc.Driver,8.0 用com.mysql.cj.jdbc.Driver,URL 后面还要加时区参数。这份源码按 5.7 写的,如果你只有 8.0,改两处配置也能跑,后面会讲。
2.2 数据库导入与连接配置
拿到源码后,先别急着往 IDEA 里导。第一步应该是把数据库建起来。源码包里一般有一个.sql文件,用命令行或 Navicat 导入都行。
# 登录 MySQL,创建数据库并导入表结构和初始数据 mysql -u root -p # 在 MySQL 命令行中执行 CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE exam_system; SOURCE /path/to/exam_system.sql; # 确认表是否导入成功 SHOW TABLES;导入完成后,检查一下有没有初始管理员账号。通常.sql文件末尾会插入一条 admin 用户记录,密码可能是明文也可能是 MD5。如果是 MD5,登录时用对应密码;如果是明文,第一次登录后建议改掉。
接下来改数据库连接配置。JSP + Servlet 项目一般用 JDBC 直连,配置文件可能是db.properties、c3p0-config.xml或者直接写在某个工具类里。找到类似下面的代码:
// 常见的 JDBC 工具类写法,位于 util 包下 public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/exam_system?useUnicode=true&characterEncoding=utf-8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; private static final String DRIVER = "com.mysql.jdbc.Driver"; public static Connection getConnection() throws Exception { Class.forName(DRIVER); return DriverManager.getConnection(URL, USER, PASSWORD); } }如果你用的是 MySQL 8.0,把 DRIVER 改成com.mysql.cj.jdbc.Driver,URL 后面加上&serverTimezone=Asia/Shanghai&useSSL=false。改完之后,写一个 main 方法测一下getConnection()能不能返回非 null,这一步能省掉后面很多排查时间。
2.3 IDEA 项目导入与 Tomcat 配置
IDEA 导入项目时,选择File → Open,指向源码根目录。如果项目不是 Maven 结构,IDEA 可能不会自动识别 Web 模块。这时候需要手动配置:
第一步,打开Project Structure,在Modules里确认Web模块存在,Web Resource Directory指向web或WebContent目录,Source Roots包含src。
第二步,在Artifacts里添加一个Web Application: Exploded,输出目录默认即可。这一步决定了 Tomcat 部署时能不能找到编译后的 class 文件和 JSP。
第三步,配置 Tomcat Run Configuration。在Server标签页选择本地 Tomcat 7 安装目录,Deployment标签页添加刚才的 Artifact,Application context建议设为/exam或/。如果设为/,访问路径就是http://localhost:8080/;如果设为/exam,就是http://localhost:8080/exam/。
启动之前,检查web.xml里的welcome-file-list,确认默认首页是login.jsp还是index.jsp。有些项目首页放在WEB-INF下,不能直接访问,需要通过 Servlet 转发。启动后如果看到登录页,说明环境基本通了。
3. 学生答题模块拆解:从选题到交卷的完整链路
3.1 试题列表与开始答题的 Servlet 流转
学生登录后,第一件事是看到试题列表。这个列表通常由PaperServlet或ExamServlet处理,从paper表里查出所有状态为可用的试卷,转发到paper_list.jsp。页面上每套试卷有一个「开始答题」按钮,点击后发起一个 GET 或 POST 请求,带上paperId。
// PaperServlet 中处理试卷列表和进入答题的片段 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action)) { List<Paper> papers = paperService.getAllEnabledPapers(); request.setAttribute("papers", papers); request.getRequestDispatcher("/student/paper_list.jsp").forward(request, response); } else if ("start".equals(action)) { int paperId = Integer.parseInt(request.getParameter("paperId")); List<Question> questions = questionService.getQuestionsByPaperId(paperId); request.setAttribute("questions", questions); request.setAttribute("paperId", paperId); request.getRequestDispatcher("/student/exam.jsp").forward(request, response); } }这里的关键参数是paperId,它决定了从question表里按试卷关联关系取出哪些题目。常见做法是试卷和题目之间有一张中间表paper_question,存paper_id和question_id的对应关系。如果源码里没有中间表,而是直接在question表里加了paper_id字段,那说明一套题目只能属于一张试卷,灵活性差一些,但毕设够用。
进入exam.jsp后,页面会用 JSTL 或 Scriptlet 遍历题目列表,渲染题干和选项。每道题的选项通常是 radio 按钮,name 属性用question_${question.id}这种格式,方便交卷时按题目 ID 收集答案。
3.2 倒计时与答题进度更新的前端逻辑
答题页右上角有倒计时和进度显示,这是通过 JavaScript 实现的。倒计时一般从试卷表里的duration字段取值,单位是分钟,前端换算成秒后每秒递减。
// exam.jsp 中的倒计时与进度更新逻辑 var totalSeconds = parseInt("${paper.duration}") * 60; var timer = setInterval(function() { totalSeconds--; var minutes = Math.floor(totalSeconds / 60); var seconds = totalSeconds % 60; document.getElementById("countdown").innerText = minutes + ":" + (seconds < 10 ? "0" : "") + seconds; if (totalSeconds <= 0) { clearInterval(timer); alert("答题时间到,系统将自动交卷"); document.getElementById("examForm").submit(); } }, 1000); // 每次选择答案后更新进度 function updateProgress() { var total = parseInt("${fn:length(questions)}"); var answered = document.querySelectorAll("input[type=radio]:checked").length; document.getElementById("progress").innerText = answered + "/" + total; }这段代码有两个容易出问题的地方。一是totalSeconds的初始值,如果试卷的duration字段为空或者为 0,倒计时会立刻触发交卷。二是setInterval在页面刷新后不会保留状态,如果学生不小心刷新了页面,倒计时会重置。常见做法是把剩余时间存到 session 或 localStorage 里,但这份源码可能没做这么细,刷新即重置算是已知边界。
进度更新依赖querySelectorAll统计已选中的 radio 数量。如果题目有多选,radio 要换成 checkbox,统计逻辑也要相应调整。原文说的是「每次答完一题会实时更新」,所以每道题的 radio 上应该绑定了onclick="updateProgress()"事件。
3.3 交卷与错题标记的实现方式
交卷时,表单提交到SubmitExamServlet,后端遍历所有题目,把学生答案和标准答案比对,算出得分,然后把答题记录插入exam_record表,同时把错题信息存下来。
// SubmitExamServlet 中判分与错题记录的核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int paperId = Integer.parseInt(request.getParameter("paperId")); int userId = (int) request.getSession().getAttribute("userId"); List<Question> questions = questionService.getQuestionsByPaperId(paperId); int score = 0; List<Integer> wrongQuestionIds = new ArrayList<>(); for (Question q : questions) { String studentAnswer = request.getParameter("question_" + q.getId()); if (studentAnswer != null && studentAnswer.equals(q.getCorrectOption())) { score += q.getScore(); } else { wrongQuestionIds.add(q.getId()); } } examRecordService.saveRecord(userId, paperId, score, wrongQuestionIds); request.setAttribute("score", score); request.setAttribute("wrongIds", wrongQuestionIds); request.getRequestDispatcher("/student/result.jsp").forward(request, response); }错题标记是在result.jsp里做的。页面重新遍历题目列表,如果题目 ID 在wrongIds里,就用红色样式标记学生答案,用绿色样式标注正确答案。常见做法是用 JSTL 的fn:contains或 EL 表达式判断,然后给对应的<span>或<div>加 class。
<!-- result.jsp 中错题标红、正确答案标绿的片段 --> <c:forEach items="${questions}" var="q"> <div class="question"> <p>${q.content}</p> <c:set var="isWrong" value="${fn:contains(wrongIds, q.id)}" /> <span class="${isWrong ? 'wrong-answer' : 'correct-answer'}"> 你的答案:${param['question_' + q.id]} </span> <c:if test="${isWrong}"> <span class="correct-answer">正确答案:${q.correctOption}</span> </c:if> </div> </c:forEach>这里有个细节:交卷后如果直接刷新result.jsp,param里的答案会丢失,因为表单参数不会保留。所以更稳妥的做法是把学生答案也存到 session 或数据库里,结果页从后端取数据渲染。如果源码里结果页刷新后答案变空,就是这个原因。
4. 管理员后台与权限体系:题目、试卷、角色的联动
4.1 题目管理与试卷管理的增删改查
管理员登录后进入后台,题目管理是最常用的功能。题目表一般包含id、content(题干)、option_a到option_d、correct_option、status(锁定状态)、score等字段。新增题目时,前端表单提交到QuestionServlet,后端用 JDBC 执行 INSERT。
// QuestionServlet 中新增题目的处理逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String content = request.getParameter("content"); String optionA = request.getParameter("optionA"); String optionB = request.getParameter("optionB"); String optionC = request.getParameter("optionC"); String optionD = request.getParameter("optionD"); String correctOption = request.getParameter("correctOption"); int score = Integer.parseInt(request.getParameter("score")); Question q = new Question(); q.setContent(content); q.setOptionA(optionA); // ... 其他选项赋值 q.setCorrectOption(correctOption); q.setScore(score); q.setStatus(1); // 1 表示可用,0 表示锁定 questionService.addQuestion(q); response.sendRedirect("question?action=list"); }试卷管理稍微复杂一点。新建试卷时,管理员填入试卷名称和题目数量,系统需要从题库里随机或手动选取对应数量的题目,建立试卷与题目的关联。如果源码用的是「随机抽题」,那PaperServlet里会有一个ORDER BY RAND() LIMIT ?的 SQL;如果是手动选题,就会有一个穿梭框或复选框列表让管理员勾选。
关键词搜索功能一般是在列表页加一个输入框,提交后后端用LIKE '%keyword%'查询。注意这里要防 SQL 注入,用PreparedStatement的?占位符,不要直接拼接字符串。
4.2 角色权限分配的表结构设计
角色管理是这份源码里比较有含金量的部分。典型的 RBAC(基于角色的访问控制)设计至少需要三张表:user、role、user_role,再加上menu和role_menu来控制菜单可见性。
| 表名 | 关键字段 | 作用 |
|---|---|---|
| user | id, username, password, status | 存储用户基本信息 |
| role | id, role_name, description | 定义角色,如管理员、学生 |
| user_role | user_id, role_id | 用户与角色的多对多关系 |
| menu | id, menu_name, parent_id, url | 系统菜单树 |
| role_menu | role_id, menu_id | 角色与菜单的权限关联 |
权限分配页面通常会渲染一棵菜单树,每个菜单节点前有一个复选框。管理员勾选后提交,后端先删除该角色在role_menu里的旧记录,再批量插入新记录。
// RoleServlet 中保存角色菜单权限的逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int roleId = Integer.parseInt(request.getParameter("roleId")); String[] menuIds = request.getParameterValues("menuIds"); roleMenuService.deleteByRoleId(roleId); if (menuIds != null) { for (String menuId : menuIds) { roleMenuService.add(roleId, Integer.parseInt(menuId)); } } response.sendRedirect("role?action=list"); }这里有个常见坑:如果菜单树有父子层级,勾选父节点时子节点应该自动全选,取消子节点时父节点应该变成半选状态。这份源码可能只做了简单的复选框,没有实现半选逻辑,但不影响功能。
4.3 登录拦截与 Session 校验
后台页面不能随便访问,所以需要一个登录拦截器。JSP + Servlet 项目里,常见做法是用一个Filter或者在每个 Servlet 里手动检查 session。
// LoginFilter 中校验 session 的片段 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String uri = request.getRequestURI(); // 放行登录页、登录接口和静态资源 if (uri.contains("login.jsp") || uri.contains("LoginServlet") || uri.contains(".css") || uri.contains(".js")) { chain.doFilter(req, res); return; } Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); } else { chain.doFilter(req, res); } }web.xml里要注册这个 Filter,并配置url-pattern为/*。注意静态资源放行,否则 CSS 和 JS 加载不了,页面样式会全乱。如果登录后仍然跳回登录页,检查一下 session 的user属性名是否和 Filter 里取的一致。
5. 避坑与排查:这份源码跑不起来时先看这几条
5.1 启动报 ClassNotFoundException: com.mysql.jdbc.Driver
现象:Tomcat 启动后访问登录页,提交表单时后台抛异常,提示找不到 MySQL 驱动类。
原因:项目WEB-INF/lib目录下没有放mysql-connector-java的 jar 包,或者放的是 8.0 版本的驱动但代码里写的还是旧驱动类名。
解决:确认WEB-INF/lib下有mysql-connector-java-5.1.x.jar。如果用的是 Maven,在pom.xml里加对应依赖并重新打包。如果只有 8.0 驱动,把DBUtil里的DRIVER改成com.mysql.cj.jdbc.Driver,URL 加上时区参数。
5.2 页面 404:JSP 放在 WEB-INF 下直接访问不到
现象:在浏览器地址栏直接输入http://localhost:8080/exam/student/exam.jsp,返回 404。
原因:WEB-INF目录下的 JSP 不能通过 URL 直接访问,必须经过 Servlet 转发。这是 Servlet 规范的安全机制,不是配置错误。
解决:从登录页正常走业务流程,让 Servlet 通过request.getRequestDispatcher("/WEB-INF/...").forward()转发。如果确实需要直接访问,把 JSP 移到WEB-INF外面,但不建议这么做。
5.3 中文乱码:表单提交后数据库里全是问号
现象:新增题目时输入中文,保存到数据库后变成???,或者页面显示乱码。
原因:三个环节的编码不一致——JSP 页面编码、request 编码、数据库连接编码。
解决:JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;Servlet 里在取参数前加request.setCharacterEncoding("UTF-8");数据库连接 URL 加useUnicode=true&characterEncoding=utf-8;MySQL 建库时用utf8mb4。如果是 GET 请求乱码,Tomcat 7 的server.xml里 Connector 标签加URIEncoding="UTF-8"。
5.4 倒计时不显示或立刻交卷
现象:进入答题页后,右上角倒计时显示0:00或者直接弹出交卷提示。
原因:试卷表里的duration字段为空、为 0,或者前端取值时类型转换失败。
解决:检查数据库中该试卷的duration值,确保是正整数。前端 JS 里加一个兜底判断,如果totalSeconds小于等于 0,默认设为 60 分钟。另外确认 EL 表达式${paper.duration}能取到值,如果paper对象没有放到 request 域里,取出来就是空。
5.5 权限菜单勾选后不生效
现象:管理员给角色分配了菜单权限,但用该角色登录后菜单没有变化。
原因:菜单渲染时没有根据当前用户的角色去查role_menu表,或者查了但缓存了旧数据。
解决:检查菜单查询的 SQL 是否关联了role_menu和user_role。常见写法是SELECT m.* FROM menu m JOIN role_menu rm ON m.id = rm.menu_id JOIN user_role ur ON rm.role_id = ur.role_id WHERE ur.user_id = ?。如果用了 session 缓存菜单列表,权限修改后需要重新登录或手动刷新 session。
6. 二次开发与验证:从能跑到能讲清楚
6.1 用 Postman 或浏览器控制台验证接口
项目跑起来之后,不要只点页面。想确认某个 Servlet 的逻辑是否正确,可以用 Postman 直接发请求。比如测试交卷接口:
# 用 curl 模拟交卷请求,带上 session cookie curl -X POST "http://localhost:8080/exam/SubmitExamServlet" \ -H "Cookie: JSESSIONID=你的sessionId" \ -d "paperId=1&question_1=A&question_2=B&question_3=C"返回结果如果是重定向到结果页,说明接口通了。如果返回 500,看 Tomcat 日志里的堆栈,定位到具体哪一行。浏览器控制台的 Network 面板也能看到每个请求的参数和响应,比在代码里打断点更快。
6.2 二次开发时优先改哪几个文件
如果打算基于这份源码做二次开发,建议按这个顺序动刀:
| 优先级 | 文件/目录 | 改动理由 |
|---|---|---|
| 高 | DBUtil.java 或 db.properties | 换数据库、换连接池最先改这里 |
| 高 | web.xml | 改默认首页、加 Filter、改 Servlet 映射 |
| 中 | exam.jsp / result.jsp | 答题和结果页的交互体验最值得优化 |
| 中 | QuestionServlet.java | 题目管理是后台核心,扩展题型从这里入手 |
| 低 | 前端 CSS 文件 | 样式调整不影响功能,可以最后做 |
如果要把 JSP + Servlet 改成 Spring Boot 项目,工作量主要在依赖注入和配置迁移上。Servlet 里的doGet/doPost逻辑可以保留,但要用@Controller或@RestController重新组织。JDBC 工具类可以换成 MyBatis 或 JPA,但毕设答辩时老师可能更认可手写 JDBC,因为能讲清楚底层原理。
6.3 答辩时怎么讲清楚这套系统的技术点
答辩不是演示功能,是讲技术决策。几个容易被问到的地方提前准备:
为什么用 JSP 而不是前后端分离?答:项目规模小,JSP 开发效率高,适合课程设计的时间约束。如果问 Servlet 的生命周期,答:init只调用一次,service每次请求都调,destroy在 Tomcat 关闭时调。问 session 和 cookie 的区别,答:session 存在服务端,cookie 存在客户端,JSESSIONID 是两者的桥梁。
还有一个高频问题:你的系统安全性怎么保证?答:登录用 Filter 拦截,SQL 用 PreparedStatement 防注入,密码可以加 MD5 摘要。如果老师追问 MD5 不安全怎么办,答:毕设层面够用,生产环境应该用 bcrypt 加盐哈希。
6.4 一个我踩过的坑:Tomcat 端口冲突
最后说一个血泪经验。有次帮学弟调这个项目,Tomcat 启动一直报Address already in use,查了半天以为是 Tomcat 配置问题,后来发现是 IDEA 里之前启动的另一个项目没关干净,8080 端口被占着。从那以后我每次启动 Tomcat 之前都强制走一遍端口检查:
# Windows 下查 8080 端口占用 netstat -ano | findstr :8080 # Linux/Mac 下查 8080 端口占用 lsof -i :8080找到 PID 后,要么 kill 掉,要么在 IDEA 的 Tomcat 配置里把 HTTP 端口改成 8081。这个习惯帮我省了很多莫名其妙的启动失败排查时间。希望帮到你。
本文还有配套的精品资源,点击获取