原生Java Web在线考试系统:Servlet+JSP+MySQL实战搭建

📅 发布时间:2026/9/11 3:05:43
原生Java Web在线考试系统:Servlet+JSP+MySQL实战搭建
简介这是一套基于Java Web技术栈开发的在线考试系统完整源码面向Java初学者与Web开发入门者适用于课程设计、毕业设计或小型教学平台搭建场景。系统采用SSMSpringSpringMVCMyBatis主流框架支持管理员、教师、学生三类角色协同操作涵盖试题管理、在线考试、成绩统计与评价反馈等核心功能模块。资源包共566个文件包含45个Java业务逻辑类、96个XML配置文件含Mapper映射与Spring容器配置、52个JS前端交互脚本、36个HTML页面模板以及SCSS/LESS/CSS样式资源和MySQL建表SQL脚本整体压缩后仅3.2MB结构清晰、依赖明确。已有902人学习下载开箱即用——提供完整IDEA工程结构、本地运行说明http://localhost:8080、三套预置账号及配套MySQL数据库便于快速部署、调试与二次开发。1. 一个能跑起来、能改代码、能连真实数据库的 Java Web 在线考试系统到底长什么样很多刚学完 Servlet、JSP、MySQL 和 JDBC 的同学在期末作业或求职简历里卡在「项目」这一步网上搜到的“在线考试系统”要么只有截图没有源码要么解压后缺表结构、缺配置、缺 Tomcat 启动说明甚至web.xml里还写着localhost:8080/exam却没告诉你怎么配数据源。更常见的是——系统能登录但一进考试页面就 404或者能查题但提交后成绩不入库。这不是代码写得差而是缺少一套可验证、可调试、可本地复现的最小闭环。本文讲的就是基于标准 Java Web 技术栈Servlet JSP JDBC MySQL构建的在线考试系统它不依赖 Spring Boot 自动装配不包装成 Docker 镜像不绑定特定 IDE所有逻辑直白可见——从ExamServlet.java的doPost()到exam.sql里的CREATE TABLE exam_record每一行都经得起打断点、查日志、改字段。适合 Java 初学者跑通流程也适合面试前快速梳理 MVC 分层与事务边界。2. 用原生 Java Web 搭建考试系统为什么选 ServletJSP 而不是 Spring Boot2.1 选型依据教学场景下轻量可控比自动配置更重要在线考试系统的核心业务链路非常明确用户登录 → 加载试卷 → 答题提交 → 自动判分 → 成绩入库。这条链路上每个环节都需要显式控制请求流转、会话状态和数据库事务。Spring Boot 虽然能快速启动但其ControllerServiceRepository的三层抽象对初学者而言容易掩盖关键细节比如HttpSession如何与SessionAttributes绑定、Transactional在什么时机开启/回滚、JDBC 连接池参数如何影响并发答题响应。而原生 Servlet/JSP 方案中request.getSession().setAttribute(user, user)直接暴露会话管理逻辑connection.setAutoCommit(false)和connection.rollback()明确体现事务边界c:forEach标签遍历题目列表时JSP 页面与 Java Bean 属性名一一对应无反射黑盒。这种“看得见摸得着”的实现方式正是数据库课程设计、Java 期末作业、以及面试官考察基础功底时最看重的落点。提示本系统不使用 Maven 多模块或 Gradle 构建全部依赖以WEB-INF/lib/下的.jar文件形式存在含mysql-connector-java-8.0.33.jar、jstl-1.2.jar、standard-1.1.2.jar避免因构建工具版本差异导致ClassNotFoundException。2.2 目录结构解析从web.xml开始读懂请求入口解压.zip后标准 Java Web 工程目录如下OnlineExamSystem/ ├── src/ │ ├── servlet/ // 所有 Servlet 类 │ │ ├── LoginServlet.java │ │ ├── ExamStartServlet.java │ │ └── SubmitAnswerServlet.java │ ├── dao/ // 数据访问对象 │ │ ├── UserDao.java │ │ ├── QuestionDao.java │ │ └── RecordDao.java │ └── entity/ // POJO 实体类 │ ├── User.java │ ├── Question.java │ └── ExamRecord.java ├── WebContent/ │ ├── WEB-INF/ │ │ ├── web.xml // 核心配置servlet-mapping 和 context-param │ │ └── lib/ // 第三方 jar 包 │ ├── index.jsp // 登录页 │ ├── exam.jsp // 考试主界面含倒计时 JS │ └── result.jsp // 成绩展示页 └── exam.sql // 建库建表 SQL 脚本含初始试题数据其中web.xml是整个系统的路由中枢。关键配置段如下!-- 定义数据库连接参数 -- context-param param-namedriver/param-name param-valuecom.mysql.cj.jdbc.Driver/param-value /context-param context-param param-nameurl/param-name param-valuejdbc:mysql://localhost:3306/examdb?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8/param-value /context-param context-param param-nameusername/param-name param-valueroot/param-value /context-param context-param param-namepassword/param-name param-value123456/param-value /context-param !-- 注册 LoginServlet -- servlet servlet-nameLoginServlet/servlet-name servlet-classservlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping这段配置决定了当浏览器访问http://localhost:8080/OnlineExamSystem/login时Tomcat 会实例化LoginServlet并调用其doPost()方法。注意amp;是 XML 实体转义实际 URL 中应为serverTimezoneAsia/Shanghai是 MySQL 8 必填项否则抛java.sql.SQLException: The server time zone value XXX is unrecognized。2.3 数据库建模三张核心表的设计意图与字段约束exam.sql脚本创建三个表user考生信息、question题库、exam_record答题记录。建表语句并非简单堆字段而是体现业务规则-- 考生表密码必须加密存储系统默认用 MD5非 BCrypt教学简化 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(32) NOT NULL, -- MD5 后长度固定 32 realname VARCHAR(20), role ENUM(student,admin) DEFAULT student, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 题库表type 字段区分单选/多选/判断score 为每题分值 CREATE TABLE question ( id INT PRIMARY KEY AUTO_INCREMENT, type TINYINT NOT NULL COMMENT 1:单选,2:多选,3:判断, content TEXT NOT NULL, option_a VARCHAR(200), option_b VARCHAR(200), option_c VARCHAR(200), option_d VARCHAR(200), answer VARCHAR(10) NOT NULL COMMENT 如 A 或 AB 或 T, score TINYINT DEFAULT 2, difficulty TINYINT DEFAULT 1 COMMENT 1:易,2:中,3:难 ); -- 答题记录表联合唯一索引确保同一考生同一试卷只提交一次 CREATE TABLE exam_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, question_id INT NOT NULL, user_answer VARCHAR(20), is_correct BOOLEAN DEFAULT FALSE, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, FOREIGN KEY (question_id) REFERENCES question(id) ON DELETE RESTRICT ); ALTER TABLE exam_record ADD UNIQUE KEY uk_user_question (user_id, question_id);关键点说明user.password存储的是MD5(123456) e10adc3949ba59abbe56e057f20f883e登录时比对加密后字符串question.answer字段设计为VARCHAR(10)而非CHAR(1)支持多选答案如AB、ACDexam_record表的FOREIGN KEY约束中ON DELETE CASCADE表示删除考生时自动清理其答题记录而ON DELETE RESTRICT防止误删题目导致记录失效uk_user_question唯一索引防止重复提交同一题避免刷分漏洞。3. 本地运行全流程从数据库导入到 Tomcat 启动一步都不能跳3.1 数据库准备用 Navicat 或命令行执行exam.sql先确认 MySQL 服务已启动端口 3306然后创建数据库并导入脚本# 方式一命令行执行推荐避免字符集问题 mysql -u root -p -e CREATE DATABASE IF NOT EXISTS examdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -u root -p examdb /path/to/exam.sql注意exam.sql中若含SET NAMES utf8mb4;则必须确保 MySQL 配置文件my.cnf中[client]和[mysqld]段均设置default-character-setutf8mb4否则中文题目内容可能乱码。Navicat 导入时需在“高级”选项中勾选“使用 UTF8MB4 字符集”。验证数据是否正确加载-- 查看考生默认含 admin/123456 和 student/123456 两个账号 SELECT username, password, role FROM user; -- 查看题目检查 answer 字段是否为 A/AB/T 格式 SELECT id, content, answer, score FROM question LIMIT 5;3.2 Tomcat 配置修改context.xml绑定数据源非必需但强烈推荐虽然web.xml中通过context-param传参给 Servlet但更规范的做法是将数据库连接交由 Tomcat 管理。编辑$CATALINA_HOME/conf/context.xml在Context标签内添加Resource namejdbc/ExamDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/examdb?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8mb4 usernameroot password123456 maxActive20 minIdle5 maxWait10000 validationQuerySELECT 1 testOnBorrowtrue/随后修改src/servlet/LoginServlet.java中获取连接的方式// 替换原来的 DriverManager.getConnection(...) Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/ExamDB); Connection conn ds.getConnection(); // 此处自动启用连接池并在web.xml中声明资源引用resource-ref descriptionExam Database Connection/description res-ref-namejdbc/ExamDB/res-ref-name res-typejavax.sql.DataSource/res-type res-authContainer/res-auth /resource-ref提示若跳过此步直接用DriverManager也可运行但高并发下易出现Too many connections错误。maxActive20表示最多 20 个活跃连接足够应付 50 人以内同时考试。3.3 Eclipse/IntelliJ 导入与部署关键三步校验步骤 1确认项目编码为 UTF-8右键项目 → Properties → Resource → Text file encoding → 选择UTF-8。否则 JSP 中中文显示为??。步骤 2配置 Tomcat RuntimeWindow → Preferences → Server → Runtime Environments → Add → Apache Tomcat v9.0 → 指向解压目录。确保 JRE 版本 ≥ Java 8本系统编译级别为 1.8。步骤 3部署并启动右键项目 → Run As → Run on Server → 选择已配置的 Tomcat。启动后访问http://localhost:8080/OnlineExamSystem/应看到登录页。输入student/123456登录进入考试页后点击“开始考试”页面应动态加载 10 道题来自question表倒计时 JS 正常运行。若遇 404请检查web.xml中url-pattern是否与表单action/login一致LoginServlet.class是否编译成功并位于WEB-INF/classes/servlet/LoginServlet.classTomcat 控制台是否有SEVERE: Error filterStart—— 通常因WEB-INF/lib/缺少jstl.jar或standard.jar。4. 关键业务逻辑实现答题提交与自动判分的完整链路4.1 提交动作拆解从前端表单到后端事务控制考试页exam.jsp中每道题生成独立form提交至SubmitAnswerServlet!-- exam.jsp 片段 -- c:forEach items${questions} varq varStatusstatus div classquestion p${status.count}. ${q.content}/p c:if test${q.type 1} input typeradio nameanswer_${q.id} valueA ${q.option_a}br input typeradio nameanswer_${q.id} valueB ${q.option_b}br !-- ... -- /c:if c:if test${q.type 3} input typecheckbox nameanswer_${q.id} valueT 正确 input typecheckbox nameanswer_${q.id} valueF 错误 /c:if /div /c:forEach button typebutton onclicksubmitAll()提交试卷/button script function submitAll() { const forms document.querySelectorAll(form); forms.forEach(form form.submit()); // 逐题提交简化版实际应 AJAX 批量 } /scriptSubmitAnswerServlet.java接收请求后执行原子性操作protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userId (String) request.getSession().getAttribute(userId); if (userId null) { response.sendRedirect(index.jsp); return; } Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); // 获取连接来自 context.xml 或 DriverManager conn.setAutoCommit(false); // 开启事务 // 遍历所有题目的 answer_* 参数 EnumerationString paramNames request.getParameterNames(); while (paramNames.hasMoreElements()) { String paramName paramNames.nextElement(); if (paramName.startsWith(answer_)) { int qid Integer.parseInt(paramName.substring(7)); String userAnswer request.getParameter(paramName); // 查询标准答案 String sql SELECT answer FROM question WHERE id ?; ps conn.prepareStatement(sql); ps.setInt(1, qid); ResultSet rs ps.executeQuery(); String correctAnswer rs.next() ? rs.getString(answer) : ; // 判分逻辑单选/多选/判断分别处理 boolean isCorrect false; if (T.equals(userAnswer) || F.equals(userAnswer)) { isCorrect userAnswer.equals(correctAnswer); // 判断题 } else if (userAnswer ! null !userAnswer.isEmpty()) { isCorrect userAnswer.equalsIgnoreCase(correctAnswer); // 忽略大小写 } // 插入答题记录 String insertSql INSERT INTO exam_record (user_id, question_id, user_answer, is_correct) VALUES (?, ?, ?, ?); ps conn.prepareStatement(insertSql); ps.setInt(1, Integer.parseInt(userId)); ps.setInt(2, qid); ps.setString(3, userAnswer); ps.setBoolean(4, isCorrect); ps.executeUpdate(); } } conn.commit(); // 全部成功才提交 response.sendRedirect(result.jsp); } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} throw new ServletException(答题失败 e.getMessage(), e); } finally { DBUtil.close(ps, conn); } }关键参数说明conn.setAutoCommit(false)确保 10 道题全部插入成功才写库任一题失败则整体回滚userAnswer.equalsIgnoreCase(correctAnswer)兼容大小写如用户选a而标准答案为ADBUtil.close()统一关闭资源避免连接泄漏。4.2 成绩计算在result.jsp中聚合查询而非内存计算result.jsp不在 Java 代码中累加分数而是通过 SQL 聚合一次性查出总分% String userId (String) session.getAttribute(userId); Connection conn DBUtil.getConnection(); String sql SELECT SUM(q.score) as total_score, COUNT(*) as answered_count FROM exam_record r JOIN question q ON r.question_id q.id WHERE r.user_id ? AND r.is_correct true; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, userId); ResultSet rs ps.executeQuery(); int totalScore 0, answeredCount 0; if (rs.next()) { totalScore rs.getInt(total_score); answeredCount rs.getInt(answered_count); } DBUtil.close(rs, ps, conn); % h2考试结果/h2 p答对题数% answeredCount %/10/p p总得分% totalScore %/20/p注意此处SUM(q.score)依赖question.score字段若题目分值不同如第 1 题 2 分、第 2 题 3 分该 SQL 自动适配若改为固定 2 分/题则可简化为COUNT(*) * 2。5. 常见故障定位与加固技巧让系统真正“可用”5.1 5 种典型报错的根因与修复指令报错现象控制台关键日志根本原因修复命令/操作HTTP Status 404 – /OnlineExamSystem/loginNo servlet mapping found for /loginweb.xml中servlet-mapping的url-pattern与表单action不一致检查web.xml的url-pattern是否为/login且action中无多余路径如/OnlineExamSystem/loginjava.lang.ClassNotFoundException: com.mysql.cj.jdbc.DriverCaused by: java.lang.ClassNotFoundExceptionWEB-INF/lib/缺少mysql-connector-java-8.0.33.jar或版本不匹配下载 MySQL Connector/J 8.0.33 → 复制到WEB-INF/lib/→ 清理 Tomcatwork/Catalina/localhost/缓存目录The server time zone value UTC is unrecognizedjava.sql.SQLException: The server time zone value UTCMySQL URL 缺少serverTimezone参数修改web.xml中url参数追加serverTimezoneAsia/ShanghaiHTTP Status 500 – Cannot create JDBC driver of class org.apache.tomcat.dbcp.dbcp.SQLNestedException: Cannot create JDBC drivercontext.xml中driverClassName拼写错误如com.mysql.jdbc.Driver旧版写法改为com.mysql.cj.jdbc.DriverMySQL 5.7 必须用cjDuplicate entry 1-5 for key uk_user_questionjava.sql.SQLIntegrityConstraintViolationException同一考生重复提交同一题前端禁用提交按钮onclickthis.disabledtrue;后端try-catch捕获该异常并提示“已提交过”5.2 三项低成本加固提升系统健壮性1登录密码加密升级从 MD5 到 BCrypt当前LoginServlet.java中验证逻辑为String inputPass request.getParameter(password); String md5Input DigestUtils.md5Hex(inputPass); // Apache Commons Codec String sql SELECT * FROM user WHERE username? AND password?; ps.setString(1, username); ps.setString(2, md5Input);升级为 BCrypt需引入bcrypt-1.0.0.jar// 验证时 String inputPass request.getParameter(password); String storedHash getUserPasswordFromDB(username); // 从数据库查出 BCrypt 哈希值 if (BCrypt.checkpw(inputPass, storedHash)) { // 自动比对盐值 // 登录成功 }提示BCrypt 哈希值长度约 60 字符需将user.password字段改为VARCHAR(60)。2防重复提交利用HttpServletRequestWrapper拦截二次请求新建RepeatableRequestWrapper.java重写getParameterMap()在SubmitAnswerServlet前添加过滤器public class RepeatFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; String uri request.getRequestURI(); if (uri.endsWith(/submit)) { String ip getClientIP(request); String key ip : System.currentTimeMillis() / 60000; // 每分钟限一次 if (RedisUtil.exists(key)) { // 假设集成 Redis ((HttpServletResponse) resp).sendError(HttpServletResponse.SC_BAD_REQUEST, 请勿重复提交); return; } RedisUtil.setex(key, 60, 1); // 60秒有效期 } chain.doFilter(req, resp); } }3考试超时强制交卷前端 JS 后端双重校验exam.jsp中倒计时结束触发if (timeLeft 0) { alert(时间到自动提交); document.getElementById(autoSubmitForm).submit(); }后端SubmitAnswerServlet增加时间戳校验long startTime (Long) session.getAttribute(examStartTime); if (System.currentTimeMillis() - startTime 600000L) { // 10分钟超时 // 记录超时标记不影响判分 request.setAttribute(timeout, true); }result.jsp中显示c:if test${timeout} p stylecolor:red⚠️ 超时提交成绩有效/p /c:if本文还有配套的精品资源点击获取