简介:一套基于jsp、Struts、Hibernate与Oracle构建的通用在线考试系统源码,面向Java Web初中级开发者及毕业设计学生,可反复研读SSH框架整合、MVC分层与真实业务逻辑。压缩包共342个文件,涵盖71个Java源文件、34个JSP页面、22个XML配置文件以及class编译文件、SQL脚本、gif图片、项目部署相关文件,包体仅3.1MB,目录结构清晰,便于按模块检索。目前已有95人学习下载。资源完整呈现用户注册登录、考试选题、在线作答、自动评卷与成绩查询的闭环流程,可对照Struts Action、Hibernate映射文件与JSP页面,理解前端展示、控制器调度、业务处理和Oracle数据库之间的协作;同时,包含的class文件与配置文件对排查环境搭建、部署发布中的常见问题也有参考价值,适合作为课程设计、项目实战或快速搭建同类在线考试系统的蓝本。
1. 一套 jsp+struts+hibernate+oracle 的在线考试系统,为什么今天还有人翻出来部署
看到“通用的在线考试系统(jsp+struts+hibernate+oracle)130220.zip”这个标题,先别急着下“过时毕设”的结论。这套技术栈确实是 2010 年前后的标准组合,但到今天,不少高校的实验课、企业内部培训、职业资格模拟考试,跑的依然是这样结构的系统——JSP 负责页面渲染,Struts 管请求分发,Hibernate 做 ORM,Oracle 存数据。它的价值在于:结构清晰、分层固定、改造成本低,等于给你一副能直接用的骨架,你要做的是把题库和试卷规则填进去。这篇笔记就按“拆骨架、搭数据库、部署、踩坑、改造”的顺序,把这条链路完整走一遍。适合接手老项目、拿来做毕设二次开发、或者想快速搭一个内部考试平台的人。
2. 先看懂架构:Struts 管流程、Hibernate 管映射、Oracle 管数据
2.1 三层结构:JSP 页面、Action、映射文件各自的边界在哪
这类在线考试系统最容易让新手懵的地方,是它不像现在的 Spring Boot 项目那样“一个注解全搞定”。你打开 zip 解压之后,会看到一堆.java、.jsp、.xml,得先分清谁负责什么。
常见做法是这样的:webroot(或WebContent)下面全是 JSP 页面,负责接收请求、展示结果;Java 代码里按 Struts 的习惯分成action、form、service、dao四层;Hibernate 的映射文件(.hbm.xml)放在实体类旁边,描述 Java 对象和 Oracle 表之间的对应关系。页面里几乎看不到 SQL,查数据都走 DAO。
这里要特别说清楚一个边界:Struts 的 Action 只做“路由”和“表单收集”,它不写业务逻辑。比如考生点“开始考试”,请求会先到examAction.do?method=start,Struts 根据struts-config.xml里的配置找到对应 Action,Action 再把请求转发给 JSP 页面。至于“这个考生能不能考这门课”“试卷怎么组”,那是在 Service 层里做的,Action 拿到结果后只负责跳转。
我一般拿到这类项目,第一件事不是看代码,而是画一张表,把每个 JSP 页面和它对应的 Action 路径列出来。通用在线考试系统里,核心页面通常就这几个:登录页、考生首页、在线考试页、成绩查询页、后台题库管理页、试卷管理页。把这六个页面和 Action 的对应关系理清楚,整个系统的骨架就出来了。剩下那些配置文件和.java文件,都能对号入座。
2.2 一次交卷请求走过的完整链路:从浏览器表单到 Oracle 事务
单纯看配置文件很难建立体感,我们拿“交卷”这个动作走一遍全链路。这也是整张试卷里最容易出 Bug 的地方,值得单独拆。
第一步,考生在 JSP 页面里点“交卷”,浏览器把整个表单 POST 给submitExam.do。Struts 的RequestProcessor会按struts-config.xml里的<action-mappings>找到对应的 Action 类。
第二步,Struts 自动把表单字段塞进 ActionForm 里。考试系统里这个 Form 通常是一个数组结构,每条记录是“题目 ID + 考生的答案”。这里有个老项目常见的坑:如果 Form 里用的是数组而不是 List,提交的题目数量一多,会有字段截断,这属于 Struts 1 的经典问题,后面避坑章节细说。
第三步,Action 调 Service,Service 调 DAO,DAO 里是 Hibernate 的Session。交卷的核心事务是“把答案逐条插入答题表,同时更新考试记录表的交卷时间”。注意,这段逻辑在 Hibernate 里要放在一个事务里,否则会出现“答案存了但考试记录没更新”的半截状态,考生成绩就丢了。
第四步,事务完成后,Action 根据 Service 返回的结果(比如“客观题得分”“是否已交卷”)转发到成绩页面。这里用的是forward而不是redirect,所以浏览器地址栏不会变,刷新会重复提交——这是老项目的通病。
Oracle 在里面的角色是数据存储和计算。客观题得分一般不在 Java 里算,而是交卷时调一个存储过程,把考生答案和标准答案做比对,批量更新分数。这是这套架构里比较聪明的地方:把计算下推到数据库,避免在 Java 层做循环比对,也方便在 Oracle 里直接出统计报表。
理解这条链路之后再去看struts-config.xml里的<action>配置和 Hibernate 的<transaction>配置,就不是死记配置了,而是知道每一行在支撑哪个环节。
3. 把 Oracle 数据库跑起来:建库、建用户、导入 130220 里的脚本
3.1 建表空间、创建用户、赋权的三段式操作
这套系统既然用的是 Oracle,你第一步就不是“建库”,而是“建用户”。Oracle 的逻辑和 MySQL 不一样:一个实例(数据库)下挂多个用户,每个用户有独立的 schema。拿到 zip 之后,压缩包里一般会带一个.sql脚本,可能是建表语句,也可能是全量导出文件。不管是哪种,先建一个干净的 Oracle 用户,避免往system用户里塞业务表。
以本地开发环境为例,我用 SQL*Plus 或者 Toad for Oracle 执行下面这段:
-- 创建表空间,数据文件路径按你本机 Oracle 安装目录调整 CREATE TABLESPACE exam_data DATAFILE 'D:\oracle\oradata\ORCL\exam01.dbf' SIZE 500M AUTOEXTEND ON NEXT 50M MAXSIZE 2G; -- 创建业务用户 CREATE USER exam IDENTIFIED BY exam_pass DEFAULT TABLESPACE exam_data QUOTA UNLIMITED ON exam_data; -- 给足业务所需的系统权限 GRANT CONNECT, RESOURCE TO exam; GRANT CREATE SESSION TO exam;这段 SQL 的逻辑是:先给数据找个物理存放位置(表空间),再创建业务用户,最后授权。CONNECT让用户能登录,RESOURCE让用户能建表、建索引、写存储过程。很多新手栽在只给了CONNECT,结果导入脚本时报ORA-01031: insufficient privileges,就是RESOURCE没给。
如果你拿到的脚本里有CREATE TRIGGER或者CREATE VIEW的语句,RESOURCE可能还不够,需要单独补GRANT CREATE TRIGGER TO exam;。先跑一遍导入,缺什么权限再补什么,别一次性把所有 DBA 权限都给了,后患无穷。
3.2 导入数据库脚本:分清 sql 文件和 dmp 文件,字符集是最大的坑
zip 的日期编码 130220 暗示这是 2013 年 2 月 20 日打包的,那个年代的 Oracle 大多还是 10g 或 11g,字符集最常见的是ZHS16GBK。你今天如果装的是 Oracle 19c,默认字符集是AL32UTF8,直接导入当年的数据,中文题目十有八九会变成乱码或者直接报ORA-12899(值太大)。
先说两种导入方式的差异:
- 如果压缩包里是
.sql文本脚本,用SQL*Plus执行即可。注意要先登录目标用户,再@脚本路径。 - 如果是
.dmp文件,要用imp工具导入,而且必须分清 dmp 文件是用exp导出的还是expdp导出的,两者不能混用。
假设是文本脚本,我用这种方式导入:
# 进入 sqlplus,登录 exam 用户 sqlplus exam/exam_pass@//localhost:1521/ORCL # 在 sqlplus 里执行脚本,注意脚本里不能有 connect 别的用户的语句 @D:\exam_project\db\exam_init.sql导入日志里如果出现ORA-00922或ORA-00904,通常是脚本里带了版本不兼容的语法,比如 10g 的CONNECT BY写法在 19c 里虽然能用,但和PIVOT混用时会有歧义。这属于老脚本的老实账,不要试图手工改几十处,优先考虑在新库里接受旧语法,或者单独把报错的那几句抽出来手工执行。
字符集这块,我的血泪经验是:导入前先查两边数据库字符集。不匹配时优先改的是导入端的会话字符集,而不是重建数据库。在导入前执行:
ALTER SESSION SET NLS_CHARACTERSET = ZHS16GBK;注意这个命令在 12c 以后版本里不一定生效,更稳妥的做法是用环境变量NLS_LANG控制客户端字符集,再执行导入。字符集问题不解决,后面 JSP 页面上显示的中文全是问号,你会误以为是代码编码问题,白排查半天。
3.3 Oracle 分页用 rownum:题库列表和成绩查询的翻页怎么写
在线考试系统里有一类页面永远存在:题库列表、考生列表、成绩列表。这类页面在 JSP 里要做分页,而 Oracle 的分页写法跟 MySQL 的LIMIT完全不同,这是整套系统里 Oracle 特色最浓的地方。
老项目里你大概率会看到这种写法:
SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT q_id, q_content, q_type FROM question WHERE q_type = '1' ORDER BY q_id ) t WHERE ROWNUM <= 20 ) WHERE rn > 10;逻辑分三层:最内层是业务查询,带着排序条件;中间层给结果集编上ROWNUM序号,同时限制最大行数;最外层用rn过滤起始行。为什么要套三层?因为ROWNUM是在结果集生成时顺序分配的,如果你在最外层直接写WHERE ROWNUM BETWEEN 10 AND 20,Oracle 永远不会返回数据——ROWNUM只在分配时判断,第一条记录的 ROWNUM 是 1,BETWEEN 10 AND 20永远不成立。
参数上要注意两点。一是排序必须放最内层,否则分页后的顺序是乱的;二是如果表里有自增主键(用序列实现的),ORDER BY q_id才有稳定意义。这套三层分页写法性能不算好,但数据量在十万以内完全够用。你在改造时可以换成FETCH FIRST 20 ROWS ONLY(Oracle 12c 以后支持),但老系统保持原样最稳妥,别为了炫技把 SQL 改出兼容性问题。
4. 在 Tomcat 里部署:struts-config 和 hibernate.cfg.xml 逐个过
4.1 struts-config.xml:考试流程的 Action 映射和表单校验
这套系统的“路由表”全在struts-config.xml里。打开这个文件,你会看到一堆<action>节点,每个节点就是一个入口。在线考试系统里,最核心的几条映射大概是这样的结构:
<struts-config> <form-beans> <form-bean name="loginForm" type="com.exam.form.LoginForm"/> <form-bean name="examForm" type="com.exam.form.ExamForm"/> </form-beans> <action-mappings> <action path="/login" type="com.exam.action.LoginAction" name="loginForm" scope="request" input="/login.jsp"> <forward name="success" path="/student/home.jsp"/> <forward name="failed" path="/login.jsp?error=1"/> </action> <action path="/submitExam" type="com.exam.action.SubmitExamAction" name="examForm" scope="session" parameter="method"> <forward name="success" path="/student/result.jsp"/> </action> </action-mappings> </struts-config>path是访问路径,type是处理类,name引用上面定义的 Form,scope决定 Form 存活范围。这里有个部署时的关键点:登录页的 Form 应该用request,交卷的 Form 必须用session。为什么?因为交卷表单里有几十道题的答案,如果放在request里,提交过程中 Action 转发到 JSP 页面时数据就丢了;而登录表单放session里会残留上一个用户的输入,换个考生登录会看到上一个人的账号。
parameter="method"是 Struts 1 的DispatchAction用法,同一个 Action 里可以根据请求参数method=start|submit|cancel分发到不同方法。这个写法省类,但调试时有个坑:方法名拼错不报编译错,运行时报NoSuchMethodException,还很难看出是哪个方法拼错了。我一般会先看 JSP 里的表单提交地址,再回到配置里核对method参数值。
4.2 hibernate.cfg.xml:方言、连接池,附送“hibernate 还有人用吗”的答案
Hibernate 在这套系统里的存在感,全在hibernate.cfg.xml和一堆*.hbm.xml里。老项目的配置通常长这样:
<hibernate-configuration> <session-factory> <property name="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</property> <property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver</property> <property name="hibernate.connection.url">jdbc:oracle:thin:@localhost:1521:ORCL</property> <property name="hibernate.connection.username">exam</property> <property name="hibernate.connection.password">exam_pass</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.hbm2ddl.auto">update</property> <property name="hibernate.c3p0.min_size">5</property> <property name="hibernate.c3p0.max_size">20</property> </session-factory> </hibernate-configuration>逐个说参数。dialect决定了 Hibernate 生成 SQL 时用的方言,这直接影响分页语句的生成。如果你用Oracle10gDialect连的是 19c 数据库,功能上没问题,但生成的ROWNUM分页会比 12c 的新语法慢一点;反向操作更危险——用 12c 方言连 10g 数据库,可能直接不兼容。最稳的做法是装了什么版本就选对应的方言。
show_sql在开发期必须开,上线前关掉,不然日志会被刷爆。hbm2ddl.auto是双刃剑:设成update会自动加表加字段,省事,但生产环境建议改成none,防止 Hibernate 启动时动表结构。这个参数曾让我在半夜翻车——Hibernate 自作主张给一张大表加了字段,第二天业务查询全走全表扫描。
至于“hibernate 还有人用吗”这个搜索热词,直接回答:生产上新项目确实少了,但存量系统里遍地都是。你接手这类在线考试系统,Hibernate 的知识不是可选项,是必选项。而且 Hibernate 的 ORM 思想没白学——现在 MyBatis-Plus 的实体注解、JPA 的映射关系,全是同一套概念。会用hbm.xml的人,转过去看 JPA 注解,半小时就上手。
连接池这里特别提醒:老项目用 c3p0 很常见,但 c3p0 和 Oracle 19c 的 JDBC 驱动偶发连接不释放。如果压测时发现连接数一直涨,先检查 c3p0 的max_size和timeout,不行就换 HikariCP——改造工作量不大,只改配置类,不动 DAO 代码。
4.3 部署时的路径与驱动:把项目变成可访问的 webapp
前面配置都改好,最后一步是把项目放进 Tomcat。常见行为是直接把解压后的目录拷到webapps下,启动,然后看到一个 404 或 500。
部署层级上要注意:.class文件必须在WEB-INF/classes下,.jar必须在WEB-INF/lib下,.hbm.xml如果放在WEB-INF/classes下会被加载,放在WEB-INF/lib里不会被加载。Oracle 的 JDBC 驱动(ojdbc14.jar或ojdbc6.jar)放哪都有讲究:如果 Tomcat 和项目里都有,会产生两个oracle.jdbc.driver.OracleDriver类,报ClassNotFoundException或奇怪的连接超时。我的习惯是把驱动只放在项目WEB-INF/lib下,不碰 Tomcat 的lib。
部署完访问http://localhost:8080/exam/login.jsp,第一次启动看到页面卡住别慌,先看日志里有没有ORA-12505(监听找不到实例)或者Listener refused the connection。这两个错七成是连接串里的服务名写错了,jdbc:oracle:thin:@localhost:1521:ORCL里的ORCL是服务名,不是实例名,大小写和 SID 都得和tnsnames.ora对得上。
5. 避坑清单:老系统部署的五个高频翻车点
5.1 现象:启动就报java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver
原因只有一个——驱动 jar 不在 classpath 里。但奇怪的是,很多人明明把 ojdbc 的 jar 扔进WEB-INF/lib了,还是报错。这时候要去查是不是 jar 包版本太老,ojdbc14.jar对应 Java 1.4,在 JDK 8 以上环境里加载会有兼容问题。另外,如果项目里同时有classes目录下的.class文件和 jar 包两份驱动,类加载器会随机选一个,就会“有时候能启动、有时候启动不了”的玄学现象。
解决:删掉多余的一份,保留WEB-INF/lib下最新的 ojdbc6 或 ojdbc8,重启 Tomcat。判断标准很简单——ojdbc8.jar支持 Oracle 12c 到 19c,也向下兼容 11g,是这个老架构里省心程度最高的版本。
5.2 现象:登录进去之后,页面上全是 “???”,或者中文变成长条的\uXXXX
原因分两层:要么是 JSP 页面本身编码和 Tomcat 解码不一致,要么是数据库里存的中文已经乱掉。老项目的 JSP 页面大多写着一句pageEncoding="GBK",而 Tomcat 默认用 UTF-8 解码 POST 参数,两边对不上,数据进数据库前就坏了。更隐蔽的是 Oracle 客户端NLS_LANG环境变量没设,Sql*Plus 或 Toad 查出来正常,一到 Java 程序连接就乱。
解决:第一层,把 JSP 顶部指令统一改成pageEncoding="UTF-8",并且在代码里对 POST 参数做一次编码转换。第二层,查数据库实际存的字符:SELECT DUMP(q_content) FROM question WHERE ROWNUM=1,如果返回的字节序列不是目标字符集,说明导入阶段就乱了,只能回源数据重新导入。这环节没有后悔药,字符集错了后期改比重建还麻烦。
5.3 现象:并发考试时,部分考生交卷报ORA-01428: argument is out of range
这个错看着像“数字越界”,实际经常藏在存储过程里——交卷时算分,用TO_NUMBER强转一个带有逗号或空格的答案字段,比如考生填了“3,5”,TO_NUMBER直接炸。另一个常见位置是SUBSTR或INSTR的参数用了负数或超长值。
解决:找到报错的存储过程,在入参处做清洗:
v_ans := REPLACE(TRIM(v_answer), ',', ''); v_num := TO_NUMBER(v_ans DEFAULT 0 ON CONVERSION ERROR);DEFAULT 0 ON CONVERSION ERROR是 12c 的新语法,老版本不支持;老版本的替代方案是先正则判断再转换。这个坑的难点不在修改,在于定位——错误发生在存储过程内部,Java 日志里只看到 ORA-01428,没有具体行号,得开DBMS_OUTPUT或临时加日志表才能定位。我一般先在存储过程入口加一段DBMS_OUTPUT.PUT_LINE打印入参,复现一次就清楚了。
5.4 现象:考生点“查看成绩”正常,点“查看答卷详情”报LazyInitializationException
这是 Hibernate 老项目的标志性报错。原因是页面要显示答卷里每道题的题干和答案,而ExamRecord的answerSet是延迟加载的,Hibernate 的Session在 Action 层已经关闭,JSP 里一访问record.getAnswers()就炸了。
解决:方案有三条。最省事的是在映射文件里把<set lazy="false">,让关联对象立即加载,牺牲一点性能;最规范的改法是加OpenSessionInView过滤器,让 Session 在页面渲染完再关闭;折中方案是在 Service 层把需要的数据先查出来,封装成一个值对象传到页面。我的建议是第三个,因为老项目的 JSP 页面和数据库字段耦合很深,用一个 VO 把页面想要的字段直接塞满,后续改造也不怕。
5.5 现象:Oracle 监听服务无法启动,连接报ORA-12541: TNS:no listener
这个问题很多不是数据库本身坏了,而是机器重启后监听没起来,或者listener.ora里写的主机名解析不到。Windows 上最常见的原因是 Oracle 服务里监听服务被禁用了,Linux 上则经常是/etc/hosts里主机名映射到了 127.0.0.1,而数据库配置的是真实 IP。
解决:先用lsnrctl status看监听状态,起不来就看listener.ora。Windows 下到“服务”里找到OracleOraDb11g_home1TNSListener,确认不是禁用状态,手动启动;Linux 下检查hosts文件,把主机名和 IP 的映射修好,再lsnrctl start。如果是监听日志把磁盘塞满导致的,清理$ORACLE_HOME/network/log下的listener.log再重启。这个问题解决后多等十几秒再连——监听刚启动时服务注册还没完成,立刻连接会报ORA-12514,这算是另一个小坑。
6. 跑通之后的三步验证:先压测,再看日志,最后动业务逻辑
整套系统部署完、能登录、能考试、能出成绩,这只算完成了一半。我接手这类老项目后,一定会按下面三步做验证,缺一步都不敢丢给用户用。
第一步,压测交卷接口。用 JMeter 模拟 50 个并发用户同时交卷,重点看两个指标:事务成功率是不是 100%,以及 Oracle 的会话数有没有飙升。老项目最容易在这个环节暴露连接池配置太小——c3p0max_size设成 20,50 个并发直接把连接池打满,后面的考生排队排到超时。压测时如果报ORA-01000: maximum open cursors exceeded,不是连接池问题,是代码里Statement没关,Hibernate 的Session用完没释放。这种问题在单用户测试时永远测不出来,必须并发才能暴露。
第二步,打开 Hibernate 的show_sql和慢查询日志,跑一遍完整的“组卷-在线考试-交卷-查成绩”流程。重点看有没有 N+1 查询——打开成绩单页面时,Hibernate 把几十道题逐条查了一遍,几十条 SQL 打在日志里,一眼就能看出来。遇到这种,就在对应的hbm.xml里调整<batch-size>,或者改用JOIN FETCH。
第三步,动业务逻辑。把随机抽题改成可配置的——从“按题库随机抽 50 道”改成“按知识点比例抽题”。这种改造不该动 JSP 和 Action,只改 Service 里的组卷算法。常见做法是写一个新的generatePaperByStrategy方法,用 Oracle 的存储过程计算好各知识点抽题数,再查题库。改完用同一套压测脚本回归,确认组卷耗时没明显上升。
这套流程走完,你手里那套 130220 的旧系统,就不再是一个“跑不跑得起来靠运气”的黑匣子,而是一套你能说清每个环节、能定位问题、能改业务逻辑的生产系统。技术栈虽然老,但三层架构的思路、Oracle 的 SQL 功底、Hibernate 的映射概念,放到今天的项目里一样能用。我自己经手过不止一个这类老系统的迁移,最深的体会是:别急着推翻重写,先把旧系统的边界摸清楚,让它在原架构上稳定跑起来,再谈改造。希望帮到你。
本文还有配套的精品资源,点击获取