基于JavaWeb的在线考试系统:Servlet、JDBC与事务实践
简介基于JavaWeb的在线考试系统毕业设计说明文档系统性地呈现了从需求分析到系统测试的完整开发流程。文档以B/S架构为主线选择JSPMySQLJDBC作为技术方案详细介绍了系统开发背景与意义、开发工具、可行性分析、数据库设计、三层结构设计、界面设计以及登录注册、在线考试、试卷设置、试题管理、成绩统计与查询等核心模块的实现思路。考生、教师、管理员三类角色的功能边界与操作流程也都有清晰说明并配有软件测试内容便于对照验证。这份毕业设计说明为单个docx文档共1.25MB包含中英文摘要、目录和约44页正文章节层次分明适合计算机相关专业毕业生、课程设计学生以及JavaWeb开发者作为论文撰写、系统开发或答辩准备的参考资料。目前已有396人学习浏览资源内容紧凑实用可帮助快速理解在线考试系统的设计与实现要点节省自研摸索时间。1. 在线考试系统设计与实现这件事先想清楚三个问题如果你打开那份标题为《基于JavaWeb的在线考试系统的设计与实现毕业设计说明》的文档看到的通常不是代码堆砌而是从选题背景一路写到测试部署的完整链路。这类系统几乎每年都会出现在毕业设计选题清单上不是因为选题陈旧而是它恰好覆盖了 JavaWeb 最核心的几个环节外层的 HTTP 请求处理、中层的业务状态管理、底层的数据库事务与一致性。把这个流程走通一遍Servlet、JSP、JDBC、Filter、Session、MySQL 这些技术就不再是孤立的概念。做在线考试系统之前先回答三个问题谁来用、考什么、怎么判分。教师要能维护题库和阅卷学生要能在线答题且提交后立刻知道分数管理员要能处理科目和用户数据。判分这件事最容易被低估——单选题、多选题、判断题三种题型的得分规则完全不同多选题漏选是否给分又是一种业务决策。这些规则一旦在纸上定死后面建表写代码都能顺理成章。本文就按照这个思路把设计文档变成可运行的工程并说明每步选择背后的理由适合准备答辩、正在做 JavaWeb 课程设计或想完整过一遍工程流程的读者。2. JavaWeb双路径选型Servlet JSP 与 Spring Boot 的取舍与工程骨架技术选型是本设计最先落地的决策点。大多数毕业设计文档用的是经典三层架构Web 层用 Servlet 接收浏览器请求业务层写 Java 类处理规则数据访问层用 JDBC 操作 MySQL。也有不少新版设计文档选择了 Spring Boot MyBatis理由是企业开发已是主流、起步快、内置 Tomcat。这两条路都成立差别在于你要演示的重点是手工搭建的完整 HTTP 处理链路还是贴近真实项目的工程化能力。2.1.1 为什么经典 Servlet 依然适合作为设计与实现的主线如果说明文档中的系统需求分析写得很详细比如有 processon 导出的用例图、E-R 图、数据流图那 Servlet JSP 的直白结构反而更好配合这些图去讲。Servlet 的生命周期、请求转发与重定向、Session 的创建与失效这些是 JavaWeb 教学案例里的高频考点答辩时评委大概率会围绕它们提问。Spring Boot 把容器细节都封装了反而少了很多可以展开讲的内容。工程骨架按 Maven 标准目录组织即可exam-system/ ├── pom.xml ├── src/main/java/com/example/exam/ │ ├── controller/ # Servlet 类 │ ├── service/ # 业务逻辑接口与实现 │ ├── dao/ # JDBC 数据访问 │ ├── entity/ # 实体类 │ └── filter/ # 登录与编码过滤器 └── src/main/webapp/ ├── jsp/ # 页面文件 └── WEB-INF/web.xmlpom.xml 里只要引入 servlet-apiprovided 作用域、mysql-connector-java、jstl 这三个依赖不需要引入任何框架。对这个体量的系统手写 JDBC 代码量完全可控每张表的增删改查加在一起不过几百行而且每一步都能在调试器里看到连接和预编译语句的真实行为。2.1.2 数据源与全局配置的初始化方式数据库连接不要在每个 DAO 方法里重复写 DriverManager.getConnection那会让连接资源失控。标准做法是写一个基于单例的数据库工具类用静态代码块完成驱动加载和连接池初始化。连接池不引入第三方库的话可以用 Apache DBCP2 或 HikariCP 单独作为一个依赖也可以退一步用 ThreadLocal 保存同一个事务内的连接引用。一个常见实现是定义 db.properties 配置文件jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/exam_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password********注意 url 里的 characterEncodingutf8 和 serverTimezoneAsia/Shanghai 两个参数。前者保证中文字符不乱码后者避免 MySQL 8.x 默认时区引发的时间类型报错。用 IDEA 创建 JavaWeb 项目时官方新版本向导虽然变了但传统做法还是建 Maven Web 项目之后手动补一个 web.xml或者直接用骨架 org.apache.maven.archetypes:maven-archetype-webapp 生成。工具类中读取配置的代码是每个 DAO 使用的基础设施下面这个封装值得直接抄进工程public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置初始化失败); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码的关键在于把配置文件的加载固定在静态代码块里类加载失败就直接抛出初始化异常应用启动阶段暴露问题而非运行到第一个请求才报错。close 方法单独封装是为了避免 finally 块里写三遍空判断。日常开发中很多人会在这个封装上偷懒在 DAO 里到处写连接获取和关闭遇到数据库连接泄漏时排查起来相当痛苦。参数说明Connection、PreparedStatement、ResultSet 三者关闭有顺序要求先 ResultSet 再 Statement 最后 Connection封装成 close(conn, ps, rs) 后调用方只需在 finally 块里传一次参数避免遗漏。连接池场景下 conn.close() 实际上是归还连接不要因为名字叫 close 就以为连接被销毁。2.1.3 web.xml 中的核心注册与编码过滤器JavaWeb 项目的请求入口都需要在 web.xml 里注册。Servlet 3.0 之后虽然可以用 WebServlet 注解替代但毕业设计说明文档通常要求有一段对部署描述符的讲解保留 web.xml 反而更好交代请求映射、过滤器链和欢迎页的关系。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameOnline Exam System/display-name filter filter-nameEncodingFilter/filter-name filter-classcom.example.exam.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-appEncodingFilter 必须映射为 /* 才能拦截所有请求包括直接访问 JSP 的请求。如果不写这个过滤器表单提交的中文会以页面编码提交与数据库连接串里的 characterEncoding 不一致查出来的数据就变成问号。课程设计里遇到中文乱码十次有八次是这一步没做。3. 在线考试系统的数据库设计与 DAO 层落地在线考试系统的表结构是整份说明文档里最好画图的部分也是最容易设计过度的地方。五张核心表足够覆盖所有业务场景用户表、科目表、题目表、试卷表含答卷、答题明细表。不需要设计复杂的角色权限中间表用户表上加一个 role 字段区分管理员、教师、学生即可。这对一个毕业设计来说职责清晰也方便在答辩时说清楚权限控制的粒度。3.1.1 核心表结构与关键字段设计建表时最重要的决策在于题目、试卷、答卷三者的关系如何表达。一个常见且稳妥的方案是题目表存所有试题并带 subject_id 外键考试表exam记录一次考试的基本信息包括科目、考试时长、总分试卷表exam_paper关联考试与题目用来固定这次考试选了哪些题答卷表记录学生提交的答案。CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT NOT NULL DEFAULT 2 COMMENT 1管理员 2教师 3学生, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; CREATE TABLE subject ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE question ( id INT PRIMARY KEY AUTO_INCREMENT, subject_id INT NOT NULL, type TINYINT NOT NULL COMMENT 1单选 2多选 3判断, content TEXT NOT NULL, option_a VARCHAR(255), option_b VARCHAR(255), option_c VARCHAR(255), option_d VARCHAR(255), answer VARCHAR(255) NOT NULL, score INT NOT NULL DEFAULT 5 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, subject_id INT NOT NULL, duration_minutes INT NOT NULL DEFAULT 60, total_score INT NOT NULL DEFAULT 100, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未发布 1已发布 2已结束 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_record ( id INT PRIMARY KEY AUTO_INCREMENT, exam_id INT NOT NULL, student_id INT NOT NULL, score INT DEFAULT NULL, submit_time DATETIME DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0考试中 1已交卷 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;question 表中的答案字段用 VARCHAR 而不是固定一个字符串是为了兼容多选题。多选题答案存成 A,B,D 这样的逗号拼接格式判分时拆分后比较集合即可。option 字段允许为空是为判断题准备的判断题只用 content 和 answer选项留空。exam 表里的 status 字段设计成 0、1、2 三态而不是单纯用布尔值方便后续扩展暂停考试等状态。考试记录表是关键中的关键。它必须在学生开始考试时插入一条记录而不是在交卷时才生成。这样可以记录谁开始过考试但没交卷、谁答到一半断线status字段能在崩溃场景下区分考试中和已交卷为管理员提供恢复依据。3.1.2 DAO 层事务处理方式与判分的数据流转判分是唯一需要数据库事务支撑的场景因为交卷时要做三件事批改答题明细、更新考试记录总分、把答题明细落库。三件事必须同时成功或同时失败否则会出现记录显示已交卷但没有成绩的脏数据。手动 JDBC 的写法是在 Service 层拿连接、关闭自动提交、继续使用这同一个连接执行多张表的更新最后统一 commit。这个模式比用 JdbcTemplate 更能讲清事务边界public void submitExam(int examId, int studentId, MapInteger, String answers) throws SQLException { Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement ps null; try { int totalScore 0; // 1. 逐题批改同时写入答题明细 for (Map.EntryInteger, String entry : answers.entrySet()) { Question q questionDao.findById(conn, entry.getKey()); boolean correct checkAnswer(q, entry.getValue()); int gotScore correct ? q.getScore() : 0; totalScore gotScore; examRecordDao.insertDetail(conn, examId, studentId, q.getId(), entry.getValue(), gotScore); } // 2. 更新考试记录状态与总分 examRecordDao.updateScoreAndStatus(conn, examId, studentId, totalScore); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { DBUtil.close(conn, ps, null); } }这个方法的逻辑说明外层 Map 的 key 是题目 IDvalue 是学生提交的答案。因为批改和插明细用的是同一个连接对象所以 checkAnswer 查题目和两个写入方法之间严格共享这一个事务。注意 DAO 方法签名上都带 Connection 参数这是手动事务管理的关键——连接由 Service 层创建和释放DAO 只负责执行 SQL事务边界由此清晰可见。参数含义说明conn.setAutoCommit(false)是事务开始的标志conn.commit()提交所有变更conn.rollback()回滚到事务开始前的状态。这里有一个容易写错的细节不要在 try 块里关闭连接放在 finally 里且要保证 rollback 时连接仍然存活。3.1.3 checkAnswer 的多选与判断题处理判分规则不是简单的字符串相等比较必须区分题型。单选题和判断题直接比较答案字符串多选题则要比较集合。这里有一个业务决策多选题漏选算不算分不同的设计文档策略不同常见做法是全对才得分这样实现最简单、答辩也好解释。private boolean checkAnswer(Question q, String studentAnswer) { if (studentAnswer null || studentAnswer.trim().isEmpty()) { return false; } String normalizedStudent normalize(studentAnswer); if (q.getType() 2) { String correct normalize(q.getAnswer()); String[] studentArr normalizedStudent.split(,); String[] correctArr correct.split(,); SetString sSet new HashSet(Arrays.asList(studentArr)); SetString cSet new HashSet(Arrays.asList(correctArr)); return sSet.equals(cSet); } return normalizedStudent.equalsIgnoreCase(normalize(q.getAnswer())); }normalize 方法统一去空格并转大写避免半角全角逗号混用导致误判。多选题判分最怕学生提交的选项顺序不同——A,B,D 和 B,D,A 是同一组答案但字符串直比会判错所以必须转成 Set 后再比较。4. 在线答题主流程与页面交互的关键实现细节整个系统的核心体验是答题页。学生点开考试、计时开始、逐题作答、提交试卷。这一个流程牵涉数据加载、倒计时刷新、交卷确认三层逻辑任何一个环节的缺陷都会在演示时被评委注意到。4.1.1 题目加载方式与页面渲染策略常见做法有两种一次性把整套题的 JSP 页面输出到浏览器或者按题号动态加载。毕业设计选前者理由很实际——代码简单、面试好讲、流量可控。在 Servlet 中查出题目列表后放入 request 域再 forward 到 exam.jsp页面通过 JSTL 的 c:forEach 循环渲染。这个方案下代码量最小WebServlet(/student/exam) public class StudentExamServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(false); if (session.getAttribute(student) null) { resp.sendRedirect(login.jsp); return; } int examId Integer.parseInt(req.getParameter(examId)); ListQuestion questions questionDao.findByExamId(examId); req.setAttribute(questions, questions); req.setAttribute(examId, examId); req.getRequestDispatcher(/jsp/exam.jsp).forward(req, resp); } }单次查询查出所有题目交给 JSP 渲染没有懒加载也不需要 AJAX 分页。题目数量控制在 50 题以内时性能完全没问题。这里需要注意的是 forward 而非 sendRedirect因为 request 域中的数据只有在 forward 情况下才能传递到 JSP。4.1.2 计时与自动交卷的核心写法答题页倒计时是最容易出 bug 的地方。只在前端用 JavaScript 倒计时是不够的页面一刷新计时就重新开始学生可以利用这个漏洞无限延长考试时间。正确处理方式是把结束时间存进 Session用 JS 每秒读取剩余秒数提示时间到后自动提交表单。let remainSeconds ${sessionScope.examEndTime - System.currentTimeMillis() / 1000}; function countDown() { if (remainSeconds 0) { document.getElementById(examForm).submit(); return; } let min Math.floor(remainSeconds / 60); let sec remainSeconds % 60; document.getElementById(timer).innerText 剩余时间 min 分 sec 秒; remainSeconds--; setTimeout(countDown, 1000); } window.onload countDown;这里的核心思路是开始考试时后端计算examEndTime System.currentTimeMillis() duration * 60000存入 Session。前端只是把它显示出来。即使学生刷新页面Session 里的截止时间不会重置刷新只会让 JS 重新获取同一个值继续倒数。后端在提交时还必须再校验一遍时间防止学生改浏览器本地时间或直接构造请求绕过前端限制。4.1.3 防止重复提交的令牌机制在线考试最容易收到的异常反馈是我明明没点交卷怎么成绩就出来了。常见诱因是学生双击提交按钮或者网络抖动导致重发请求。防止重复提交的 URL 层手段是重定向到结果页但更稳妥的做法是加一次性令牌。public class TokenFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; if (POST.equalsIgnoreCase(req.getMethod()) req.getRequestURI().contains(/submitExam)) { HttpSession session req.getSession(); String sessionToken (String) session.getAttribute(submitToken); String requestToken req.getParameter(token); if (sessionToken null || !sessionToken.equals(requestToken)) { ((HttpServletResponse) response).sendRedirect(error.jsp); return; } session.removeAttribute(submitToken); } chain.doFilter(request, response); } }页面生成时在 JSP 里放一个随机 token 到隐藏字段同时放入 Session。交卷提交时过滤器比较两者只允许第一次请求通过。注意提交成功后必须立刻从 Session 中移除这样第二次点击就无法通过校验。把这段逻辑写在 Filter 里而不是写在具体的 Servlet 中能让题库维护、用户管理等同样需要防重复的接口直接复用。5. Session 管理、异常处理与跨浏览器支持的三处排错在线考试系统里有两个高频扣分点考试中途 Session 过期导致提交失败以及学生用不兼容浏览器访问页面导致页面错乱或倒计时失效。这两类问题在设计说明中都属于运行时质量章节但往往被忽略直到答辩演示时才暴露。5.1.1 Session 超时策略与有效期刷新机制Tomcat 默认 Session 超时时间为 30 分钟但一次考试可能设置 60 分钟或更长。如果考试时长超过 Session 过期时间学生答到一半 Session 失效提交时直接被踢回登录页。解决方式是在 web.xml 中显式调大 Session 超时时间session-config session-timeout120/session-timeout /session-configsession-timeout 单位是分钟。设置 120 分钟在多数考试场景下够用也可以在设计文档里说明这是按最长考试时长加 30 分钟冗余计算的。注意这是一个全局配置会影响登录状态的有效期所以需要判断是否会与安全要求冲突。如果担心 Session 固定攻击可以在登录成功后调用request.changeSessionId()重新生成会话 ID。更优雅的方案是滑动过期在过滤器里检查请求是否来自已登录用户是则调用session.setMaxInactiveInterval(120 * 60)顺延有效期。但这个方案只适合有有效交互的场景学生开着页面不动是不算活跃的所以滑动过期对考试系统来说意义不大直接调大全局超时时间即可。5.1.2 统一异常处理与日志记录Servlet 3.0 之后可以在 web.xml 声明错误页error-page error-code500/error-code location/jsp/error.jsp/location /error-page error-page exception-typejava.lang.Exception/exception-type location/jsp/error.jsp/location /error-page配置后任何 Servlet 抛出未捕获异常用户都会看到统一的错误页面而不是 Tomcat 的默认堆栈信息页。这个细节对于答辩演示非常重要因为一旦演示时蹦出异常堆栈观感很差。与此同时堆栈不能只给用户看还要落到日志文件里通常做法是结合 log4j2 或直接在 catch 块中调用e.printStackTrace()并配合 IDEA 控制台排查。正经项目建议用 log4j2 的异步 Logger但课程设计里写清楚printStackTrace的局限性和日志框架的替代方案即可。5.1.3 跨浏览器兼容的三个常见坑在线答题页面依赖 JavaScript 倒计时和表单提交以下三个点在 Chrome 以外环境最容易出问题第一Date 对象解析的差异。倒计时代码里parseInt((new Date().getTime() - startTime) / 1000)这种写法没有兼容性问题但如果使用了new Date(2025-01-01 10:00:00)这种带横杠的字符串IE 会解析失败返回 NaN。统一使用时间戳传递。第二表单的自动完成行为。Chrome 会在刷新后自动恢复之前填过的 input 内容这在考试场景可能导致学生看到自己之前做的答案。在 form 标签上增加autocompleteoff即可禁用但对 MooTools 等老框架渲染出的内容可能需要额外处理。第三浏览器对 JSP 中\n在 textarea 里的回显差异。学生提交的文字型答案如果包含换行应统一转换后再存入 MySQL。\r\n与\n的混用在 Windows 与 Linux 环境中表现不一简单方案是入库前把\r\n替换成\n读取时前端用 CSSwhite-space: pre-wrap保持换行。6. 部署与答辩演示前的三个验收技巧开发完成不是终点真正的挑战是把整套工程在干净机器上从零跑通并且让验收环境稳定复现。这个环节最常见的翻车点是数据库版本差异和本地文件路径问题下面给出三个能直接照做的技巧。6.1.1 一键初始化数据库的脚本设计把建库、建表、插入种子数据三个步骤合并进一个 init.sql 脚本不要手动在 Navicat 里点鼠标执行。脚本头部加上建库和 USE 语句CREATE DATABASE IF NOT EXISTS exam_db DEFAULT CHARACTER SET utf8mb4; USE exam_db; SOURCE /absolute/path/to/schema.sql;SOURCE 命令在 MySQL 8.x 客户端下可以正常工作但路径必须是客户端机器的绝对路径。如果演示用的电脑没有配置环境变量应用启动时优先检查数据库连接串是否能通用 IDEA 自带的 Database 面板测试连接不要等到浏览器里查不到数据才去排查。6.1.2 演示数据的质量控制种子数据比其他任何东西都影响答辩观感。用户表里准备一个管理员账号、一个教师账号、两个学生账号。题库里为每个科目准备至少 20 道题覆盖单选、多选、判断三种题型。其中故意留一道多选题的正确答案是 A,C,D用来演示漏选不得分的规则。最重要的技巧是提前用学生账号完完整整考一遍试把成绩和答题记录留在库里。演示时优先展示已有成绩的过往记录然后再现场发起一场新考试。这样即使现场网络出了状况也不会因为没有数据可讲而冷场。6.1.3 验证自动判分结果的 SQL 检查方法演示完一场考试后用一行 SQL 核对总分计算是否正确SELECT er.id, er.student_id, er.score, SUM(rd.got_score) AS detail_sum FROM exam_record er JOIN record_detail rd ON er.id rd.record_id WHERE er.id 1 GROUP BY er.id, er.student_id, er.score HAVING detail_sum er.score;这条 SQL 找出总分字段与明细累加值不一致的记录专门用来验证事务提交的完整性。如果查询结果为空说明批改、累分、更新三步都处于一致状态如果查出数据则立即定位到事务回滚失效或某道题的 score 字段赋值异常。部署完成后在浏览器地址栏直接访问http://localhost:8080/exam-system/login.jsp用种子数据里的学生账号走一遍完整的答题、交卷、查看成绩的流程再回到 MySQL 里执行上面这条 SQL。本文还有配套的精品资源点击获取