ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Web在线考试系统实战:数据库设计与Servlet+JSP核心实现

Java Web在线考试系统实战:数据库设计与Servlet+JSP核心实现

简介:一套基于Java Web的在线考试系统完整项目包,面向计算机相关专业学生、Java初学者及需要课程设计或毕业设计参考的开发者。系统内置管理员、教师、学生三类角色,涵盖题库管理、组卷、在线答题、自动评分、成绩查看等典型环节,适合用来理解Java Web分层开发与前后端交互思路。包体共566个文件,以Java源码、class编译文件、XML配置为主,辅以JS、HTML、CSS、SCSS等前端资源,以及gif演示图片和SQL数据库脚本,整体仅3.2MB,轻量易部署。资源描述中给出了可直接使用的管理员、教师、学生账号,配合IDEA+MySQL+JDK+Tomcat环境即可运行。目前已有903人浏览学习,对有在线考试系统开发需求的人群有直接参考价值,可直接借鉴其数据库设计、权限控制、页面交互与代码结构,减少从零搭建的时间成本。

1. 评判一个Java web在线考试系统能不能用,先看它的数据库脚本,而不是先看代码

很多同学拿到“基于Java web的在线考试系统(源码+数据库).zip”这类资源包,第一反应是解压、找《使用说明》、导入IDE、点运行。真正做过课程设计或毕业设计的人都知道:百分之八十的失败发生在数据库连不上、表单提交乱码、刷新页面成绩翻倍,而不是代码本身写得有多复杂。这个zip包背后是一个标准的学生、教师、管理员三端业务闭环:管理员维护基础数据,教师管理题库和发布考试,学生登录后参加随机组卷的考试并立即看到客观题成绩。它适合正在准备Java Web课程设计或毕业设计的在校生,也适合想靠“传统Servlet+JSP项目”梳理前后端交互逻辑、为后续Spring框架打底子的初级开发者。本文不猜包里的源码长什么样,只按这个题目最常规的实现方案,把它的角色权限、数据库表结构、环境配置、核心代码和最容易翻车的边界条件全部拆开讲清楚,你拿到类似的包也能照样落地。

2. 系统拆解与技术选型:这门课为什么还在用Servlet+JSP

2.1 三种角色与一次完整机考的业务闭环

在线考试系统虽然叫法各异,但核心角色始终是三个:学生、教师、管理员。解压zip包后看到的Java web项目,九成以上会按这三类账号做页面与Servlet的划分。

角色核心功能对应页面与入口
管理员用户管理(增删改查学生/教师账号)、科目管理、考试状态启停、数据统计admin/ 目录下的管理页,如 user_list.jsp、exam_list.jsp
教师题库管理(单选/多选/判断/填空)、创建试卷、设置考试时长与总分、查看学生成绩列表teacher/ 目录下的 question_add.jsp、paper_create.jsp
学生登录后查看可参加的考试、在线答题、交卷后查看客观题得分与正确答案student/ 目录下的 exam_list.jsp、exam_paper.jsp

一次完整机考的业务闭环是这样的:管理员先启动一场考试,教师把已经录入题库的题目按题型和数量抽取成一份试卷,学生在有效时间段内进入答题页,交卷后后台按标准答案自动判分并把成绩写入成绩表,学生能在个人中心查到自己各科的历史成绩。这个闭环能跑通,意味着这个zip包在答辩现场站得住;跑不通,基本就卡在下文第4章的环境配置和第5章的Session与编码处理上。

2.2 技术选型为什么是JSP+Servlet+MySQL,而不是Spring Boot

拿到标题带着“Java web”的项目,你要有个预期:它大概率是JSP+Servlet+JDBC+MySQL这套经典组合,只有少量项目会用SSM或Spring Boot重写。原因很现实——课程设计与毕业设计考察的是你对HTTP请求、Servlet生命周期、Session状态管理、JDBC连接数据库这些底层概念的掌握程度。用Spring Boot三分钟搭一个Controller出来,答辩时老师问“请求从浏览器到数据库经过哪些环节”,你很难把框架自动完成的步骤讲清楚。而JSP+Servlet方案里,每个登录请求都要自己写doPost、自己从request里取参数、自己new Connection,反而更容易让新手建立完整的Web开发心智模型。

