
简介一套基于jspStrutsHibernateOracle构建的通用在线考试系统源码适合正在学习Java Web分层开发的学生或初级开发者。系统按MVC模式组织jsp负责界面展示Struts作为控制器处理请求与业务调度Hibernate完成对象关系映射Oracle存储试题、考生和成绩数据代码中可看到注册登录、在线选题、答案提交、成绩查看等完整交互流程。压缩包共342个文件以java源码、class字节码、jsp页面为主另有xml与properties配置、tld标签库、sql数据库脚本、gif/jpg界面素材以及bat部署脚本整体仅3.1MB目录模块划分清晰便于按表现层、业务层、持久层对照学习。随包附带的sql脚本与数据库备份可还原初始表结构和测试数据帮助快速在本地Oracle环境复现项目。资源在CSDN已有95人学习下载适合用于理解框架整合思路和在线考试系统的功能拆解也可作为课程设计或毕业设计的前期参考快速搭建项目骨架。1. 一套现成的在线考试系统JSP、Struts、Hibernate 与 Oracle 各管什么“在线考试系统”这个名字听起来像上个年代的产物——事实上 jspstrutshibernateoracle 这套组合正是 2013 年前后 Java Web 项目的主流形态。拿到这份源码包时你不需要从零搭架子登录、题库维护、随机组卷、在线考试、自动判分、成绩查询这些模块都已经写好表结构和 SQL 脚本也随包给出。对于两类人最有用一是毕业设计需要一套完整可演示项目的学生二是工作中要维护或重构这类老系统的开发者。这套系统里 Struts 管请求跳转Hibernate 管 Java 对象与 Oracle 表的映射Oracle 管最终的数据存储。接下来的内容我会按“环境怎么搭、代码怎么读、坑怎么避”的思路把它拆开。2. 先把环境凑齐Oracle 实例、JDK 与 Tomcat 的版本配合拿到压缩包第一件事不是急着导入 IDE而是把数据库和运行环境对齐。这类老项目对环境的敏感程度远超新项目Oracle 驱动版本不对、Tomcat 版本太新、JDK 位宽不匹配任何一个都会让你在启动阶段白耗半天。我先从 Oracle 说起。2.1 Oracle 实例准备好建用户、导数据、测连接解压后先看目录。传统 JavaWeb 项目的源码包结构基本一致src 放 Java 源码WebRoot 或 web 放 JSP 和静态资源lib 放依赖 jar 包根目录下通常还有一个 sql 或 database 目录。数据库脚本可能是 dmp用 imp 导入也可能是 sql用 sqlplus 执行。不管哪种先建一个专用用户再导入。sqlplus / as sysdba CREATE USER exam IDENTIFIED BY exam123 DEFAULT TABLESPACE USERS TEMPORARY TABLESPACE TEMP; GRANT CONNECT, RESOURCE TO exam; GRANT CREATE SEQUENCE, CREATE TRIGGER TO exam;这里我给账号统一用 exam/exam123实际项目里密码可能在 hibernate.cfg.xml 或 jdbc.properties 里导入后记得去配置文件核对。GRANT CREATE SEQUENCE和GRANT CREATE TRIGGER容易漏因为 RESOURCE 角色并不包含这两个权限而很多在线考试系统的试题主键和用户主键都靠序列加触发器生成没有这两个权限表建好也无法正常录入。如果导入时报 ORA-01950说明表空间配额不足补一句ALTER USER exam QUOTA UNLIMITED ON USERS;再继续。导入 dmp 用下面这种写法fully 表示全库导入log 参数方便回看失败行。imp exam/exam123127.0.0.1:1521:ORCL fileexam.dmp fully logimp.log如果作者给的是 .sql 脚本直接在 sqlplus 里 执行。导入完成后务必用 sqlplus 做一次真实连接测试这一步能排除掉监听和连接串两个最容易出错的点sqlplus exam/exam123127.0.0.1:1521:ORCL能进说明监听正常。但注意Oracle 12c 以后如果是可插拔数据库PDB要用服务名写法而不是 SID 写法SID 写法会提示 ORA-12505。连接串格式我列在下面关键在于 thin 模式下的主机:端口:SID和//主机:端口/服务名是两种完全不同的写法项目配置文件里写错任何一个字母都连不上。配置项示例说明driveroracle.jdbc.driver.OracleDriver老项目一般用这个urlSIDjdbc:oracle:thin:127.0.0.1:1521:ORCL非容器数据库或 CDB 常见url服务名jdbc:oracle:thin://127.0.0.1:1521/XEPDB112c PDB 推荐username/passwordexam/exam123与 SQL 脚本里的权限用户一致2.2 JDK、Tomcat、lib 三方版本要互相迁就这个项目如果是 struts 2.3.x 那套JDK 1.7 或 1.8 都行Tomcat 7/8 是绝对安全区。Tomcat 9 凑合能跑Tomcat 10 直接跑不了——因为 Jakarta EE 把 javax.servlet 整个改了包名。所以如果你电脑上只有 Tomcat 10先去下一个 Tomcat 8.5 或者 9.0不要跟它硬刚。lib 目录是另一个重灾区。打开 lib 你会看到一摞 jar其中最关键的是 ojdbc 和 javassist。ojdbc14 是 Oracle 9i 时代的驱动用它连 Oracle 11g 以上的库会报 ORA-01017 或 ORA-28040一句话概括就是“认证协议不匹配”换成 ojdbc6对应 11g或者 ojdbc812c/19c 都能用立刻解决。javassist 则要留意版本单一性Hibernate 3 自带 javassist 3.xStruts 2 的依赖也会带一份 javassist两份同时存在时启动阶段经常抛 ClassCastException后面避坑章我会单独讲。导入 IDE 这一步推荐按 Eclipse 工程导入IDEA 里选 File → New → Project from Existing Sources指定源码包根目录识别出 Eclipse 工程后把 Web 目录指到 WebRoot。如果作者已经写了 pom.xml当然优先 Maven 导入但这类老项目更多是纯 lib 管理导入后双击项目打开 Project Structure 检查 lib 下依赖是否都被识别为库尤其是 ojdbc没识别到就手动加。java -version # 期望输出 1.8.x # 然后看 Tomcat 的 conf/context.xml 是否有全局数据源配置我一般在导入 IDE 前快速执行一下 Java 和 Tomcat 环境验证确认 JDK 小版本不像某些 Hibernate 3 版本在 JDK 1.8 以上的某个 update 有序列化兼容问题。整体思路是先向 Oracle 要一个能用的库再向 Tomcat 要一个能跑的容器最后才轮到代码本身。这是我处理这类项目时的一条血泪经验顺序反了后面排查会同时面对环境和代码两堆问题。3. 从登录到组卷Struts 动作链与 Hibernate 映射怎么配合环境跑通后下一步是读懂这套代码的调用链路。Struts 在这里扮演的是前端控制器Hibernate 负责把 Java 对象和 Oracle 表互相转换两个框架通过 struts.xml 和 hbm.xml 文件连接起来。我先从最常用的登录功能拆起。3.1 登录鉴权Action 收到请求后怎么找到数据库Struts 2 的请求路由全部写在 struts.xml 里。一个典型的登录 Action 配置长这样struts package nameexam namespace/ extendsstruts-default action namelogin classcom.exam.action.LoginAction result namesuccess/main.jsp/result result nameerror/login.jsp?error1/result /action /package /strutsaction 的 name 对应页面里的请求路径比如form actionlogin.action。class 指向 LoginAction 类Struts 会自动实例化并调用它的 execute() 方法。result 里的 name 对应 execute() 返回的字符串success 就跳主页面error 就回登录页并带错误参数。这套路由逻辑跟 Spring MVC 的 RequestMapping 本质一样只是 XML 配置和约定更重一些。LoginAction 核心代码通常是这样public String execute() throws Exception { // username/password 是页面表单字段Struts 注入到 Action 的同名属性 User user userDao.findByUsernameAndPassword(username, password); if (user null) { return ERROR; // error } // 把用户放进 session后续 JSP 用 session.get(loginUser) 取 ActionContext.getContext().getSession().put(loginUser, user); return SUCCESS; // success }Action 上半部分是从表单注入的用户名密码下半部分是查询结果判断。这里有个老项目常见的写法差异有的版本用userDao.findByUsername(username)先查出来再在 Java 里比对密码有的直接用 HQL 把两个参数拼进查询后一种在 SQL 日志里会暴露密码明文。我在维护时一般会改成先查用户再比对但如果源码包是直接查的就别大改——优先保证它能跑。3.2 组卷随机抽题与按题型抽取的实现在线考试系统最核心的模块是组卷。这套系统通常支持两种组卷方式手动按题型题量配比或者完全随机。随机抽题在 Hibernate 里最常见的写法是public ListTQuestion randomPick(int subjectId, int count, int difficulty) { Session session HibernateUtil.getSession(); try { Query query session.createQuery( from TQuestion q where q.subject.id:sid and q.difficulty:diff order by dbms_random.value()); query.setParameter(sid, subjectId); query.setParameter(diff, difficulty); query.setMaxResults(count); return query.list(); } finally { session.close(); } }注意order by dbms_random.value()是 Oracle 专用的随机排序函数这是 Oracle 函数库里非常高频的一个用法。如果换成 MySQL必须改成order by rand()换成 SQL Server 就得用NEWID()。这类 HQL 里嵌的数据库特有函数是项目从 Oracle 迁移到其他数据库时最容易翻车的地方改代码前先全局搜一下dbms_前缀。参数说明subjectId 是科目 ID对应科目表的主键count 决定这套试卷抽多少题difficulty 是难度系数比如 1 到 5传 0 或者为空表示不限难度。我还见过一些系统在这里加了题型过滤比如多选和判断题分开抽那就是再加一个q.type:type的条件非常好扩展。3.3 自动判分交卷时的事务处理与分数统计交卷判分逻辑通常在 ExamAction 或 PaperAction 里。核心是拿用户提交的答案和题库里的标准答案比对逐题统计得分int score 0; ListExamItem items getSubmitItems(); // 从页面表单解析得到 for (ExamItem item : items) { String userAnswer item.getUserAnswer(); String rightAnswer item.getRightAnswer(); if (userAnswer ! null rightAnswer ! null userAnswer.trim().equalsIgnoreCase(rightAnswer.trim())) { score item.getScore(); // 本题分值写在试卷明细表里 } } ExamRecord record new ExamRecord(); record.setUserId(loginUser.getId()); record.setSubjectId(subjectId); record.setScore(score); examDao.save(record);看完这段你就能理解为什么很多在线考试系统题库里判断题必须存T和F而不是对和错——因为判分代码直接用字符串比较格式不统一就只能得 0 分。多选题也类似A,B,C和ABC在大部分老系统里都会被判为不一致。交卷涉及多张表写入考试记录、试卷明细、可能还有成绩统计表。如果项目没有引入 Spring这段 save 操作所在的方法要么整体成功要么整体失败代码里通常用手动 beginTransaction/commit 包裹我在第 4 章会专门讲。在线考试系统拆成三个子问题就是路由Struts 管、存取Hibernate 管、数据一致性事务管理解了这三条线后面改功能就是往模板里填业务。4. Oracle 数据访问层分页、序列与事务边界的实战写法老在线考试系统里题目表轻轻松松几千上万行成绩列表按学生和科目过滤也有分页需求所以数据访问层必须重点看几个 Oracle 专属写法。4.1 Oracle 分页ROWNUM 三层嵌套与 Hibernate 内置分页MySQL 里写分页是LIMIT ?, ?一句话搞定。Oracle 不支持 LIMIT经典做法是 ROWNUM 三层嵌套SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM T_QUESTION ORDER BY QID ) t WHERE ROWNUM ? -- 先截到结束行 ) WHERE rn ? -- 再扔掉开始行之前的数据为什么必须三层而不是两层是因为 ROWNUM 在 ORDER BY 之前就分配了序号如果先取前 20 行再排序出来的顺序是乱的。内层先做排序中层控制上边界外层削掉下边界这就是 Oracle 分页的标准套路。实际项目里如果不是手写 SQL 而是用 Hibernate直接调Query q session.createQuery(from TQuestion q order by q.id); q.setFirstResult((pageNo - 1) * pageSize); q.setMaxResults(pageSize); ListTQuestion list q.list();Hibernate 的 Oracle 方言会帮你在底层自动生成类似的嵌套 SQLsetFirstResult和setMaxResults分别对应偏移量和每页条数。但有个细节老版本 Hibernate 在setFirstResult(0)配合setMaxResults时生成的 SQL 偶发多取一条表现为最后一页数据异常如果你在维护时遇到这个现象检查一下 Hibernate 小版本或者手写分页 SQL 绕开。数据库分页写法Hibernate 方言OracleROWNUM 三层嵌套Oracle10gDialect / Oracle12cDialectMySQLLIMIT offset, countMySQLDialectSQL ServerOFFSET ... FETCH NEXTSQLServerDialect4.2 主键生成Oracle 序列在 Hibernate 里的配置Oracle 没有 MySQL 的自增列主键全靠序列。对应的 Hibernate 映射有两种落地方式一种是在 hbm.xml 里显式指定 generator另一种是数据库触发器在 insert 前自动取序列值。源码包常见的写法是前者id nameid columnQID typejava.lang.Long generator classsequence param namesequenceSEQ_T_QUESTION/param /generator /id这段配置看id标签就明白name 是 Java 实体的属性名column 是 Oracle 表的列名generator 告诉 Hibernate 去 SEQ_T_QUESTION 这个序列取主键。如果包里的 SQL 脚本没有建序列你可以在 sqlplus 里手动补CREATE SEQUENCE SEQ_T_QUESTION START WITH 1 INCREMENT BY 1 NOCACHE;NOCACHE建议保留虽然牺牲一点性能但能避免 Oracle 实例重启后序列缓存断裂跳过大量 ID 号或者报 ORA-08002。用触发器方案的话核心是BEFORE INSERT触发器里写SELECT SEQ_T_QUESTION.NEXTVAL INTO :NEW.QID FROM DUAL。两条路选一条就行不要又配 generator 又建触发器否则主键会重复取号插入时报主键冲突。4.3 事务边界没有 Spring 时手动管理 Session 的写法这套技术栈如果不带 Spring事务就只能在代码里手动开。一个反面教材是把session.beginTransaction()写在 Action 的 execute() 里页面点一次请求就开一个事务虽然能跑但会导致 Session 泄漏短期看不出问题运行一整天后 Tomcat 内存悄悄涨。我一般建议在 Service 层或者 DAO 外层包成这样Session session HibernateUtil.getSession(); Transaction tx null; try { tx session.beginTransaction(); // 这里放一组 DAO 操作存考试记录、更新成绩、写试卷明细 tx.commit(); } catch (RuntimeException e) { if (tx ! null) { tx.rollback(); } throw e; // 把异常抛给 Struts 全局异常处理 } finally { session.close(); }要点就两个事务必须在你认为的“业务单元”边界开和收异常时 rollback 并重新抛出让上游感知。还有一个更工程化的做法是写一个 OpenSessionInViewFilter让一个 HTTP 请求共享同一个 Session这样 Action 调多个 DAO 时不用各自开关 Session同时把事务边界控制在 Service 层。老系统我倾向只在改造时加这个 Filter运行稳定最重要。5. 避坑Hibernate Session 打不开、监听起不来、中文乱码五个典型故障这类 JSPStrutsHibernateOracle 项目的坑位非常集中前三个发生在环境启动期后两个发生在业务运行期。我把实际维护中碰到的高频故障按现象、原因、解决的顺序列在下面每条都是可以直接翻日志对着查的。5.1 故障一Could not open Hibernate Session for transaction现象Tomcat 启动正常页面也能打开但一点登录或查询按钮就抛异常堆栈最显眼的是Could not open Hibernate Session for transaction; nested exception is org.hibernate.exception.GenericJDBCException: Cannot open connection。原因Hibernate 要拿一个 Oracle 连接却拿不到。细分有三种连接串 SID 写错、Oracle 监听没起来、用户名密码与库不一致。顺带一提驱动太旧连新版数据库会报 ORA-28040属于同类问题。解决先用你最稳妥的工具验证连接再回头检查 hibernate.cfg.xml。我在 sqlplus 里测一次连接sqlplus exam/exam123127.0.0.1:1521:ORCL能进说明监听和连接串没问题问题一定在配置或驱动。打开 hibernate.cfg.xml 重点看这两处property nameconnection.urljdbc:oracle:thin:127.0.0.1:1521:ORCL/property property nameconnection.usernameexam/property property nameconnection.passwordexam123/property property namedialectorg.hibernate.dialect.Oracle10gDialect/property对比 url、账号、密码三项是否与 sqlplus 实测一致。dialect 也要改对Oracle 11g 用 Oracle10gDialect 是兼容的Oracle 12c 以上建议改成 Oracle12cDialect否则某些数据类型读取会走错误分支。5.2 故障二Oracle 监听服务无法启动现象Windows 服务列表里 OracleOraDb11g_home1TNSListener 启动失败系统日志显示退出代码 1命令行lsnrctl status报 TNS-12541 或 TNS-12560。原因监听器配置文件 listener.ora 里的 HOST 写的是机器名而机器名解析不到对应 IP。这个故障在笔记本上改过主机名、切换过网络环境之后特别容易触发因为它从“上次能跑”到“突然起不来”之间没有任何代码变动。解决打开$ORACLE_HOME/network/admin/listener.ora把 HOST 改成 127.0.0.1 或固定 IPLISTENER (DESCRIPTION_LIST (DESCRIPTION (ADDRESS (PROTOCOL TCP)(HOST 127.0.0.1)(PORT 1521)) ) )改完执行lsnrctl reload再lsnrctl start。注意改完 HOST 后之前用机器名生成的 TNS 条目也可能失效最好把 tnsnames.ora 里的 HOST 也同步改成同一个地址。如果机器上之前装过 Oracle 12c 没删干净注册表残留会导致监听服务指向旧目录这是另一种“删不干净”的问题只能先彻底清理再重装代码层面没有后悔药。5.3 故障三JSP 页面中文全部是问号现象登录页和考试页中文变成?????但数据库里查出来的数据正常或者反过来页面正常但存入数据库后变成乱码。原因三层编码不一致。JSP 页面声明编码、Struts 接收请求时的编码过滤器、Oracle 客户端与服务端字符集这三个环节只要有一层不统一中文就会在某一处变形。最常见的是 JSP 用了 GB2312 而数据库是 AL32UTF8或者 web.xml 里没配编码过滤器Tomcat 默认按 ISO-8859-1 解码 POST 参数。解决先统一页面编码每个 JSP 头部写% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%然后在 web.xml 里加编码过滤器filter filter-nameencodingFilter/filter-name filter-classcom.exam.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping最后确认数据库字符集。如果库是 ZHS16GBK你硬要 JSP 用 UTF-8 写数据存入时 Oracle 会做字符集转换常用汉字没问题但特殊符号会丢反过来库是 AL32UTF8JSP 用 GBK一样出问题。最省心的组合是全链路 UTF-8注意 Oracle 客户端那边同时设置NLS_LANGSIMPLIFIED CHINESE_CHINA.AL32UTF8。5.4 故障四Struts 与 Hibernate 的 jar 包版本冲突现象Tomcat 启动扫描 lib 时抛java.lang.NoClassDefFoundError: javassist/util/proxy/MethodFilter或者运行中抛ClassCastException: javassist.util.proxy.ProxyFactory cannot be cast to ...。原因lib 目录同时存在多个 javassist 版本Hibernate 的实体懒加载和 Struts 的字节码增强都会用到 javassist加载器不知道加载哪个。这类老项目里 Hibernate 3.x 自带 javassist 3.x而 Struts 2.3.37 的依赖树里也可能带一份解压后是两份同名 jar。解决删掉旧那份只保留一个版本。判断方法是看WEB-INF/lib下有没有类似javassist-3.12.1.GA.jar和javassist-3.18.1-GA.jar两个文件留新的。动手前先备份我习惯把被删的 jar 挪到项目外一个临时目录而不是直接扔回收站因为有些老类库对 javassist 版本极其敏感删错了还能换回去。5.5 故障五Oracle DATE 字段映射成 Timestamp现象Java 代码里查出来的日期变成2025-01-01 00:00:00和页面上的预期日期不一致或者更新时把时分秒一起写进 Oracle 的 DATE 列导致前后端日期对不上。原因Oracle 的 DATE 类型本身就带时分秒Hibernate 默认把它映射成 Java 的 Timestamp而 Timestamp 在页面展示时会带出时间部分。如果建表时用 DATE 存生日、考试日期这类只关心日期的字段就会出现这种“多余的时间”。解决在 Hibernate 映射里显式指定 typeproperty nameexamDate columnEXAM_DATE typedate /注解写法则是Temporal(TemporalType.DATE)。改完映射后Hibernate 读写这个字段就只保留年月日。如果是维护别人的代码记得全局搜一下Timestamp和java.util.Date混用的地方老项目特别喜欢两种类型交替赋值那才是真正的玄学 Bug 之源。6. 把 War 包打出来传统 JSP 项目的部署验证与后续改造6.1 手工打 war 包没有 IDE 的情况下手工打 war 是最后的手段。war 包本质是一个带固定结构的 zip命令如下mkdir -p exam/WEB-INF/classes cp -r WebRoot/* exam/ cp -r WEB-INF/classes/* exam/WEB-INF/classes/ cp -r WEB-INF/lib exam/WEB-INF/ cd exam jar cvf ../exam.war .war 包目录里 WEB-INF/classes 放编译后的 classWEB-INF/lib 放所有依赖 jar。有个顺序问题classes 目录里的类优先于 lib 里的 jar 加载所以如果 lib 下同时有一个旧版本的类会被 classes 里的新版本覆盖。打 war 前先确认编译输出目录是干净的避免 class 和数据连接配置的残留。6.2 部署验证清单war 放进 Tomcat 的 webapps 后启动并观察 catalina.out。验证不要只点登录按这个清单过一遍模块验证点预期结果登录正确账号密码跳转到主页面session 有用户登录错误密码回到登录页并提示错误考试随机组卷每次进入试卷题目顺序不同考试交卷分数正确写入考试记录表管理端新增试题试题列表出现新记录管理端分页浏览第二页数据正常显示6.3 如果我要往 Spring Boot 迁如果后续要改造我的建议是保留 Oracle 表结构和这套 Hibernate 实体把 Struts 的 Action 替换成 Spring MVC 的 Controller——两者 URL 路由的本质一致struts.xml里的 result 对应 RequestMapping 方法加返回值。迁移时不求一步到位先让登录和查成绩两个接口跑通再逐步替换其他模块。所以现在我每次接到这类老项目的第一件事一定是先核对 ojdbc 和 javassist 的版本再点登录按钮而不是急着读业务代码。希望帮到你。本文还有配套的精品资源点击获取