
简介这是一套基于 Java Web 技术栈实现的通用在线考试系统源码采用 JSPStrutsHibernateOracle 组合开发适合正在学习 SSH 框架整合与 MVC 分层架构的中级 Java 学习者。压缩包共 342 个文件、约 3.1MB包含 71 个 Java 源码、34 个 JSP 页面、22 个 XML 配置、SQL 脚本及项目文档代码结构完整。系统涵盖用户注册登录、选择考试、在线作答、提交答案与成绩查询等核心功能并通过 Session 保存作答状态。资源完整展现了 JSP 负责视图展示、Struts 处理请求分发、Hibernate 完成对象关系映射的核心流程对于理解 MVC 设计模式、持久层配置及 Oracle 数据库交互具有很好的参考价值。已有 95 人学习适合作为课程设计或 SSH 框架入门实践项目参考。1. 通用的在线考试系统为什么 JSPStrutsHibernateOracle 这套老组合还值得你接手拿到“通用的在线考试系统(jspstrutshibernateoracle)130220.zip”这种压缩包第一反应往往是这都什么年代了还在用 JSP 和 Struts但真进了企业内网或高校机房你会发现这类系统不仅还在运行而且“通用”这两个字恰恰是它的价值点——题库、组卷、考试、阅卷四条链路都留了可替换的扩展点。这篇笔记要讲的就是把这套老组合在本地跑通、读懂它的数据流再按你的业务去改造它。适合刚接手课程设计、毕设或老系统维护的开发者也适合想快速搭一套考试平台但不想从零写题组逻辑的后端工程师。先别急着换 Spring Boot把 Struts Hibernate Oracle 的协作逻辑吃透项目才能真正落到你手里。2. 先看懂这套组合的分工Struts 管流程Hibernate 管持久化Oracle 管数据总有新人问 Hibernate 还有人用吗——在这类老项目里你躲不开它因为业务逻辑全骑在 ORM 映射上。Struts 1.x 负责 HTTP 请求的流转Hibernate 负责把 Java 对象变成数据库表记录Oracle 负责最终的数据存储和复杂查询。三个环节如果只理解一个部署时就会陷入“能启动但点几下就报错”的泥潭。这一章我按一条考试请求的路径把三个层次拆开讲后面改动时你才知道该去改哪个文件。2.1 Struts 管流程Action、ActionForm、struts-config.xml 的三层流转Struts 1.x 是这套系统的控制器。JSP 只负责展示表单提交到 ActionServlet由 struts-config.xml 把请求路径映射到具体的 Action 类。Action 里拿到的 ActionForm 相当于一个绑定好请求参数的对象比如登录页提交的 username 和 password框架会自动帮你填进 LoginForm 的属性里。校验通过后Action 调用业务层最后返回 ActionForward 跳转到下一个页面。在线考试系统里的登录、组卷、交卷全都走这条链路。一个典型的登录 Action 配置长这样struts-config form-beans form-bean nameloginForm typecom.exam.form.LoginForm/ /form-beans action-mappings action path/login typecom.exam.action.LoginAction nameloginForm scoperequest forward namesuccess path/main.jsp/ forward nameerror path/login.jsp/ /action /action-mappings /struts-config这里path是浏览器访问的 URL和 JSP 里的form actionlogin.do对应type是真正干活的 Action 类name指向上面定义的 form-beanscoperequest表示这个表单对象的生命周期只在当前请求里。老项目里经常把 scope 写成 session结果用户登录后表单数据一直占着内存这是后来内存溢出的隐患之一。改造时我一般会把只读数据全部收回 request只有考试中的答案才放 session。Action 类里的写法是旧式 Struts 风格类要继承 Action重写 execute 方法public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { LoginForm loginForm (LoginForm) form; String username loginForm.getUsername(); String password loginForm.getPassword(); // 调业务层验证成功后把用户信息放进 session request.getSession().setAttribute(currentUser, user); return mapping.findForward(success); } }看到这段代码你就明白了Struts 本身不干数据库的活它只负责“请求进来之后该找谁”以及“干完之后跳到哪个页面”。如果系统里所有业务逻辑都被塞进 Action就会变成业界常说的“胖 Action”。改造成通用考试系统时我的习惯是让 Action 只做参数校验和页面跳转真正的组卷、判分逻辑放到 Service 类里。2.2 Hibernate 管持久化hbm.xml 映射与 Session 的坑这个项目能被称为“通用”很大程度是因为 Hibernate 把题、卷、学生、成绩这几个核心对象做成了可脱离数据库表结构的 POJO。老项目里注解还没普及基本都是.hbm.xml映射文件。一个试题对象的映射长这样hibernate-mapping packagecom.exam.po class nameExamQuestion tableEXAM_QUESTION id nameid columnQUESTION_ID generator classsequence param namesequenceSEQ_EXAM_QUESTION/param /generator /id property namestem columnQUESTION_STEM typestring/ property nametype columnQUESTION_TYPE typeinteger/ property namescore columnSCORE typefloat/ property nameanswer columnANSWER typestring/ /class /hibernate-mapping这里最需要注意的是generator classsequence。Oracle 没有 MySQL 那样的自增列主键只能靠序列生成。如果你改成nativeHibernate 会在 Oracle 上自动选 sequence但选出来的序列名未必是你建好的那个。新人最容易踩的坑就是数据库里明明有表一保存就报ORA-02289: sequence does not exist。Hibernate 的 Session 是个短命对象每次请求最好开关一次不要全局共享。老项目里常见的写法是Session session HibernateUtil.getCurrentSession(); Transaction tx session.beginTransaction(); session.save(question); tx.commit();getCurrentSession和openSession的区别在于前者绑定了事务和线程后者每次要手动 close。如果你用openSession又不关连接池很快被耗尽Tomcat 在跑两个小时后开始出现“无法获得数据库连接”。我排查过的好几个在线考试系统都是死在这个细节上。Hibernate 在这里真正省事的是让你用session.get(ExamQuestion.class, id)就能把一条记录变成对象而不用写一堆 JDBC 模板。但代价是取列表时容易触发 N1 查询先查出来 30 道题的 ID再对每个 ID 查一次详情页面就会明显卡顿。后面第 4 章组卷时我会给一个避免方式核心是让 Hibernate 走 join fetch。2.3 Oracle 存数据分页、序列与存储过程Oracle 分页和 MySQL 完全不是一个套路。MySQL 的LIMIT offset, size在 Oracle 里不存在老项目里手写分页基本是三层嵌套 ROWNUMSELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT q.* FROM EXAM_QUESTION q ORDER BY q.QUESTION_ID ) t WHERE ROWNUM ? ) WHERE rn ?;第一个参数是页码乘以每页条数第二个参数是 (页码减一) 乘以每页条数。这段 SQL 的意思是先按题目 ID 排序再给排序结果编一个行号最后截取行号区间。看起来很绕但这是 Oracle 分页的标准姿势。如果用 Hibernate 的 Oracle10gDialect它生成的也是这种结构你只需要配置好hibernate.dialectorg.hibernate.dialect.Oracle10gDialect。在线考试系统还经常把阅卷统计逻辑写进 Oracle 存储过程。原因是几十个学生同时交卷时客观题和主观题分数要回写到多张表用 Java 逐个更新容易出现中间状态。一个简单的存储过程签名大致是CREATE OR REPLACE PROCEDURE SP_CALC_EXAM_SCORE( p_exam_id IN NUMBER, p_student_id IN NUMBER, p_score OUT NUMBER ) IS BEGIN -- 更新 EXAM_RECORD 表的总分 UPDATE EXAM_RECORD SET TOTAL_SCORE p_score WHERE EXAM_ID p_exam_id AND STUDENT_ID p_student_id; COMMIT; END;调用时用CallableStatement注册出参这是老项目里常见的套路。你在改造成通用考试系统时如果自己写阅卷逻辑优先考虑把“一次交卷要改三张表”的动作放进一个事务或存储过程别在 Java 里拆成三次 update。第 4 章我会再展开这一点。3. 在本地把系统跑起来JDK、Tomcat、Oracle 的版本搭配与部署步骤这类压缩包拿到手最怕的是在环境上耗掉一个周末。版本不匹配时Tomcat 能启动但一访问数据库就抛各种奇怪异常。我这几年处理老项目的血泪经验是先定版本再动代码。不要用最新版 JDK 硬跑 2013 年前后的东西。130220 这个编号我一般先当成打包日期看那个年代的 Struts 项目普遍是 JDK 6 或 7Tomcat 6 或 7Oracle 11g 最常见。3.1 环境版本搭配JDK 1.7 Tomcat 7 Oracle 11g 最省事下面这张表是我接手老系统默认采用的搭配不是绝对标准但能避开九成兼容性问题。组件推荐版本说明JDK1.7jdk7u80支持 JSP 编译和旧库反射不会像 JDK8 那样出现某些 XML 解析异常Tomcat7.0.x兼容 Servlet 3.0Struts 1.x 能正常跑比 Tomcat 6 内存管理好Oracle11g XE / 11g R2免费版够用字符集建议建库时选 AL32UTF8ojdbc 驱动ojdbc6.jar匹配 JDK 1.7别用 ojdbc14 也别用 ojdbc8EclipseKepler 或 Luna可直接导入老项目的 .classpathMyEclipse 更省事确认本机已经装了 JDK 后先看版本java -version echo $JAVA_HOME我遇到过一次 Tomcat 能启动但 JSP 编译报UnsupportedClassVersionError的情况原因就是系统里有两个 JDKTomcat 启动脚本里的 JAVA_HOME 指向了 JDK8而项目编译用的是 JDK7。解决办法是编辑 Tomcat 的setenv.sh或catalina.bat强制指定 JAVA_HOME。Linux 下这样写export JAVA_HOME/usr/lib/jvm/java-7-oracle export CATALINA_HOME/opt/tomcat7 $CATALINA_HOME/bin/startup.sh版本定了之后接下来不要急着解压项目先把 Oracle 跑起来因为后面部署一旦报错八成问题都出在数据库没准备好。3.2 初始化数据库导入建表脚本、序列和初始化管理员这类项目的压缩包里一般会带一个 database 目录里面是建表 SQL。常见做法是用 sqlplus 按顺序执行。我先说一句单独建表脚本、序列脚本、初始化数据脚本最好分文件因为 oracle 不像 mysql 能容忍重复执行一个文件里跑到底中途任何一条语句报错后面的对象都不会建。我一般的导入方式是sqlplus system/password//localhost:1521/XE database/create_tables.sql sqlplus system/password//localhost:1521/XE database/create_sequences.sql sqlplus system/password//localhost:1521/XE database/init_data.sql如果手上只有一个大 init.sql可以在 sqlplus 里先执行第一条语句确认没有ORA-00955: name is already used by an existing object再继续。这里要特别检查三个东西序列是否创建、管理员账号是否存在、试题表里有没有测试数据。序列检查用SELECT sequence_name FROM user_sequences;很多系统在登录页输 test 账号进不去不是代码错了而是初始化数据根本没导入。我在第 5 章会专门讲这个现象。3.3 部署到 Tomcat上下文路径与数据源配置部署时最省事的方式是直接丢 war 包到 Tomcat 的 webapps 目录但老项目通常是 exploded 目录也就是 src、WebRoot、database 分层。你需要把 WebRoot 里的内容整理成标准的 Web 应用结构WEB-INF/classes、WEB-INF/lib、jsp 页面。用 Eclipse/MyEclipse 导入后右键项目选择 Export WAR 也行。我一般手工打包因为这样可以顺便删掉编译产生的临时文件。web.xml 里要确认 Struts 的 ActionServlet 配置以及默认首页。典型片段servlet servlet-nameaction/servlet-name servlet-classorg.apache.struts.action.ActionServlet/servlet-class init-param param-nameconfig/param-name param-value/WEB-INF/struts-config.xml/param-value /init-param load-on-startup0/load-on-startup /servlet servlet-mapping servlet-nameaction/servlet-name url-pattern*.do/url-pattern /servlet-mapping welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list数据库连接建议走 Tomcat 的 JNDI 数据源而不是在 Hibernate 配置里写死连接池。原因很简单切换环境时只改一个 context.xml。在META-INF/context.xml里写Context Resource namejdbc/ExamDS authContainer typejavax.sql.DataSource maxActive20 maxIdle10 maxWait10000 driverClassNameoracle.jdbc.OracleDriver urljdbc:oracle:thin:localhost:1521:XE usernameexam passwordexam validationQuerySELECT 1 FROM DUAL/ /ContextmaxActive控制最大连接数在线考试场景下按并发用户数来调我一般设 20 起步压测后再加。validationQuery用SELECT 1 FROM DUAL是 Oracle 专用写法MySQL 是SELECT 1别混。Hibernate 那边再通过jndi.dataSource引用这个数据源即可。启动 Tomcat 后访问http://localhost:8080/项目名/如果出现首页但登录报数据库连接失败回到这里检查 context.xml 里的用户名密码和 Oracle 实例名。实例名 XE 是 Oracle 11g XE 的默认 SID完整版可能是orcl写错占位符会报Listener refused the connection。4. 把老系统改造成“通用”在线考试系统题库、组卷、考试、阅卷四个关键点一套在线考试系统能不能叫“通用”不看登录页漂不漂亮只看四件事题库是不是可以随便加题改题组卷是不是能按题型和难度抽题考试过程中答案会不会丢交卷后分数准不准。这一章我按这四个维度拆开讲每个都给出了可复现的 SQL 或 Java 写法你照着改就能从“只能跑演示数据”变成“能真正投入小规模使用”。4.1 题库表设计把多题型统一成一张表很多毕设项目把单选、多选、判断、简答各建一张表表面看很清晰实际上写组卷逻辑时会疯掉——每种题型都要写一套查询和拼接逻辑。通用系统的做法是统一放进一张 EXAM_QUESTION 表用类型字段区分。CREATE TABLE EXAM_QUESTION ( QUESTION_ID NUMBER(10) NOT NULL, QUESTION_TYPE NUMBER(2) NOT NULL, QUESTION_STEM VARCHAR2(2000) NOT NULL, OPTION_A VARCHAR2(500), OPTION_B VARCHAR2(500), OPTION_C VARCHAR2(500), OPTION_D VARCHAR2(500), ANSWER VARCHAR2(500), SCORE NUMBER(4,1) DEFAULT 5, DIFFICULTY NUMBER(1) DEFAULT 1, STATUS NUMBER(1) DEFAULT 1, CONSTRAINT PK_EXAM_QUESTION PRIMARY KEY (QUESTION_ID) );QUESTION_TYPE我习惯定义为1 单选、2 多选、3 判断、4 简答。单选题的ANSWER存 “A”多选题存 “ABD”取题时前端手工处理成字符串判断时按字母排序后比较判断题存 “T” 或 “F”简答题的ANSWER留空留给人工阅卷。STATUS是逻辑删除位考试系统里的题不能物理删否则历史试卷的引用全断。查询时这样写SELECT * FROM EXAM_QUESTION WHERE QUESTION_TYPE 1 AND STATUS 1 ORDER BY DIFFICULTY;这套设计的核心好处是组卷时只有一个表可查加新题型时只需要在代码里多注册一个枚举值不用改表结构。选项超过 D 或需要图片题时可以再加 OPTION_E、OPTION_F或者用一个子表存附加选项。图片题在老系统里通常是把图片路径存进QUESTION_STEM里前端用固定前缀拼完整 URL。如果你做的是 JSP 页面图片坐标定位可以用 CSS 的 background-position但考试题一般用不着存路径就够了。4.2 随机组卷DBMS_RANDOM 与 SAMPLE 的取舍组卷本质是“按一定规则从题库抽题”最简单的实现是ORDER BY DBMS_RANDOM.VALUESELECT * FROM ( SELECT * FROM EXAM_QUESTION WHERE QUESTION_TYPE 1 AND STATUS 1 ORDER BY DBMS_RANDOM.VALUE ) WHERE ROWNUM 10;这段 SQL 先对单选题随机排序再取前 10 道。题库量在几千道以内时这个写法完全够用响应时间几十毫秒。但题库到了几十万道时ORDER BY DBMS_RANDOM.VALUE要对全表做一次随机排序代价很大。Oracle 提供了SAMPLE子句做更快但更粗糙的随机抽样SELECT * FROM EXAM_QUESTION SAMPLE(1) WHERE QUESTION_TYPE 1 AND STATUS 1 AND ROWNUM 10;SAMPLE(1)表示从表中随机读取约 1% 的块速度极快但有两个副作用小表可能抽不出足够行抽样分布不均匀同一套题可能连续抽到同一段数据。所以我的实际建议是题库小于 5 万条用DBMS_RANDOM.VALUE大于 5 万或对性能敏感时用 SAMPLE 后补非重复条件。还有一个“抽过的题下次不再出现”的需求。简单方案是在组卷 SQL 里排除 EXAM_PAPER_DETAIL 里已有的题目SELECT * FROM ( SELECT * FROM EXAM_QUESTION q WHERE q.STATUS 1 AND q.QUESTION_ID NOT IN ( SELECT QUESTION_ID FROM EXAM_PAPER_DETAIL WHERE EXAM_ID ? ) ORDER BY DBMS_RANDOM.VALUE ) WHERE ROWNUM 10;这里把已用题目隔离在外避免学生两次考试碰到完全一样的题。注意NOT IN子查询里有 NULL 会导致结果为空所以字段一定要加 NOT NULL 约束。4.3 考试会话答案存 Session 还是存库考试进行中学生每做一题最直觉的做法是立刻把答案写进数据库。但这会给数据库带来不必要的写压力而且一旦中途断网未提交的答案可能只在界面上存了数据库里没有。我一般采用的是“Session 为主自动保存为辅”的双层策略每道题的答案先放进一个内存 Map同时用 Ajax 每隔 30 秒把答案快照写进临时表。这样用户刷新页面不会全丢后端也不会频繁写库。答案临时表的 Java 逻辑可以这样做public class ExamSessionManager { SuppressWarnings(unchecked) public static void saveAnswer(HttpSession session, Integer questionId, String answer) { MapInteger, String answers (MapInteger, String) session.getAttribute(answerMap); if (answers null) { answers new HashMapInteger, String(); session.setAttribute(answerMap, answers); } answers.put(questionId, answer); } }Session 里放一个 Map 比放满是 JSP 输出标签的页面碎片要清爽得多。不过注意HttpSession 默认有 30 分钟过期时间考试时长如果超过 30 分钟必须单独维护一个 examTime 字段并在每次自动保存时刷新 session 的存活时间否则学生刚做到一半被踢下线。常见做法是让自动保存的 Ajax 请求同时 touch 一次 session或者在 web.xml 里把 session-timeout 改成大于考试时长的值。session-config session-timeout120/session-timeout /session-config这个参数的单位是分钟。把 30 改成 120 后学生即使停下来想了几分钟也不会因为 Session 过期被强制交卷。4.4 交卷与阅卷客观题自动判分、主观题人工回写交卷是整个系统最核心的动作因为牵涉到多张表的读写试卷记录、学生答卷明细、总分汇总。我用一个独立方法处理交卷逻辑所有写操作放在同一个数据库事务里。客观题判分在 Java 层做因为客观题答案短查询快主观题分数由老师在后台逐个批改改完后统一调存储过程汇总。客观题判分的核心代码private boolean isAnswerCorrect(ExamQuestion question, String userAnswer) { if (question null || userAnswer null) { return false; } if (question.getType() 2) { // 单选多选 String correct question.getAnswer().trim().toUpperCase(); String answer userAnswer.trim().toUpperCase(); if (question.getType() 2) { // 多选顺序无关 char[] correctChars correct.toCharArray(); char[] answerChars answer.toCharArray(); Arrays.sort(correctChars); Arrays.sort(answerChars); return Arrays.equals(correctChars, answerChars); } return correct.equals(answer); } if (question.getType() 3) { // 判断 return question.getAnswer().trim().equalsIgnoreCase(userAnswer.trim()); } return false; // 简答题不自动判分 }这段代码有三个细节答案两边都要 trim否则用户在选项后不小心打了空格就判错多选要排序后比较否则正确答案是 ABD用户选 BDA 会被误判判断题的答案统一用 T/F不要存成对错否则字符串比较很绕。交卷事务的伪代码如下public void submitExam(int examId, int studentId, MapInteger, String answers) { // 开启事务 // 逐题判分写入 EXAM_ANSWER_DETAIL // 更新 EXAM_RECORD 的总分 // 删除临时快照表记录 // 提交事务 }这里不能把删除临时表放在判分之前否则事务回滚时学生已经提交的答案也一起没了。我在一个真实课程设计里就见过这种顺序错误导致的“交卷后查不到成绩”问题后面第 5 章会列成常见问题。5. 部署与运行中的 5 个常见问题排查从监听连不上到中文乱码这一章是纯踩坑记录。每一类问题我都按“现象 → 原因 → 解决”的顺序写清楚你遇到类似报错时可以直接照着排查省得翻一个下午论坛。5.1 Oracle 监听服务无法启动先看 listener.ora 的主机名现象执行lsnrctl start报TNS-01103: Protocol specific component is invalid或者监听启动成功但远程连接报ORA-12541: TNS:no listener。原因listener.ora 里的 HOST 写的是旧机器的主机名换电脑后系统里没有这个映射或者 hosts 文件里没有配置127.0.0.1 主机名。还有一个常见原因同时装过多个 Oracle 版本监听配置文件被后装的覆盖。解决先用lsnrctl status看当前监听状态再打开$ORACLE_HOME/network/admin/listener.ora把 HOST 改成127.0.0.1或当前机器的局域网 IP。改完重启lsnrctl stop lsnrctl start如果还是失败检查防火墙是否放行了 1521 端口。Linux 下执行netstat -tlnp | grep 1521这条命令会显示监听进程是否存在。看不到输出就是监听根本没起来回到 listener.ora 继续排查。5.2 Hibernate 方言配置不匹配导致分页报 ORA-00933现象列表页第一页正常点第二页时后台日志报ORA-00933: SQL command not properly endedSQL 里出现了LIMIT。原因hibernate.cfg.xml 里把数据库方言配成了 MySQLDialect或者手写分页 SQL 用了 MySQL 的LIMIT ? OFFSET ?。开发者可能之前移植过 MySQL 版本改 Oracle 时漏了这一处。解决把方言改成 Oracle10gDialect 或 Oracle11gDialect。property namehibernate.dialectorg.hibernate.dialect.Oracle10gDialect/property同时检查代码里有没有直接拼接LIMIT的 SQL。我一般全局搜索LIMIT找到就改成 ROWNUM 写法。另外注意Oracle11gDialect 在 Hibernate 3.6 之后才有如果你用的是 Hibernate 3.3就用 Oracle10gDialect功能差不多。5.3 中文乱码JSP、JDBC 驱动、Oracle 字符集三处必须一致现象登录后页面上的题目中文变成一串问号数据库里查出来的数据也是问号最典型的是“科目名称”全乱。原因三处字符集不一致——JSP 页面编码是 ISO-8859-1项目里用 GBKOracle 数据库建库时选了 ZHS16GBK 或 AL32UTF8驱动传输时又用默认编码。乱码来源非常隐蔽可能 JSP 正常但写入数据库时乱了。解决把整套链路统一成 UTF-8。第一处JSP 文件顶部加% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %第二处Oracle 客户端字符集要通过 NLS_LANG 环境变量指定export NLS_LANGAMERICAN_AMERICA.AL32UTF8Windows 下在注册表或系统环境变量里设置同样的值。第三处数据库本身的字符集检查SELECT userenv(language) FROM dual;如果返回值里不是 AL32UTF8说明数据库是 ZHS16GBK 之类。ZHS16GBK 也能存中文但和 UTF-8 的 JSP 页面配合时容易转码出错。我建议新建数据库时直接选 AL32UTF8这样 Java 字符串和 Oracle 存储都是 Unicode乱码概率最低。已经建好的库改字符集有风险不如重建后导入。5.4 交卷按钮重复提交Struts 的 token 机制怎么用现象学生交卷时双击按钮成绩单里出现了两条一模一样的记录总分为两倍。原因交卷请求被连续发送了两次Action 每次都把考试成绩写了一遍也没有检查重复记录。这是 Struts 1 时代最经典的重复提交问题。解决利用 Struts 的 Token 机制在进入考试页时生成一个一次性 token交卷时校验并立刻作废。先saveTokensaveToken(request);然后在 Action 的 execute 方法开头加上if (!isTokenValid(request, true)) { // token 无效或已被使用说明是重复提交 return mapping.findForward(duplicate); }isTokenValid第二个参数传 true表示校验通过后立即销毁 token这样第二次点交卷时 token 已经不存在直接拦住。配合前端把交卷按钮在第一次点击后设成 disabled 效果更好但后端校验必须要有因为双击产生的是两个独立请求。5.5 ojdbc 版本与 JDK 不匹配的 NoSuchMethodError现象Tomcat 启动正常登录页能打开但一点“进入考试”就抛NoSuchMethodError: oracle.jdbc.driver.OracleDriver.getConnection或AbstractMethodError。原因WEB-INF/lib 里放了两份 ojdbc或者 ojdbc14.jar 跑在 JDK 8 上。ojdbc14 是 JDK 1.4 时代的产物老方法在高版本 JDK 里可能被移除而 ojdbc8 需要 JDK 8放在 JDK 7 环境又会报Unsupported major.minor version 52.0。解决查看 lib 目录ls WEB-INF/lib/ojdbc*.jar如果有多份只留一份。JDK 1.7 用 ojdbc6.jarJDK 1.6 用 ojdbc14.jar。确认后重启 Tomcat并在启动日志里搜索Loading class看是否只加载了一个驱动类。还可以直接在 jsp 里写一段临时测试代码验证连接但记得用完删除Class.forName(oracle.jdbc.OracleDriver); Connection conn DriverManager.getConnection( jdbc:oracle:thin:localhost:1521:XE, exam, exam);能连上就说明驱动没问题。报ClassNotFoundException: oracle.jdbc.OracleDriver时则反过来查驱动 jar 是不是被 Maven 或工具过滤掉了。6. 最后一步把黑匣子变成白盒跑一条完整的考试链路老系统接手后我最怕的是只看“登录成功就宣布部署完成”。考试系统真正可怕的故障都在链路后半段组卷抽出的题数对不对、交卷总分是否等于每道题分数之和、主观题回写后统计是否更新。所以我会把整条链路当成一个黑匣子用最少的数据做一次完整的端到端验证。验证方法很简单先手工构造一张只有 5 道题的试卷然后模拟学生答卷、交卷最后跑一条汇总 SQL 对比预期总分。SELECT p.exam_id, COUNT(d.question_id) AS question_count, SUM(q.score) AS expected_total, r.total_score AS actual_total FROM exam_record r JOIN exam_paper p ON r.exam_id p.exam_id LEFT JOIN exam_paper_detail d ON p.exam_id d.exam_id LEFT JOIN exam_question q ON d.question_id q.question_id WHERE r.student_id ? GROUP BY p.exam_id, r.total_score;如果question_count不是 5、expected_total不等于actual_total说明组卷或阅卷链路有断点。这是我养成的一个习惯任何考试系统我都先拿 5 题跑一遍“建单选卷 → 作答 → 交卷 → 查总分”的冒烟测试再交还给业务方。之前接手一个旧系统时我发现总分总是少 2 分查了一下午才发现主观题默认按 0 分处理没有判分完成就提前汇总后来我在交卷事务里加了“主观题未批改不得汇总总分”的状态判断才算真正修好。希望这套排查思路对你也有帮助接手老项目时少走我走过的弯路。本文还有配套的精品资源点击获取