从部署角度看,Servlet+JSP项目依赖很少,不需要Maven拉一堆starter依赖,也不用配置Spring容器。传统的项目结构就是src放Java源码、WebContent放JSP页面和WEB-INF/web.xml,压缩包解压后直接导入IDE,配置一个Tomcat就能跑。这和“标题自带. zip源码+数据库”的资源分发方式完全匹配。我的建议是:除非你已经有稳定的Maven私服依赖下载环境,否则拿到这类项目不要着急把它迁移成Spring Boot,先把原始结构跑通,理解每一行请求处理逻辑,迁移是后面的事。

版本搭配上,最稳的组合是JDK 1.8 + Tomcat 8.5或9.0 + MySQL 5.7或8.0 + mysql-connector-java对应版本的驱动jar。这里多说一句:MySQL 8.0的驱动类名虽然兼容com.mysql.jdbc.Driver,但连接URL里的时区参数不配会直接报错,见第4章。

3. 数据库设计:用六张表还原一次完整机考

3.1 用户、科目、题库三张基础表的结构设计

不管zip包里的SQL脚本怎么命名,在线考试系统的数据模型核心就两类:基础数据和考试过程数据。先看基础数据。用户表除了账号密码,一定要有角色字段,否则登录后没办法区分跳转到哪个管理后台。科目表用于把题库和考试挂到具体课程下,比如“Java程序设计”“数据库原理”。题库表是整张业务里字段最多的,因为它要兼容单选、多选、判断、填空四种题型。

下面是一份与课程设计常见风格一致的核心建表脚本,适合MySQL 5.7及以上版本。

CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码,MD5加密或明文均可,答辩建议存哈希', real_name VARCHAR(50) COMMENT '真实姓名', role TINYINT NOT NULL COMMENT '角色:1管理员,2教师,3学生', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_subject ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '科目ID', name VARCHAR(100) NOT NULL COMMENT '科目名称' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_question ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '题目ID', subject_id INT NOT NULL COMMENT '所属科目,逻辑外键关联t_subject', qtype TINYINT NOT NULL COMMENT '题型:1单选,2多选,3判断,4填空', difficulty TINYINT DEFAULT 1 COMMENT '难度:1简单,2中等,3困难', content TEXT NOT NULL COMMENT '题干', option_a VARCHAR(255), option_b VARCHAR(255), option_c VARCHAR(255), option_d VARCHAR(255) COMMENT '选项,判断和填空题为空', answer VARCHAR(255) NOT NULL COMMENT '标准答案:单选存A,多选存ABD,判断存对/错,填空存文本', score INT NOT NULL DEFAULT 2 COMMENT '本题分值' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有两个值得你抄下来的设计决策。第一个是逻辑外键而不是物理外键:t_question里的subject_id没有用外键约束,只是普通索引。很多初始化脚本在导入时失败,原因是建表顺序和物理外键相互依赖。课程设计阶段用逻辑外键能避免删题时被外键拦截,代码里通过JOIN t_subject ON t_question.subject_id = t_subject.id查询也完全够用。第二个是answer字段的混合存储:单选存单个大写字母,多选存“ABD”这种字符串,判断存“对/错”,填空存文本。这样判分逻辑可以统一从answer字段取值比对,不需要为每种题型设计独立的答案表。

3.2 考试、试卷题目关系、成绩三张过程表

考试要支持多次发布,同一套题库不能因为生成一次试卷就被清空,所以必须有t_exam和t_exam_question两张表配合。t_exam记录考试的标题、时长、总分和状态,t_exam_question记录一场考试选出了哪些题目。学生交卷后,判分结果落在t_score表里。

CREATE TABLE t_exam ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '考试ID', title VARCHAR(200) NOT NULL COMMENT '考试标题', subject_id INT NOT NULL COMMENT '所属科目', duration INT NOT NULL DEFAULT 60 COMMENT '考试时长,单位分钟', total_score INT NOT NULL DEFAULT 100 COMMENT '试卷总分', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0未开始,1进行中,2已结束' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_exam_question ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '关系ID', exam_id INT NOT NULL COMMENT '考试ID', question_id INT NOT NULL COMMENT '题目ID', question_order INT NOT NULL COMMENT '题目在试卷中的序号' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '成绩ID', exam_id INT NOT NULL COMMENT '考试ID', user_id INT NOT NULL COMMENT '学生用户ID', score DECIMAL(5,2) NOT NULL COMMENT '卷面得分', submit_count INT NOT NULL DEFAULT 1 COMMENT '提交次数,用于防止重复提交统计', exam_time DATETIME NOT NULL COMMENT '交卷时间', UNIQUE KEY uk_exam_user (exam_id, user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

