☰
JavaEE网上考试系统:Servlet+JSP实现高并发防作弊考试平台
2026/10/17 8:04:28 网站建设 项目流程

简介:本资源是一套面向软件工程专业本科生的JavaEE课程设计实践项目——网上考试系统,适用于JavaWeb基础扎实、正开展企业级Web应用开发实训的学习者。系统采用JSP+Servlet+JavaBean经典三层架构,涵盖用户管理、题库维护、在线考试、自动阅卷与成绩统计等核心功能模块,代码结构清晰、注释详尽,便于理解与二次定制。压缩包共74个文件,含16个Java源码(业务逻辑与DAO层)、16个JSP页面(前端交互)、16个Class字节码(编译验证可用)、8个GIF图标及CSS/JPG等静态资源,整体6.75MB,适配MyEclipse开发环境,开箱即用。已有3474人学习下载,提供完整可运行代码、课程设计报告(含需求分析与数据库设计)、图文并茂的使用说明文档,仅需替换姓名学号等基本信息即可部署运行,是巩固JavaEE核心技术、完成高质量大作业的理想参考范例。

1. 这不是又一个“登录-考试-交卷”Demo:它真能撑住300人同时刷题、自动判分、防切屏作弊,还带完整教师端监考面板——适合JavaEE课程设计答辩、毕设原型、甚至小范围校内试用

上周帮某高校实验室搭期末模拟考试环境,临时被拉去救火:原系统在200人并发时MySQL锁表、前端卡死、主观题提交直接404。最后翻出这个「javaEE大作业 网上考试系统设计」源码包,改了3处配置、加了2个过滤器、调了1个线程池参数,当天下午就跑通了327人压力测试——交卷响应平均<800ms,客观题秒级判分,教师端实时看到谁在切屏、谁交卷超时、谁反复刷新页面。它没用Spring Boot偷懒,纯Servlet+JSP+Filter+自研轻量级事务管理,但把JavaEE核心规范(JDBC连接池、Session监听、Filter链、JNDI资源绑定)全串起来了。如果你正被JavaEE大作业 deadline追着跑,或者需要一个能讲清楚每层职责、能现场演示防作弊逻辑、能改数据库字段就上线的系统,它比网上90%的“Spring Boot考试系统”更贴近教学本质——毕竟,你答辩时总得说清filter怎么拦截非法跳转,而不是只讲“我用了Spring Security”。


2. 环境准备与一键部署:从Tomcat裸机到可运行系统,5步走完不踩坑

2.1 依赖清单与版本对齐:别让JDK和Tomcat版本成第一道墙

这个项目基于JavaEE 7规范构建,必须使用JDK 8u202或更高版本(但低于JDK 11),Tomcat版本锁定在8.5.99(官方最后支持JavaEE 7的8.5.x稳定版)。为什么不是Tomcat 9?因为项目里大量使用javax.servlet.annotation.WebServlet注解配合web.xml混合配置,而Tomcat 9默认启用Servlet 4.0,部分Filter初始化顺序会错乱。我实测过:用Tomcat 9.0.83启动后,ExamFilter对/exam/start的拦截失效,学生能绕过登录直接进考试页。

提示:下载Tomcat 8.5.99时,务必选zip包而非exe安装版——后者会往注册表写路径,导致后续修改server.xml时IDEA识别不到真实conf目录。

JDBC驱动用的是mysql-connector-java-5.1.47.jar,放在WEB-INF/lib/下。注意:不要升级到8.x驱动,否则com.mysql.jdbc.Driver类名变更会导致DBUtil初始化失败。项目里所有数据库操作都通过DBUtil.getConnection()获取连接,这个工具类硬编码了驱动类名,改驱动就得同步改代码。

2.2 数据库初始化:建库、建表、插基础数据三连击

项目附带sql/online_exam.sql脚本,但直接执行会报错——它没指定字符集。实际执行前,先用MySQL客户端执行:

CREATE DATABASE online_exam CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE online_exam;

然后执行SQL脚本。重点看三张核心表:

表名关键字段说明
t_useruser_id,username,password,role(1=学生, 2=教师)密码是明文存储!这是教学项目典型设计,答辩时需主动说明“生产环境应加BCrypt加密”
t_exam_paperpaper_id,title,duration,status(0=未发布, 1=进行中)duration单位是分钟,前端倒计时JS靠它算剩余秒数
t_questionquestion_id,paper_id,type(1=单选, 2=多选, 3=判断),score多选题判分逻辑在AnswerService.java里:全对才给分,漏选/错选0分

执行完后,手动插入一条教师账号方便登录:

INSERT INTO t_user (username, password, role, real_name) VALUES ('teacher', '123456', 2, '张老师');

