简介:这是一套面向高校计算机相关专业学生与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、Tomcat 7与JDK 1.8环境下运行。目前已有43人学习,适合作为课程设计或毕业设计的参考实现,帮助读者快速理解JSP+Servlet项目的分层结构与考试业务逻辑。
1. 在线考试管理系统:从 JSP+Servlet 到数据库落地的完整路径
很多做 Java Web 课程设计或企业内训平台的工程师,第一次接触在线考试管理系统时,都会卡在同一个地方:前端页面能画出来,数据库表也能建,但一到“交卷后自动判分、成绩实时入库、错题回显”这条链路就翻车。这套基于 JSP+Servlet 的在线考试管理系统,核心就是用最经典的 Java Web 技术栈把“出题、答题、判卷、查分”四个动作串成闭环。它适合两类人:一是需要一套能跑通的 Java Web 综合案例来理解 MVC 分层和数据库增删改查的开发者;二是想拿它当骨架,二次改造成企业内训、认证刷题或校园考试平台的从业者。源码和数据库脚本是这套系统的落地载体,但真正值钱的是它把 Servlet 当控制器、JSP 当视图、JDBC 直连数据库这条链路讲清楚了。下面按“先跑通、再改对、后避坑”的顺序拆开讲。
2. 先把环境跑通:JSP+Servlet 项目的最小可运行配置
2.1 为什么这套系统仍然值得用 JSP+Servlet 做骨架
现在新项目大多直接上 Spring Boot,但 JSP+Servlet 的价值在于它把 HTTP 请求到数据库的每一层都暴露在你面前。在线考试管理系统里最典型的动作是“提交试卷”,这个动作在 Servlet 里就是doPost拿到参数、调用 DAO 批量插入答题记录、再触发判分逻辑。没有自动配置帮你隐藏细节,你被迫理解web.xml里 servlet 映射和 filter 的执行顺序。对于要理解数据库增删改查本质的人来说,这比直接写 MyBatis 映射文件更直观。常见做法是:用 Tomcat 作为容器,JDK 8 或 11 都行,MySQL 5.7/8.0 作为数据库,IDE 选 IntelliJ IDEA 或 Eclipse。这套组合在课程设计案例源码里出现频率最高,资料也最好找。
2.2 目录结构与 web.xml 的关键配置
拿到源码后先别急着改代码,把目录结构看清楚。典型布局是src/main/java下按controller、dao、model、util分包,src/main/webapp下放 JSP、CSS、JS 和WEB-INF/web.xml。数据库连接配置一般放在src/main/resources/db.properties或直接写在DBUtil类里。下面是一个最小化的web.xml配置片段,重点看 servlet 和 filter 的映射关系:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1"> <!-- 登录过滤器:拦截未登录访问考试页 --> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.exam.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/exam/*</url-pattern> </filter-mapping> <!-- 交卷 Servlet:处理答题提交 --> <servlet> <servlet-name>SubmitExamServlet</servlet-name> <servlet-class>com.exam.controller.SubmitExamServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>SubmitExamServlet</servlet-name> <url-pattern>/submitExam</url-pattern> </servlet-mapping> </web-app>这段配置的逻辑是:所有/exam/开头的请求先过AuthFilter,检查 session 里有没有用户信息;/submitExam单独映射到交卷 Servlet。参数说明:url-pattern决定拦截范围,filter-class必须写全限定类名。如果你用的是 Servlet 3.0+ 注解方式,可以不用web.xml,直接在类上加@WebServlet("/submitExam"),但课程设计里为了展示配置能力,通常保留 XML。
2.3 数据库建表与连接池的最小配置
在线考试管理系统的数据库至少需要五张表:用户表、试卷表、题目表、答题记录表、成绩表。下面给出核心三张的建表 SQL,字段类型按 MySQL 8.0 写:
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(64) NOT NULL, `role` tinyint DEFAULT '0' COMMENT '0学生 1教师', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `question` ( `id` int NOT NULL AUTO_INCREMENT, `paper_id` int NOT NULL, `content` text NOT NULL, `option_a` varchar(255) DEFAULT NULL, `option_b` varchar(255) DEFAULT NULL, `option_c` varchar(255) DEFAULT NULL, `option_d` varchar(255) DEFAULT NULL, `answer` char(1) NOT NULL, `score` int DEFAULT '5', PRIMARY KEY (`id`), KEY `idx_paper` (`paper_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `record` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `paper_id` int NOT NULL, `question_id` int NOT NULL, `user_answer` char(1) DEFAULT NULL, `is_correct` tinyint DEFAULT '0', `submit_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_paper` (`user_id`,`paper_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表时注意utf8mb4而不是utf8,否则题干里的特殊符号会乱码。record表的idx_user_paper联合索引是为了加速“查某用户某试卷的答题记录”这个高频查询。连接池用 Druid 或 HikariCP 都行,如果源码里是直连 JDBC,建议改成连接池,否则并发交卷时连接数会爆。常见做法是在DBUtil里初始化DruidDataSource,把url、username、password从db.properties读进来。
3. 核心链路实现:从登录鉴权到自动判分的 Servlet 写法
3.1 登录与权限过滤:AuthFilter 的完整逻辑
登录功能看起来简单,但在线考试管理系统里权限分学生和教师两种角色,学生只能答题查分,教师能出题阅卷。AuthFilter要做的就是在请求到达 Servlet 之前判断 session 里的role字段。下面是一个可抄的过滤器实现:
public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); // session 为空或没有用户信息,跳回登录页 if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 教师页面额外校验角色 String uri = request.getRequestURI(); if (uri.contains("/teacher/") && !"1".equals(String.valueOf(session.getAttribute("role")))) { response.sendError(403, "无权限访问"); return; } chain.doFilter(req, resp); } }逻辑说明:getSession(false)表示不主动创建新 session,避免未登录用户也生成 session 浪费内存。sendRedirect要带上request.getContextPath(),否则部署到非根路径时会 404。参数说明:role存的是字符串"0"或"1",比较时用String.valueOf防止类型转换异常。这个过滤器是整套系统安全的第一道门,翻车最多的地方是忘记在web.xml里配filter-mapping,导致过滤器根本不生效。
3.2 交卷与自动判分:SubmitExamServlet 的批量处理
交卷是在线考试管理系统最核心的动作。前端把每道题的答案以questionId_answer的形式提交,Servlet 拿到后要逐题比对正确答案、计算得分、批量插入record表、更新score表。下面是一个简化但可运行的实现:
@WebServlet("/submitExam") public class SubmitExamServlet extends HttpServlet { private QuestionDao questionDao = new QuestionDao(); private RecordDao recordDao = new RecordDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); int userId = (int) session.getAttribute("userId"); int paperId = Integer.parseInt(req.getParameter("paperId")); // 查出该试卷所有题目,用于比对答案 List<Question> questions = questionDao.listByPaper(paperId); int totalScore = 0; List<Record> records = new ArrayList<>(); for (Question q : questions) { String userAnswer = req.getParameter("q_" + q.getId()); boolean correct = q.getAnswer().equalsIgnoreCase(userAnswer); if (correct) totalScore += q.getScore(); Record r = new Record(userId, paperId, q.getId(), userAnswer, correct); records.add(r); } // 批量插入答题记录,再写成绩 recordDao.batchInsert(records); recordDao.saveScore(userId, paperId, totalScore); resp.sendRedirect("score.jsp?paperId=" + paperId); } }逻辑说明:先查题目再循环比对,避免每道题都查一次数据库。batchInsert用 JDBC 的addBatch和executeBatch,比单条插入快一个数量级。参数说明:q_前缀是前端 input 的 name 约定,改前端时两边要同步。totalScore是累加得分,最后写入成绩表。这里最容易踩的坑是userAnswer为 null 时equalsIgnoreCase会抛空指针,所以实际代码里要先判空。
3.3 成绩查询与错题回显:JSP 里怎么安全展示数据
成绩页要展示总分和每道题的对错,JSP 里用 JSTL 遍历record列表最稳妥。下面是一个错题回显的片段:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <c:forEach items="${records}" var="r"> <tr class="${r.correct ? 'correct' : 'wrong'}"> <td>${r.questionContent}</td> <td>你的答案:${r.userAnswer}</td> <td>正确答案:${r.correctAnswer}</td> </tr> </c:forEach> </table>逻辑说明:c:forEach遍历后端request.setAttribute("records", ...)传过来的列表,r.correct是布尔值,直接控制 CSS 类名。参数说明:JSTL 的c标签需要在web.xml或 Maven 依赖里引入jstl和standard两个 jar。注意 JSP 里不要用脚本片段<% %>写业务逻辑,否则后期改造成前后端分离时工作量翻倍。常见做法是把所有查询逻辑放在 Servlet 里,JSP 只负责渲染。
4. 避坑与排查:JSP+Servlet 考试系统最常见的 5 个翻车点
4.1 中文乱码:从请求到响应要设三道编码
现象:题干或用户名显示成问号,交卷后答案比对全部错误。原因:Tomcat 默认用 ISO-8859-1 解析 POST 请求体,JSP 页面没声明pageEncoding。解决:在web.xml里加CharacterEncodingFilter,或者每个 Servlet 开头写req.setCharacterEncoding("UTF-8"),同时 JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>。数据库连接 URL 也要加useUnicode=true&characterEncoding=utf8。这三处缺一处都会乱码,血泪经验是先用一个中文参数从表单打到 Servlet 再打到数据库,全链路验证一遍。
4.2 数据库连接泄漏:Connection 没关导致 Tomcat 卡死
现象:系统跑十几分钟后所有请求都超时,Tomcat 日志里出现Cannot get a connection。原因:DAO 里Connection、PreparedStatement、ResultSet没有在finally块里关闭,连接池被耗尽。解决:用 try-with-resources 写法,或者统一在DBUtil.close()里关。如果源码里是直连 JDBC,建议直接换成 Druid 连接池,并在db.properties里设maxActive=20、maxWait=3000。排查时可以在 MySQL 里执行show processlist,看有多少个 Sleep 状态的连接。
4.3 交卷重复提交:刷新页面导致成绩翻倍
现象:学生交卷后按 F5 刷新,成绩表里出现两条记录,总分翻倍。原因:doPost处理完后直接sendRedirect到成绩页,但浏览器刷新的是submitExam这个 POST 请求。解决:用 session 存一个submitToken,交卷前校验并立即失效;或者交卷成功后sendRedirect到一个 GET 请求的成绩页,让浏览器地址栏变成 GET URL。更稳妥的做法是在record表上加user_id + paper_id唯一索引,重复插入直接报错回滚。
4.4 题目顺序错乱:HashMap 遍历导致选项对不上
现象:学生看到的选项顺序和数据库里存的不一致,判分时正确答案对不上。原因:后端用HashMap存题目,遍历时顺序不确定。解决:用LinkedHashMap或ArrayList保证顺序,前端渲染时按questionId排序。如果题目选项本身要随机打乱,那就在后端打乱后把映射关系存 session,判分时按映射还原。这个坑在课程设计案例源码里非常常见,因为很多模板代码直接用HashMap图省事。
4.5 时间字段时区偏差:交卷时间比实际早 8 小时
现象:成绩表里的submit_time比北京时间少 8 小时。原因:MySQL 的serverTimezone没配,JDBC 用 UTC 解析datetime。解决:连接 URL 加serverTimezone=Asia/Shanghai,或者把 MySQL 全局时区设为+08:00。如果已经存了错误数据,用UPDATE record SET submit_time = DATE_ADD(submit_time, INTERVAL 8 HOUR)批量修正。注意 MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.Driver。
5. 进阶改造:把 JSP+Servlet 考试系统变成可复用的判分引擎
跑通基础链路后,真正值得投入的是把判分逻辑抽成独立模块。我一般会把SubmitExamServlet里的判分代码抽到一个ScoreEngine类,输入是List<Question>和Map<Integer, String>的答案映射,输出是ScoreResult(总分 + 错题列表)。这样改的好处是:以后加多选题、判断题、填空题,只需要在ScoreEngine里加分支,Servlet 不用动。下面是一个支持多选判分的核心方法:
public class ScoreEngine { // 支持单选和多选,多选答案用逗号分隔且顺序无关 public ScoreResult calculate(List<Question> questions, Map<Integer, String> userAnswers) { int total = 0; List<WrongItem> wrongList = new ArrayList<>(); for (Question q : questions) { String ua = userAnswers.getOrDefault(q.getId(), ""); boolean correct; if ("multi".equals(q.getType())) { // 多选:排序后比较,忽略顺序 correct = sortAnswer(ua).equals(sortAnswer(q.getAnswer())); } else { correct = q.getAnswer().equalsIgnoreCase(ua); } if (correct) { total += q.getScore(); } else { wrongList.add(new WrongItem(q.getId(), ua, q.getAnswer())); } } return new ScoreResult(total, wrongList); } private String sortAnswer(String ans) { if (ans == null) return ""; char[] chars = ans.replace(",", "").toCharArray(); Arrays.sort(chars); return new String(chars); } }逻辑说明:sortAnswer把多选答案的字符排序后比较,解决“AB”和“BA”被判错的问题。参数说明:q.getType()需要在question表加一个type字段,默认single。改造时注意兼容旧数据,type为 null 时按单选处理。验证方法是造一组多选答案顺序不同的测试用例,跑单元测试看是否都判对。
另一个值得做的改造是加一个ExamSession对象,把“开始考试时间、剩余时长、已答题数”存在 session 里,前端用 JavaScript 定时器倒计时,后端在交卷时校验是否超时。超时交卷可以强制收卷,也可以拒绝提交并提示。这个逻辑放在AuthFilter之后、SubmitExamServlet之前,用一个TimeCheckFilter实现。我自己的习惯是:任何跟时间相关的逻辑,先在数据库里存start_time和duration,判分时用System.currentTimeMillis()算差值,不要依赖前端传的时间戳,否则改系统时间就能作弊。这套系统改到这一步,基本能支撑一个校内小规模考试场景,剩下的就是压力测试和日志监控了。希望帮到你。
本文还有配套的精品资源,点击获取