t_score表上加UNIQUE KEY uk_exam_user (exam_id, user_id)是个很容易被忽略但很重要的细节。如果学生对着同一次考试重复交卷,没有唯一约束,数据库里就会出现两份成绩,成绩列表和后续统计全部翻车。有了唯一约束,第二次插入直接报DuplicateKeyException,代码里捕获后更新成绩即可,这样“重复提交”的bug在数据库层面就被拦住了。t_exam_question表则把“考试”和“题库”解耦,一套题库可以生成100份不同试卷,每份试卷只记录题目ID和顺序,学生看到的页面动态按question_order渲染。

3.3 初始化数据与答辩追问点

初始化脚本通常包含三类数据:管理员账号、测试学生账号、一份可以直接开考的题库。内置账号的写法很固定,常见是这样:

INSERT INTO t_user (username, password, real_name, role) VALUES ('admin', '123456', '系统管理员', 1), ('teacher', '123456', '张老师', 2), ('stu001', '123456', '张三', 3); INSERT INTO t_subject (name) VALUES ('Java程序设计'), ('数据库原理'); INSERT INTO t_question (subject_id, qtype, difficulty, content, option_a, option_b, option_c, option_d, answer, score) VALUES (1, 1, 1, '下列哪个类是所有Java类的父类?', 'String', 'Object', 'Class', 'System', 'B', 2), (1, 2, 2, '下面哪些是Java的集合接口?', 'List', 'Set', 'Map', 'Queue', 'ABD', 3);

初始化数据这里几乎一定会被答辩老师追问“密码为什么是明文”。我的建议是:不管zip包里原来存的是不是明文,你交项目前至少做一次MD5加密改造,哪怕只改登录逻辑一处。常见做法是登录时对用户输入的密码做MD5(password)再和库里比对,注册或初始化数据时存MD5("123456")的散列值。这样既不用引入spring-security等重框架,也能在答辩时理直气壮地讲“我做了密码哈希存储”。如果你愿意再多做一步,在MD5基础上拼接用户名做盐值,就足够堵住大部分追问。

4. 从zip到跑通:环境搭建与项目导入全流程

4.1 版本匹配:JDK、Tomcat、MySQL怎么配才不玄学

在线考试系统这类传统Java web项目,最容易栽在版本搭配上。常见组合有两条路线,建议你按自己电脑现状选:

组合JDKTomcatMySQLJDBC驱动jar连接URL注意点
稳妥路线1.88.5或9.05.7mysql-connector-java 5.1.49不用配serverTimezone
新版本路线1.8或119.08.0mysql-connector-java 8.0.x必须配serverTimezone=Asia/Shanghai

如果你机器上已经装了JDK 17或更高,Tomcat 8.5启动大概率报org.apache.jasper.JasperException或直接起不来。这不是项目代码的问题,是Jasper JSP编译器对高版本JDK的兼容性翻车。优先安装JDK 1.8,把JAVA_HOME指过去,比在IDE里反复改Tomcat版本设置省事得多。

打开命令行确认环境,输入下面三条命令检查输出:

java -version javac -version

javac存在说明配置好了JDK而不是只装了JRE。接下来确认MySQL服务已启动并且root账号能用密码登录:

mysql -uroot -p

如果这条命令提示mysql: command not found,说明MySQL的bin目录没加进PATH,在服务里找到MySQL的bin路径后手动加,或者在连接数据库的配置里用本机IP而不是直接用命令行验证。这一步失败的常见原因是安装MySQL时用了“无密码安装”或密码为空的root账户,后面Jdbc连接抓瞎。

4.2 解压导入与数据库连接配置的修改点

zip包解压后,先别急着双击导入。看一层目录结构,标准Servlet+JSP项目长这样:src/放Java包,WebContent/或WebRoot/放JSP和WEB-INF,根目录下有SQL脚本和说明文档。用IDEA导入时选“Import Project”或打开目录后识别为Eclipse项目,记得在Project Structure里把Language Level设成8,再把Web目录标记为Web Resources根。Eclipse的话用“Import → Existing Projects into Workspace”即可。

修改数据库连接要找到配置文件,一般是src/jdbc.properties、db.properties,也可能硬编码在某个DBUtil.java里。配置文件写法通常长这样:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/exam_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的数据库密码