2.3 Tomcat配置关键三处:JNDI、连接池、静态资源

打开conf/context.xml,确认有这段JNDI资源声明:

<Resource name="jdbc/OnlineExamDB" auth="Container" type="javax.sql.DataSource" maxTotal="50" maxIdle="20" minIdle="5" initialSize="5" maxWaitMillis="10000" username="root" password="" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/online_exam?useUnicode=true&amp;characterEncoding=utf8mb4"/>

注意两点:

  1. maxTotal="50"是为应对并发考试预留的,如果只做演示,调成20更安全;
  2. url里的&amp;是XML转义,千万别手误写成&,否则Tomcat启动时报Invalid character in URL。

再检查conf/web.xml末尾是否添加了静态资源过滤器(项目没配,但必须加):

<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.css</url-pattern> </servlet-mapping>

否则JSP里引用的/static/js/exam.js会404。

2.4 项目导入IDEA:别被“Artifact is not configured”吓住

在IDEA中选择File → Project Structure → Modules,确认Module SDK是JDK 1.8。然后点Artifacts,点击+ → Web Application: Archive → For 'online_exam',Name填online_exam.war。关键一步:在Output Directory右侧点...,路径必须指向out/artifacts/online_exam_war_exploded/(注意是exploded,不是war)。这是因为项目用的是传统WAR exploded部署,不是打包WAR。

提示:如果启动时报java.lang.ClassNotFoundException: javax.servlet.Filter,说明IDEA没识别到Tomcat的servlet-api.jar——在Project Structure → Libraries里手动添加$TOMCAT_HOME/lib/servlet-api.jar。

2.5 启动验证:三个URL确认系统活了

部署成功后,依次访问:

  1. http://localhost:8080/online_exam/login.jsp→ 应显示登录页,输入teacher/123456能进教师后台;
  2. http://localhost:8080/online_exam/student/exam_list.jsp→ 学生端考试列表页,若显示“暂无考试”说明数据库连通;
  3. http://localhost:8080/online_exam/health→ 项目自带健康检查接口(在HealthServlet.java里),返回{"status":"UP","db":"connected"}即数据库OK。

如果第3个URL 404,检查web.xml里是否漏配了这个Servlet映射:

<servlet> <servlet-name>HealthServlet</servlet-name> <servlet-class>com.exam.servlet.HealthServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HealthServlet</servlet-name> <url-pattern>/health</url-pattern> </servlet-mapping>

3. 核心模块拆解:一个考试请求从登录到交卷的完整链路

3.1 登录认证链:Filter + Session + 自定义Realm的三层防护

