简介:这是一套基于Java Web技术栈开发的在线考试系统完整源码,面向Java初学者与Web开发入门者,适用于课程设计、毕业设计或小型教学平台搭建场景。系统采用SSM(Spring+SpringMVC+MyBatis)主流框架,支持管理员、教师、学生三类角色协同操作,涵盖试题管理、在线考试、成绩统计与评价反馈等核心功能模块。资源包共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 搭建考试系统:为什么选 Servlet+JSP 而不是 Spring Boot?
2.1 选型依据:教学场景下,轻量可控比自动配置更重要
在线考试系统的核心业务链路非常明确:用户登录 → 加载试卷 → 答题提交 → 自动判分 → 成绩入库。这条链路上,每个环节都需要显式控制请求流转、会话状态和数据库事务。Spring Boot 虽然能快速启动,但其@Controller+@Service+@Repository的三层抽象,对初学者而言容易掩盖关键细节:比如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-name>driver</param-name> <param-value>com.mysql.cj.jdbc.Driver</param-value> </context-param> <context-param> <param-name>url</param-name> <param-value>jdbc:mysql://localhost:3306/examdb?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8</param-value> </context-param> <context-param> <param-name>username</param-name> <param-value>root</param-value> </context-param> <context-param> <param-name>password</param-name> <param-value>123456</param-value> </context-param> <!-- 注册 LoginServlet --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>这段配置决定了:当浏览器访问http://localhost:8080/OnlineExamSystem/login时,Tomcat 会实例化LoginServlet并调用其doPost()方法。注意&是 XML 实体转义,实际 URL 中应为&;serverTimezone=Asia/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"、"ACD";exam_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-set=utf8mb4,否则中文题目内容可能乱码。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 name="jdbc/ExamDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/examdb?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4" username="root" password="123456" maxActive="20" minIdle="5" maxWait="10000" validationQuery="SELECT 1" testOnBorrow="true"/>随后修改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> <description>Exam Database Connection</description> <res-ref-name>jdbc/ExamDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>提示:若跳过此步,直接用
DriverManager也可运行,但高并发下易出现Too many connections错误。maxActive="20"表示最多 20 个活跃连接,足够应付 50 人以内同时考试。
3.3 Eclipse/IntelliJ 导入与部署:关键三步校验
步骤 1:确认项目编码为 UTF-8
右键项目 → Properties → Resource → Text file encoding → 选择UTF-8。否则 JSP 中中文显示为??。
步骤 2:配置 Tomcat Runtime
Window → 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.class;- Tomcat 控制台是否有
SEVERE: Error filterStart—— 通常因WEB-INF/lib/缺少jstl.jar或standard.jar。
4. 关键业务逻辑实现:答题提交与自动判分的完整链路
4.1 提交动作拆解:从前端表单到后端事务控制
考试页exam.jsp中,每道题生成独立<form>,提交至SubmitAnswerServlet:
<!-- exam.jsp 片段 --> <c:forEach items="${questions}" var="q" varStatus="status"> <div class="question"> <p>${status.count}. ${q.content}</p> <c:if test="${q.type == 1}"> <input type="radio" name="answer_${q.id}" value="A"> ${q.option_a}<br> <input type="radio" name="answer_${q.id}" value="B"> ${q.option_b}<br> <!-- ... --> </c:if> <c:if test="${q.type == 3}"> <input type="checkbox" name="answer_${q.id}" value="T"> 正确 <input type="checkbox" name="answer_${q.id}" value="F"> 错误 </c:if> </div> </c:forEach> <button type="button" onclick="submitAll()">提交试卷</button> <script> function submitAll() { const forms = document.querySelectorAll('form'); forms.forEach(form => form.submit()); // 逐题提交(简化版,实际应 AJAX 批量) } </script>SubmitAnswerServlet.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_* 参数 Enumeration<String> 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"而标准答案为"A");DBUtil.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/login | No servlet mapping found for /login | web.xml中<servlet-mapping>的<url-pattern>与表单action不一致 | 检查web.xml的<url-pattern>是否为/login,且action中无多余路径如/OnlineExamSystem/login |
java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver | Caused by: java.lang.ClassNotFoundException | WEB-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 unrecognized | java.sql.SQLException: The server time zone value 'UTC' | MySQL URL 缺少serverTimezone参数 | 修改web.xml中url参数,追加&serverTimezone=Asia/Shanghai |
HTTP Status 500 – Cannot create JDBC driver of class '' | org.apache.tomcat.dbcp.dbcp.SQLNestedException: Cannot create JDBC driver | context.xml中driverClassName拼写错误(如com.mysql.jdbc.Driver旧版写法) | 改为com.mysql.cj.jdbc.Driver(MySQL 5.7+ 必须用cj) |
Duplicate entry '1-5' for key 'uk_user_question' | java.sql.SQLIntegrityConstraintViolationException | 同一考生重复提交同一题 | 前端禁用提交按钮onclick="this.disabled=true;",后端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 style="color:red">⚠️ 超时提交,成绩有效</p> </c:if>本文还有配套的精品资源,点击获取