这份配置有三个参数你必须理解,不然改了也只是瞎试。第一个是characterEncoding=utf8,它保证Jdbc连接和MySQL之间的传输使用UTF-8,不配就会出现中文乱码。第二个是useSSL=false,本地开发不加密,省去证书告警。第三个是serverTimezone=Asia/Shanghai,MySQL 8.0必须配,5.7可以不配,但配了也不报错。注意驱动类名:MySQL 8.0虽然兼容com.mysql.jdbc.Driver,但官方要求改成com.mysql.cj.jdbc.Driver,如果驱动jar是8.x而配置里写的是老驱动名,控制台会打Warning,有些版本直接建连失败。建议统一改成新驱动类名。

改完数据库配置后,把SQL脚本导入MySQL。命令行操作为例:

mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS exam_db DEFAULT CHARSET utf8mb4;" mysql -uroot -p exam_db < exam_system.sql

第一条命令建库,第二条把你zip包里带的SQL脚本导入。如果SQL脚本里本身带了CREATE DATABASE语句,可以直接执行第二条,但要注意脚本里的库名和jdbc配置里的exam_db是否一致。这里算是一个高频坑:很多人只改jdbc配置文件里的IP和密码,没注意SQL脚本创建的库名是另一个,运行时报Unknown database。

4.3 启动Tomcat、访问与第一次登录验证

导入完成后,在IDE里添加一个Tomcat Server运行时,把项目Deployment配置成以/exam或类似路径访问。启动成功后看Tomcat控制台日志,出现Server startup in [xxx] milliseconds才算起来。然后浏览器访问:

http://localhost:8080/exam/

第一次验证不要上来就点“开始考试”,先分别用第3.3节里的admin、teacher、stu001三个账号各登录一次。看跳转逻辑:管理员进的是用户管理页,教师进的是题库管理页,学生进的是考试列表页。如果三个账号都跳到了同一个页面,说明登录Servlet里按角色分发页面的逻辑有问题——这是这个项目最核心的功能之一,答辩演示时三端切换是最直观的展示点。

启动中途大概率会遇到8080端口被占用。Windows下查占用并结束进程的命令:

netstat -ano | findstr :8080 taskkill /F /PID 进程号

如果Tomcat配置的是8080端口但启动失败,先在命令行执行catalina run看详细异常,不要只看IDE那几行红字。Tomcat的日志在logs/localhost.yyyy-MM-dd.log里,JSP编译错误和Servlet初始化异常都在这里,比IDE控制台完整得多。

5. 核心代码实现与常见问题排查:登录、随机组卷、自动判分

5.1 登录验证与Session拦截:写对第一次判断

用户登录的Servlet是理解这个项目的第一块基石,它会把request里的账号密码取出来,查t_user表,匹配成功就写入Session并跳转到对应角色首页。核心逻辑与课程设计里最常见的写法一致:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String md5Password = MD5Util.md5(password); // 对输入做MD5,再和库中散列值比对 User user = userDao.findByUsernameAndPassword(username, md5Password); if (user == null) { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setAttribute("role", user.getRole()); if (user.getRole() == 1) { response.sendRedirect(request.getContextPath() + "/admin/user_list.jsp"); } else if (user.getRole() == 2) { response.sendRedirect(request.getContextPath() + "/teacher/question_list.jsp"); } else { response.sendRedirect(request.getContextPath() + "/student/exam_list.jsp"); } }

这段代码有三个参数层面的细节要讲清楚。第一行setCharacterEncoding("UTF-8")必须在读取任何参数之前执行,否则POST请求里的中文参数到后端会变成乱码。getParameter是阻塞获取,依赖前端表单的name属性和这里一致,所以教材里常说“LoginServlet里最容易报的是NullPointerException,一看就是name写错了”。Session里存两个值:loginUser存完整对象,方便页面直接${sessionScope.loginUser.realName}显示姓名;role单独存一份,是为了后续Filter拦截时不必查库就能判断权限。

登录之外必须有一个Filter做全局权限控制,否则用户直接访问/student/exam_list.jsp就绕过了登录。典型做法是给所有*.jsp请求配一个拦截器,对login.jsp和静态资源放行,其余请求检查session里有没有loginUser,没有就重定向回登录页。这个Filter同时也是统一配置编码的地方,把request.setCharacterEncoding("UTF-8")放这里,就不必在每个Servlet里重复写。