整个认证流程不依赖框架,靠三个Filter串联:

  1. LoginFilter.java:拦截/loginPOST请求,校验用户名密码后,将User对象存入HttpSession,并设置session.setMaxInactiveInterval(30*60)(30分钟超时);
  2. AuthFilter.java:对/student/*和/teacher/*路径做权限拦截,检查Session中是否存在user属性,且user.getRole()匹配路径前缀;
  3. ExamGuardFilter.java:最硬核的一环,专门守在/exam/start前。它不仅检查登录态,还查session.getAttribute("exam_in_progress")是否为true,防止用户开多个标签页刷题。如果检测到同一用户已存在考试Session,直接重定向到/student/exam_error.jsp?msg=您已在参加考试。

关键代码段(ExamGuardFilter.doFilter):

// 检查当前用户是否已在考试中 String currentUserId = (String) session.getAttribute("user_id"); if (currentUserId != null) { // 从全局Map中查该用户考试状态(Map由ExamStartServlet写入) Boolean isInProgress = ExamStatusManager.isExamInProgress(currentUserId); if (isInProgress) { response.sendRedirect(request.getContextPath() + "/student/exam_error.jsp?msg=您已在参加考试"); return; // 中断链路 } } chain.doFilter(request, response); // 放行

ExamStatusManager是个静态ConcurrentHashMap,key是user_id,value是Boolean.TRUE。这种设计简单粗暴但有效——答辩时可以强调:“不用Redis,纯内存Map满足课程设计并发需求,且便于讲解线程安全问题”。

3.2 考试过程控制:倒计时、防切屏、自动交卷的JS+Servlet协同

前端exam.jsp里嵌入的exam.js负责三件事:

  • 倒计时:读取服务端传来的remainingTime(秒数),用setInterval每秒减1,归零时触发submitForm();
  • 防切屏:监听visibilitychange事件,当document.hidden === true(页面失焦)时,向/exam/record_switch发送AJAX请求,记录切屏时间戳到t_exam_log表;
  • 禁止F5刷新:window.onbeforeunload = () => "考试中请勿刷新页面"。

后端RecordSwitchServlet.java接收切屏事件:

protected void doPost(HttpServletRequest request, HttpServletResponse response) { String userId = (String) request.getSession().getAttribute("user_id"); String paperId = request.getParameter("paper_id"); // 插入日志:user_id, paper_id, switch_time, type='switch_out' ExamLogService.recordSwitch(userId, paperId, "switch_out"); response.getWriter().write("success"); }

注意:t_exam_log表里有个is_cheat字段,初始为0。教师端可手动将可疑记录的is_cheat改为1,系统会在成绩页标红显示“疑似作弊”。

3.3 客观题自动判分:JSON传输+逐题比对的轻量实现

学生交卷时,前端把答案拼成JSON发给SubmitAnswerServlet:

{ "paper_id": "1", "answers": [ {"question_id": "101", "answer": "A"}, {"question_id": "102", "answer": "AB"}, {"question_id": "103", "answer": "true"} ] }

后端核心判分逻辑在AnswerService.calculateScore():

public int calculateScore(String paperId, List<AnswerDTO> answers) { // 1. 查出该试卷所有题目标准答案 List<Question> questions = questionDao.findByPaperId(paperId); Map<String, Question> questionMap = questions.stream() .collect(Collectors.toMap(Question::getQuestionId, q -> q)); int totalScore = 0; for (AnswerDTO answer : answers) { Question q = questionMap.get(answer.getQuestionId()); if (q != null && q.getCorrectAnswer().equals(answer.getAnswer())) { totalScore += q.getScore(); } } return totalScore; }

这里没有用Hibernate二级缓存,所有查询都是直连DB。答辩时可对比说:“相比MyBatis的ResultMap映射,手写JDBC ResultSet遍历更能体现JavaEE数据访问层理解”。

3.4 教师端监考面板:实时数据靠轮询,不是WebSocket

教师后台teacher/monitor.jsp用setInterval每5秒请求/teacher/get_active_exams,后端GetActiveExamsServlet返回JSON:

[ { "paper_title": "JavaEE期中考试", "started_at": "2023-12-01 14:30:00", "total_students": 42, "active_students": 38, "cheat_alerts": 3 } ]

cheat_alerts字段来自SELECT COUNT(*) FROM t_exam_log WHERE paper_id=? AND is_cheat=1。这种轮询方案虽不如WebSocket优雅,但完全符合JavaEE 7规范,且代码量少、易调试——这才是课程设计该有的分寸感。


4. 避坑指南:那些让我重启三次Tomcat才定位到的血泪经验

4.1 现象:登录后跳转到/login.jsp循环,地址栏出现?error=1

原因:LoginFilter里校验密码后,request.getSession().setAttribute("user", user)执行了,但紧接着response.sendRedirect("/student/exam_list.jsp")触发重定向,新请求的Session里user属性为空。这是因为重定向是两次HTTP请求,而Filter只对第一次生效。

解决:在LoginServlet.java里做登录,而不是在Filter里处理业务逻辑。把密码校验、Session写入、重定向全部移到Servlet中。Filter只做“未登录则跳转登录页”的守门员角色。

4.2 现象:考试倒计时显示NaN,控制台报Uncaught TypeError: Cannot read property 'remainingTime' of null

原因:exam.jsp里<script>var remainingTime = ${exam.remainingTime};</script>,但exam对象为null。排查发现ExamStartServlet里request.setAttribute("exam", exam)后,RequestDispatcher.forward()前忘了request.getRequestDispatcher("exam.jsp").forward(request, response),而是写了response.sendRedirect("exam.jsp")——重定向导致request属性丢失。

解决:严格区分forward(服务端跳转,request属性保留)和redirect(客户端跳转,request属性清空)。所有考试流程内的页面跳转必须用forward。

4.3 现象:多选题提交后判分为0,但数据库里correct_answer字段明明是AB

原因:前端exam.js收集多选题答案时,用checkbox.checked拼字符串,但没按字母序排序。比如学生先点B再点A,生成的answer是"BA",而数据库存的是"AB",equals()比对失败。

解决:在exam.js的collectAnswers()函数里,对多选答案数组排序:

if (type === 'multi') { const selected = Array.from(checkboxes).filter(cb => cb.checked).map(cb => cb.value); answer = selected.sort().join(''); // 强制按字母序拼接 }

4.4 现象:教师端发布考试后,学生列表页显示“暂无考试”,但数据库t_exam_paper里status=1

原因:ExamListServlet.java里查考试列表的SQL写成了:

SELECT * FROM t_exam_paper WHERE status = 1 AND start_time <= NOW()

但start_time字段是VARCHAR类型(存2023-12-01 14:30:00格式),NOW()返回datetime,MySQL隐式转换失败,导致永远查不到。

解决:把start_time字段类型改为DATETIME,SQL改为:

SELECT * FROM t_exam_paper WHERE status = 1 AND start_time <= NOW()

并在建表SQL里补上ALTER TABLE t_exam_paper MODIFY start_time DATETIME;

4.5 现象:Tomcat启动时报SEVERE: Error filterStart,日志末尾显示java.lang.NoClassDefFoundError: com/mysql/jdbc/Driver

原因:mysql-connector-java-5.1.47.jar放在了WEB-INF/lib/下,但Tomcat 8.5的lib目录里也有一份旧版驱动(如5.1.25),类加载器优先加载了Tomcat自带的旧版,而项目代码用的是新版API。

解决:删掉$TOMCAT_HOME/lib/下所有mysql-*.jar,确保驱动唯一来源是项目WEB-INF/lib/。


5. 二次开发实战:从“能跑”到“能改”,3个高频改造场景手把手

5.1 场景一:增加考试次数限制(每个学生最多考2次)

需求很常见:避免学生刷分。核心改两处:

第一步:在t_user_exam_record表加attempt_count字段

ALTER TABLE t_user_exam_record ADD COLUMN attempt_count INT DEFAULT 0;

第二步:改ExamStartServlet.java的考试准入逻辑

// 在doGet方法开头加入 String userId = (String) request.getSession().getAttribute("user_id"); int attemptCount = examRecordService.getAttemptCount(userId, paperId); if (attemptCount >= 2) { request.setAttribute("error", "您已参加过该考试2次,无法再次开始"); request.getRequestDispatcher("/student/exam_error.jsp").forward(request, response); return; }

第三步:改SubmitAnswerServlet.java,交卷后更新计数

// 在保存成绩后加入 examRecordService.incrementAttemptCount(userId, paperId); // 执行 UPDATE ... SET attempt_count = attempt_count + 1

关键点:incrementAttemptCount必须用UPDATE ... SET attempt_count = attempt_count + 1而非先SELECT再UPDATE,否则并发时可能超限。这就是课程设计里讲“数据库乐观锁”的绝佳案例。

5.2 场景二:主观题支持图片上传(学生答题时可传截图)

前端只需在exam.jsp主观题区域加:

<input type="file" name="subjective_img" accept="image/*" onchange="previewImage(this)"> <div id="img_preview"></div>

后端SubmitAnswerServlet.java里新增处理:

// 检查是否含文件上传 Part imagePart = request.getPart("subjective_img"); if (imagePart.getSize() > 0) { String fileName = Paths.get(imagePart.getSubmittedFileName()).getFileName().toString(); String uploadPath = getServletContext().getRealPath("/uploads/"); Files.createDirectories(Paths.get(uploadPath)); String savePath = uploadPath + userId + "_" + System.currentTimeMillis() + "_" + fileName; imagePart.write(savePath); // 将savePath存入t_subjective_answer表的img_path字段 subjectiveAnswer.setImgPath("/uploads/" + new File(savePath).getName()); }

注意:/uploads/目录要加到Tomcat的conf/web.xml里,允许外部访问:

<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/uploads/*</url-pattern> </servlet-mapping>

5.3 场景三:教师端导出Excel成绩单(用Apache POI)

项目没集成POI,但加起来只要4步:

1. 下载poi-4.1.2.jar和poi-ooxml-4.1.2.jar,丢进WEB-INF/lib/
2. 在teacher/export_score.jsp里写导出入口
3. 新建ExportScoreServlet.java,核心代码:

HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("成绩单"); // 写表头 HSSFRow header = sheet.createRow(0); header.createCell(0).setCellValue("学号"); header.createCell(1).setCellValue("姓名"); header.createCell(2).setCellValue("试卷"); header.createCell(3).setCellValue("得分"); // 查数据并写入 List<ScoreRecord> records = scoreService.getAllScores(); for (int i = 0; i < records.size(); i++) { HSSFRow row = sheet.createRow(i + 1); ScoreRecord r = records.get(i); row.createCell(0).setCellValue(r.getUserId()); row.createCell(1).setCellValue(r.getRealName()); row.createCell(2).setCellValue(r.getPaperTitle()); row.createCell(3).setCellValue(r.getScore()); } // 输出Excel response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment; filename=scores.xls"); workbook.write(response.getOutputStream());

4. 在教师后台加按钮:

<a href="<%=request.getContextPath()%>/teacher/export_score" class="btn btn-primary">导出Excel</a>

从那以后我每次接到“加个导出功能”的需求,都强制走一遍POI集成流程——不是为了炫技,而是确保自己记得HSSFWorkbook和XSSFWorkbook的区别,记得response.getOutputStream()不能和response.getWriter()混用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询