资讯动态

网上考试系统实战:从三层架构到答辩部署

发布时间:2026/10/21 21:01:42 来源:尧图企业网站定制
简介这是一份面向Java EE课程设计或大作业场景的网上考试系统完整项目包主要面向软件工程及相关专业学生解决毕业设计、课程考核中需要“可运行系统配套文档”的痛点。项目基于MyEclipse工程结构开发涵盖16个Java业务类、16个JSP页面及配套class编译文件同时包含CSS、GIF/JPG等页面资源和classpath、project等工程配置结构符合经典三层模式。另附设计报告和使用说明文档doc格式使用说明给出了环境配置、数据库导入和部署运行步骤能帮助非原作者快速接手。代码注释详细、难度适中只需修改姓名、学号等个人信息即可作为自己的作业提交。压缩包共74个文件整体仅6.75MB目录清晰便于导入IDE直接运行和二次扩展。目前已有3475人学习浏览完成度与实用性得到多位下载者认可。若你需要一份能快速落地且文档齐全的网上考试系统参考这套资料能省去大量从零搭建的时间。1. 网上考试系统为什么值得认真做从“能跑就行”到“答辩能站住脚”每年到期末时段总能看到一批人拿「网上考试系统」当 javaEE 大作业的选题。这个题目看起来满大街都是但真动手写的时候登录 Session 失效、中文乱码、试题和考试数据对不上、评分规则一塌糊涂的情况几乎每个做这个题的人都会撞几个。这份资源能把这一整套链路串起来从代码到设计报告到使用说明直接覆盖答辩前你要面对的所有环节。适合正在赶大作业、但不想交一份只有增删改查的“玩具”的人也适合想把三层结构、Session 机制、JDBC 封装一次讲清楚的人。读完这一篇你至少能把系统的骨架立住再遇到问题也知道该往哪个方向查。2. 技术栈与架构JSP/Servlet/JDBC 下手写一套三层结构的考试系统2.1 目录规划与服务拆分别把三层结构写成一坨选题是 javaEE 大作业最稳妥的技术栈还是 JSP Servlet JDBC而不是一上来就引 Spring Boot。原因是很多学校的课程进度就停在 Servlet 和 JSP答辩时老师问“你这个项目用了什么框架”你要是说 Spring Boot 却没有真正讲清楚 IOC 和 AOP 的原理反而容易被追问到底。用原生 Servlet 手写一套三层结构代码量可控逻辑透明老师问每一层的作用你都能答上来。常见的做法是把项目拆成这样的包结构src/main/java ├── com.exam.dao // 数据访问层封装 JDBC 操作 ├── com.exam.domain // 实体类Student、Teacher、Subject、Paper、Record ├── com.exam.service // 业务逻辑层登录校验、随机组卷、阅卷评分 ├── com.exam.servlet // 控制层接收请求、调用 Service、跳转 JSP ├── com.exam.util // 工具类DBUtil、StringUtil、DateUtil └── com.exam.filter // 过滤器编码处理、登录拦截 webapp ├── admin // 管理员页面题库管理、考试管理 ├── student // 学生页面在线考试、成绩查询 ├── teacher // 教师页面手动阅卷、成绩统计 ├── WEB-INF │ ├── web.xml │ └── views // JSP 目录这个结构不是随便分的。Dao 只做 SQL 和结果集映射Service 做事务和业务规则Servlet 只解析参数和跳转页面。这样分的好处是如果你后面想换框架Dao 层改成 MyBatis 接口Service 层几乎不用动。2.2 数据库表设计五张表覆盖登录、试题、考试、成绩全流程网上考试系统最少需要五张表学生表、教师表、试题表、考试记录表、答题明细表。当然你可以加管理员表但很多大作业把管理员和教师复用同一张表角色字段区分就行。CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, class_name VARCHAR(50) ); CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(50) NOT NULL, role TINYINT DEFAULT 1 -- 1 教师2 管理员 ); CREATE TABLE subject ( id INT PRIMARY KEY AUTO_INCREMENT, question_type TINYINT NOT NULL, -- 1 单选2 多选3 判断 question_text TEXT NOT NULL, option_a VARCHAR(255), option_b VARCHAR(255), option_c VARCHAR(255), option_d VARCHAR(255), correct_answer VARCHAR(255) NOT NULL, score INT DEFAULT 5, difficulty TINYINT DEFAULT 1, -- 1 简单2 中等3 困难 create_time DATETIME ); CREATE TABLE exam_paper ( id INT PRIMARY KEY AUTO_INCREMENT, paper_name VARCHAR(100) NOT NULL, teacher_id INT NOT NULL, total_score INT DEFAULT 100, create_time DATETIME ); CREATE TABLE exam_record ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, paper_id INT NOT NULL, score DECIMAL(5,1), start_time DATETIME, end_time DATETIME, status TINYINT DEFAULT 0 -- 0 考试中1 已交卷 );注意几个字段的设计逻辑。subject表里必须有question_type和difficulty否则后续随机抽题和自动判分逻辑没法做。exam_record里必须有start_time和end_time这是后面做考试时间控制和判断是否超时交卷的依据。score用 DECIMAL(5,1) 而不是 INT因为后续可能有 0.5 分的题。我之前见过不少同学用 INT 存成绩遇到半分的题只能四舍五入答辩时被老师拿数据一验就露馅。2.3 数据源与 JDBC 工具类连接池前置脱离 DriverManager大作业里最常见的 JDBC 写法是先Class.forName再DriverManager.getConnection。这么做在小规模数据量下没什么大问题但遇到一个页面反复查数据库的情况每次请求都新建连接Tomcat 会报连接数太多的错。比较明智的做法是用连接池。我一般习惯用 dbcp 或 c3p0二选一即可配置方式几乎一样。先在WEB-INF/web.xml里配置数据源然后写一个 DBUtil 封装获取连接和释放资源的逻辑Resource namejdbc/examDB authContainer typejavax.sql.DataSource driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 maxActive20 maxIdle5 maxWait10000 /public class DBUtil { public static Connection getConnection() throws SQLException { try { Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/examDB); return ds.getConnection(); } catch (NamingException e) { throw new SQLException(数据源查找失败, e); } } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) {} } if (ps ! null) { try { ps.close(); } catch (SQLException e) {} } if (conn ! null) { try { conn.close(); } catch (SQLException e) {} } } }代码里的dbcp连接池由 Tomcat 容器托管maxActive20表示最多同时活跃 20 个连接maxWait10000表示连接池满时最多等 10 秒超过就抛异常。连接池的意义在于连接是复用的而不是每次创建新连接Ping 高并发场景下的稳定性会好很多。3. 核心模块实现登录鉴权、自动阅卷与随机抽题3.1 登录与角色分流Session Filter 拦截器的实现网上考试系统第一个要被老师问的地方就是“你如何区分三种角色、如何防止未登录直接访问某个页面”。如果只在 Servlet 里判断 Session 是否为空那几个页面直接输入 URL 就能绕过登录。加一个 Filter 统一处理是最简单的方案。WebFilter(/*) public class LoginFilter 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); String uri request.getRequestURI(); if (uri.endsWith(login.jsp) || uri.endsWith(LoginServlet) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } String role (String) session.getAttribute(role); if (uri.contains(/admin/) !admin.equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (uri.contains(/student/) !student.equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); } }这段 Filter 的逻辑是核心先放行登录相关的 URL 和静态资源再检查 Session 里有没有用户信息没有就重定向到登录页然后根据 URI 里的目录段判断角色是否匹配。request.getSession(false)的作用是如果当前没有 Session不主动创建一个新的避免每次请求都往服务器塞垃圾 Session。这里有一个细节把用户角色存在 Session 里而不是每次查数据库能减少很多无谓的查询。在实际大作业里我会让登录后的用户对象存到 Session 的user属性里角色单独存一份role属性。因为后续很多地方只需要判断角色完全没必要把整个用户对象取出来。3.2 题库管理与随机组卷避免前后端同卷的防作弊逻辑考试系统里最有技术含量的部分不是增删改查而是随机组卷和自动阅卷。随机组卷的常规做法是从题库中按题型和难度随机抽取指定数量的题组成一张试卷保存为一份试卷快照。这样做的目的不是每个学生拿到的题目都不同而是题目顺序不同且选项顺序可能不相同。这能在一定程度上防止相邻考生互抄。public ListSubject generatePaper(int easyCount, int mediumCount, int hardCount) { ListSubject paper new ArrayList(); paper.addAll(randomPickByDifficulty(1, easyCount)); paper.addAll(randomPickByDifficulty(2, mediumCount)); paper.addAll(randomPickByDifficulty(3, hardCount)); Collections.shuffle(paper); return paper; } private ListSubject randomPickByDifficulty(int difficulty, int count) { String sql SELECT * FROM subject WHERE difficulty ? AND question_type ? ORDER BY RAND() LIMIT ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, difficulty); ps.setInt(2, 1); // 只选单选题多选和判断另一套逻辑处理 ps.setInt(3, count); try (ResultSet rs ps.executeQuery()) { ListSubject list new ArrayList(); while (rs.next()) { list.add(mapToSubject(rs)); } return list; } } catch (SQLException e) { throw new RuntimeException(随机抽题失败, e); } }ORDER BY RAND()在大数据量下性能不太好但大作业的数据量一般也就几百道题完全够用。如果老师问性能问题你可以说“对于万级以下的数据量RAND()的随机性足够且实现简单数据量再大的话可以考虑先查 id 列表再IN查询”。这样回答比硬扛“我用的是最好的方案”要诚恳得多。组卷之后要把试卷内容固化下来不能每次进入考试都重新抽题否则学生刷新页面题目就变了。需要一张“试卷快照表”把这次考试抽中的题号和分值存起来学生端从快照里读取题目交卷后也按快照判分。3.3 自动阅卷核心算法字符串匹配与评分规则自动阅卷对于单选题、判断题来说是最容易实现的核心就是比对correct_answer和学生提交的答案。但这里有个很容易翻车的地方多选和判断题的答案存储格式不统一。如果单选存“A”多选存“ABD”判断存“对/错”那么判分逻辑就要针对每个题型单独处理。public double gradeAnswer(Subject subject, String studentAnswer) { if (subject.getQuestionType() 1) { // 单选题完全匹配才得分 return subject.getCorrectAnswer().equalsIgnoreCase(studentAnswer.trim()) ? subject.getScore() : 0; } if (subject.getQuestionType() 2) { // 多选题全对才给满分少选不给分或按比例给分看需求 String correct sortAnswer(subject.getCorrectAnswer()); String student sortAnswer(studentAnswer); return correct.equals(student) ? subject.getScore() : 0; } if (subject.getQuestionType() 3) { // 判断题对 - true错 - false可统一转成 true/false 再比 boolean correct 正确.equals(subject.getCorrectAnswer()); boolean student 正确.equals(studentAnswer); return correct student ? subject.getScore() : 0; } return 0; } private String sortAnswer(String answer) { char[] chars answer.toUpperCase().replaceAll(\\s, ).toCharArray(); Arrays.sort(chars); return new String(chars); }这段代码的关键是sortAnswer它把“ABD”和“BDA”视为同一个答案避免因为选项顺序不同误判。另一个值得注意的细节是学生提交的答案要先做空值和空白字符处理否则空字符串和 null 会导致equalsIgnoreCase抛异常。自动阅卷只能处理客观题。简答题和论述题不在自动判分范围内但系统要能记录学生答案并把“待人工阅卷”的题目标出来。教师端做人工阅卷时界面显示学生原文和标准答案教师输入分数后保存。3.4 考试成绩统计不只是汇总还要能出报表成绩统计也是被老师高频追问的部分。常见的需求是按班级统计平均分、及格率、最高分、最低分以及按题目统计正确率用来分析哪些题太难或太简单。SELECT c.class_name, COUNT(DISTINCT e.student_id) AS total_students, ROUND(AVG(e.score), 1) AS avg_score, SUM(CASE WHEN e.score 60 THEN 1 ELSE 0 END) AS pass_count, ROUND(SUM(CASE WHEN e.score 60 THEN 1 ELSE 0 END) / COUNT(DISTINCT e.student_id) * 100, 1) AS pass_rate, MAX(e.score) AS max_score, MIN(e.score) AS min_score FROM exam_record e JOIN student c ON e.student_id c.id JOIN exam_paper p ON e.paper_id p.id WHERE e.status 1 AND p.id ? GROUP BY c.class_name;这个 SQL 的统计口径在答辩时一定要能解释清楚pass_count是及格人数pass_rate是及格率total_students是考试的参考人数而不是班级总人数因为可能有人没参加考试。老师经常会问“你没考的人怎么算”回答“只统计参考学生的及格率未参考学生在另一张出勤表里体现”就行。做这张成绩统计表时最好在页面端把平均分、最高分、最低分用一张柱状图展示出来不用什么高端图表库简单用 JFreeChart 或 ECharts 都行。有了图报告和答辩 PPT 都会好看很多。4. 避坑手册乱码、脏数据、时间格式与连接泄漏4.1 中文乱码POST 与 GET 两条路径的编码设置现象存进数据库的中文显示成问号或者 JSP 页面显示乱码。原因GET 请求的乱码出在 Tomcat 默认的 ISO-8859-1 解码上POST 请求的乱码出在request.setCharacterEncoding没有在取参数前调用。很多人只处理了 POST没有处理 GET。解决Connector port8080 protocolHTTP/1.1 URIEncodingUTF-8 /在server.xml的 Connector 上加URIEncodingUTF-8解决 GET 请求。对于 POST在 Filter 里加request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8);这个设置必须在request.getParameter()之前执行否则等于白写。我见过很多同学把setCharacterEncoding写在 Servlet 中段、参数已经取完之后结果还是一堆问号。最保险的做法是把编码设置放在 Filter 的doFilter方法最前面加上一个判断只对POST和PUT请求设置避免重复设置报警告。4.2 连接泄漏finally 里没关就是事故现象系统跑着跑着某一天突然变慢最后报Connection is not available, request timed out。原因某个 DAO 方法执行完 SQL 后没有关闭连接连接池里的连接被耗尽。更隐蔽的情况是异常抛出后后续的关闭代码根本没机会执行。解决用 try-with-resources 替代传统的 finally 写法。try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { // 业务处理 } } catch (SQLException e) { throw new RuntimeException(查询失败, e); }这里要解释的是 try-with-resources 的作用范围Connection、PreparedStatement、ResultSet必须都实现了AutoCloseable接口Java 会自动按逆序关闭它们即使中途抛异常也会执行关闭。如果你用的是 Java 7 以下版本或者项目环境不支持那必须在 finally 里挨个判空关闭并且关闭顺序不能乱先关ResultSet再关PreparedStatement最后关Connection顺序反了可能把连接池搞出半开连接的状态。4.3 考试时间控制JS 倒计时不可信后端必须校验现象前端用 JavaScript 做了 30 分钟倒计时学生把系统时间改成 5 分钟前考试时间无限延长。原因JS 倒计时和服务器时间没有任何绑定关系它只是本地计时器。只要客户端不是服务器代码就无法信任浏览器时钟。解决考试开始时后端把start_time写入exam_record表。每次学生提交答案时后端比对当前服务器时间与start_time超过规定时长直接拒绝收卷或标记超时。public boolean checkExamTimeout(int recordId, int limitMinutes) { String sql SELECT start_time FROM exam_record WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, recordId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Timestamp start rs.getTimestamp(start_time); long elapsed System.currentTimeMillis() - start.getTime(); return elapsed limitMinutes * 60 * 1000L; } return false; } } catch (SQLException e) { throw new RuntimeException(检查超时失败, e); } }这个方法的本质是“以服务器时间戳为准”。前端倒计时只是用户体验后端checkExamTimeout才是真正的哨兵。写的时候注意limitMinutes * 60 * 1000L这个乘法一定要带L否则limitMinutes * 60 * 1000在 int 范围内可能溢出时间长了会得到负数超时判断直接失效。4.4 脏数据问题删除试题前先查关联表现象管理员删除了一道已经出现在某张试卷里的试题结果学生端那套历史试卷的题目显示不全或者判分时空指针报错。原因题库表subject和试卷快照之间存在关联但没有外键约束删除主表记录后关联表里的题目引用悬空。解决删除前先查关联表中是否存在引用。public boolean checkSubjectInUse(int subjectId) { String sql SELECT COUNT(*) FROM paper_question WHERE subject_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, subjectId); try (ResultSet rs ps.executeQuery()) { rs.next(); return rs.getInt(1) 0; } } catch (SQLException e) { throw new RuntimeException(检查试题引用失败, e); } }这个问题的本质是“已发布的试卷需要保留历史快照不能因为题库维护影响历史考试记录”。在答辩时能主动说出这个逻辑说明你是真做过系统设计而不是写完作业就扔。5. 部署与验收从代码到答辩的全流程5.1 Tomcat 部署与数据库初始化一次部署成功的三个细节很多人的代码在 IDE 里能跑但导出 WAR 包部署到独立的 Tomcat 就各种问题。这里说三个我踩过的坑。第一JDK 版本和 Tomcat 版本必须匹配。Tomcat 9 对应 Java 8 及以上Tomcat 10 对应 Java 11 及以上而且 Tomcat 10 把包名从javax.servlet改成了jakarta.servlet你要是项目里用的是javax包名部署到 Tomcat 10 会直接 404连报错都看不出来。第二MySQL 驱动 JAR 包放在WEB-INF/lib里不要在 IDE 里能连上就万事大吉。Tomcat 独立部署时类加载机制和 IDE 不同驱动放错位置会报ClassNotFoundException。正确的做法是把 mysql-connector-java JAR 放入WEB-INF/lib如果是连接池资源驱动 JAR 放 Tomcat 的lib目录也可以但大作业放在WEB-INF/lib更直观。第三web.xml里的数据源配置依赖 Tomcat 的 context 配置。如果你用的是WebServlet注解还要检查metadata-complete属性如果web.xml设置为metadata-completetrue注解会被忽略Servlet 全部失效访问任何地址都是 404。这个问题非常隐蔽因为 IDE 内嵌的 Tomcat 有时会宽容处理正式部署就严格了。部署完成后用一个简单的巡检清单确认系统健康登录页能否打开、学生登录后能否看到考试列表、创建考试后题目能否正常抽取、交卷后成绩是否正常写入。5.2 毕业设计报告怎么写按评审老师的评审顺序来组织这份资源里附带的报告我建议拿到手后不要直接交先改三处。第一把报告里的项目名称改成你自己的所有代码截图换掉因为代码细节往往一眼就能看出来是不是同一个人的。第二把“系统实现”这一章的截图换成一个你没提交过的功能页面最好换一个老师没见过的视角。第三检查数据表字段命名是否和你真正运行的数据一致有些模板里会有预留字段不改就是漏洞。报告的组织顺序建议是摘要300 字内写清楚系统实现了什么、用了什么技术、达到什么效果→ 目录 → 需求分析功能需求 角色分析→ 系统设计架构图 数据库 ER 图 表结构说明→ 系统实现核心模块代码和截图每个模块先写逻辑再贴关键代码→ 系统测试功能测试 用例表→ 总结与展望。重点放在“系统设计”和“系统实现”两章这两章占全篇 50% 以上的篇幅。数据库 ER 图最好用工具画标准一些评审老师看图的频率比文字高。还有一个细节报告里每一张表都要写清楚字段含义和约束不要只贴 SQL 建表语句否则显得像是从网上直接拷的。5.3 答辩高频问题与应对被问住的情况怎么处理答辩时老师最喜欢问的几类问题提前准备好就不会慌。第一类是“为什么选 JSP/Servlet 而不用框架”。回答思路课程大纲覆盖的就是 Servlet 和 JSP用原生技术能更好地理解 HTTP 请求处理流程、Session 机制和 MVC 思想框架是对这些的封装基础打好了未来再学框架更容易。第二类是“你的系统如何保证同一道题不会重复出现两次”。回答随机抽题时记录本次已抽取的题号集合每次抽题前判断是否已存在如果题库数量不足以支撑抽题页面会提示题目不足。第三类是“学生提交的答案和标准答案比对如何避免空格和大小写引起的误判”。回答判分前统一做 trim 和 toUpperCase 处理多选题排序后比对。如果被问到一个真答不上来的问题不要硬编诚恳说“这个问题我目前的理解是…后面我会再查资料深入一下”比强行解释要稳得多。6. 进阶改进登录安全、压测验证与代码评审习惯这套系统跑通容易但离“能演示得好”还有几步可走。我建议在交作业前做两个小改进提升质感很明显。第一个改进是登录密码的存储方式。很多大作业直接明文存密码老师如果查看数据库会扣分。最轻量的做法是 MD5 加盐不用引额外的库就用 JDK 自带的MessageDigest。加盐的意思是每个用户单独生成一个随机字符串和密码拼在一起再算摘要数据库里存“盐值 摘要值”登录时重新拼接比对。这个工作量很小但答辩时能体现你对常见 Web 安全问题的了解。第二个改进是做一个简单的压测验证。不需要引 JMeter 那么复杂写一段多线程并发请求的简单逻辑模拟 50 个学生同时交卷看系统的响应时间和是否有连接异常。这个验证过程放进报告“系统测试”章节是很加分的点因为很多人的测试只有“我试了试能用”而你能说出并发场景下的表现数据。代码写完后我还建议做一次自查重点看几个位置所有 DAO 方法是否都用了 try-with-resourcesSession 里是否只放了必要的对象数据库连接池配置是否落地在web.xml而不是只写在 IDE 的配置里。每次帮人排查一个考勤系统或者作业项目最后翻车的地方都出在这些看起来不起眼的工程习惯上。从那以后我自己写 JavaEE 大作业类的代码都强制要先走一遍资源里的使用说明再动代码先把部署环境踩平了再改功能。希望这份笔记能帮你在期末周少熬几宿也希望你交出去的代码和报告经得起答辩现场多问几轮。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