5.2 随机组卷:ORDER BY RAND()在什么规模下够用

组卷是“在线考试系统”区别于“题库管理系统”的关键功能。教师创建考试时先设置标题、科目、时长和总分,再按题型指定题目数量,后台从题库里随机抽取。小规模系统最常见的抽题SQL如下:

SELECT * FROM t_question WHERE subject_id = ? AND qtype = ? ORDER BY RAND() LIMIT ?

三个参数分别对应是哪一科、第几题型的题、抽多少道。ORDER BY RAND()的原理是给每行生成一个随机值再排序,题目量5000以内性能没有压力,课程设计的题库一般就几百道,完全不需要上复杂算法。如果以后题库到了几十万题,才需要考虑在应用层用SELECT id FROM t_question WHERE subject_id=?先拿ID列表,再用随机算法选ID集合,最后WHERE id IN (...)查详情,避免全表排序。

组卷的Servlet把抽题结果写入t_exam_question表,同时计算总分并更新t_exam。逻辑如下:

public int createExamPaper(Exam exam, Map<Integer, Integer> questionCountMap) { int examId = examDao.insert(exam); // 先插入考试基本信息,拿到自增主键 int totalScore = 0; int order = 1; for (Map.Entry<Integer, Integer> entry : questionCountMap.entrySet()) { int qtype = entry.getKey(); int count = entry.getValue(); List<Question> questions = questionDao.findRandomQuestions(exam.getSubjectId(), qtype, count); for (Question q : questions) { examQuestionDao.insert(examId, q.getId(), order++); totalScore += q.getScore(); } } examDao.updateTotalScore(examId, totalScore); return examId; }

这里的参数questionCountMap是从页面表单里传来的,键是题型编码,值是抽题数量。注意一个容易被忽略的业务约束:抽题前先SELECT COUNT(*)判断题库里该题型够不够抽,不够直接提示教师改数量,否则findRandomQuestions返回的List不足count条,试卷总分会小于预期,学生那边会出现“总分100的试卷实际只有80分”的诡异问题。t_exam_question表里的question_order字段决定学生答题页的题目展示顺序,不是按照每道题的数据库id排,而是按照抽取顺序写死的,避免每次进入试卷页面题目顺序都不一样。

5.3 自动判分:遍历答卷、比对答案、写入成绩

学生提交答卷时,前端表单里每一道题的name="answer_题目ID",后端依次取出,和t_question表里的answer字段比对。判分核心逻辑如下:

public SubmitResult submitExam(int examId, int userId, HttpServletRequest request) { List<ExamQuestion> eqList = examQuestionDao.findByExamId(examId); BigDecimal totalScore = BigDecimal.ZERO; int correctCount = 0; for (ExamQuestion eq : eqList) { Question q = questionDao.findById(eq.getQuestionId()); String userAnswer = request.getParameter("answer_" + q.getId()); userAnswer = (userAnswer == null ? "" : userAnswer.trim()); boolean correct = false; if (q.getQtype() == 1 || q.getQtype() == 3) { // 单选、判断:答案字符串直接相等即可 correct = q.getAnswer().equalsIgnoreCase(userAnswer); } else if (q.getQtype() == 2) { // 多选:必须完全一致才给分,顺序不同也算错 correct = sortAndEquals(q.getAnswer(), userAnswer); } else if (q.getQtype() == 4) { // 填空:去掉首尾空格后不区分大小写比对 correct = q.getAnswer().equalsIgnoreCase(userAnswer.trim()); } if (correct) { correctCount++; totalScore = totalScore.add(BigDecimal.valueOf(q.getScore())); } } Score score = new Score(examId, userId, totalScore, 1, new Date()); try { scoreDao.insert(score); } catch (DuplicateKeyException e) { scoreDao.updateScore(score); // 唯一约束兜底,重复提交更新成绩 } return new SubmitResult(correctCount, totalScore); }

判分逻辑里有三个边界处理值得反复讲。第一个是多选题判分标准:课程设计阶段普遍采用“完全一致才得分”,也就是学生选了A、B,标准答案是B、A,判错。这样实现简单,答辩时也可以自圆其说。第二个是填空和判断题型输入框的内容去空格,不然学生在“对 ”后面多敲个空格就被判错,反馈极差。第三个是重复提交处理——学生交卷后如果手滑刷新页面,表单会再次提交,这里利用t_score表的唯一约束捕获DuplicateKeyException再执行update,而不是insert,这样成绩不会被覆盖丢记录。加上重定向到成绩页面,才能从根源上避免刷新重复交卷。

5.4 四个高频排查点与修复记录

这一节是血泪经验,现象、原因、解决都给你理顺,遇到同类问题直接照着查。

现象一:登录或保存题目后,数据库里的中文全是问号。

原因通常是三层中的某一层不一致:JSP页面编码、Tomcat请求编码、MySQL表编码。常见情况是JSP页面没写<%@ page contentType="text/html; charset=UTF-8" %>,或者数据库表和连接URL用的是latin1。解决:统一把JSP页面顶部的page指令编码设为UTF-8;确认第4.2节连接URL里有characterEncoding=utf8;建库脚本里加DEFAULT CHARSET utf8mb4;清空数据库重新导入SQL脚本。改完以上还是乱码,就看MySQL服务端character_set_server变量,把my.ini里设为utf8mb4并重启服务。

现象二:Tomcat启动时控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。

原因:mysql-connector驱动jar没有放进WEB-INF/lib目录。IDE不报错是因为编译期引用到了jar,但运行时Tomcat的classpath不会自动包含项目外部的jar。解决:把驱动jar复制到项目WebContent/WEB-INF/lib/下,然后Project Structure里确认该jar已经被标记为Artifact的一部分。这个坑非常隐蔽,很多人在pom里加依赖习惯了,忘了传统项目要物理复制jar才能进war包。

现象三:刷新成绩页,分数变成原来的两倍。

原因:提交请求没有做重定向,浏览器刷新时把整个POST请求重新发了一遍,判分逻辑再次执行。解决:第5.3节里用DuplicateKeyException兜底属于后手,最好的方案是Servlet执行完判分后执行response.sendRedirect(request.getContextPath() + "/student/score.jsp?examId=" + examId),让地址栏变成GET请求,刷新只会重复读取成绩,不会重新判分。注意重定向以后需要把成绩再查一遍展示,不能在redirect时把成绩对象塞进request。

现象四:我的JDK 17运行Tomcat 9,页面能打开但登录后报JasperException。

原因:高版本JDK移除了JSP编译所需的javax.tools工具类相关行为,Tomcat的Jasper编译器兼容性不稳定。解决:卸载高版本JDK,装JDK 1.8并把JAVA_HOME指过去,重启IDE和Tomcat。别在IDE里改半天项目配置,这个问题不在代码层面。如果你必须用JDK 17跑项目,那就需要换Tomcat 10以上并引入Jakarta命名空间的依赖,课程设计阶段没有这个必要,别给自己找事。

6. 从“能跑起来”到“经得起验收”:三个必做的验证动作

项目能在浏览器里打开只是第一步,答辩演示和自测是两回事。我一般会在交项目前做三个固定验证,每个都踩过真坑。

验证一:把数据库全删了重新导一遍脚本。DROP DATABASE exam_db;再重新CREATE DATABASE、导入SQL,然后走一遍登录、随机组卷、学生交卷完整流程。这个动作保证你的“源码+数据库”压缩包换一台全新电脑也能复现,而不是依赖当前环境里已经存在的脏数据。多少人的演示翻车都发生在换了答辩教室的电脑以后——数据库根本没导入成功,台上现查报错。

验证二:手动算一遍分数。随机组一份只抽5道题的试卷,请人用纸笔按正确答案算出总分,再交卷对比系统自动判分的结果。多选判分尤其要验证:全对、漏选、错选三种情况各测一次,确认漏选不给分是你预期的标准。这个验证直接防住第5.3节里“答案比对大小写”和“填空去空格”两个边界坑。

验证三:同一个学生账号开两个浏览器窗口同时进考场,先交一个,再交另一个,观察成绩是否被正确更新而不是出现两条记录,同时看交卷后点刷新是否重复判分。这是答辩老师最爱问的高频问题,也是最容易暴露设计缺漏的地方。

如果这三个验证都通过,这个在线考试系统的核心功能就具备被验收的资格了。扩展方向上,建议优先做考试倒计时自动交卷和成绩导出Excel,这两个功能代码量不大,但对“在线考试系统”完整度的提升非常明显。这个zip包的价值从来不是那份源码本身,而是你能不能把它讲成自己的系统。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进