
简介一套适用于Java Web课程设计、毕业设计或SSH框架学习者的在线考试系统源码基于JSP、Struts、Hibernate和Oracle数据库实现覆盖用户注册登录、选择考试、在线作答、成绩查看等完整流程。压缩包共342个文件约3.1MB包含71个Java源码、71个class编译文件、34个JSP页面、22个XML配置文件、9个依赖包以及SQL数据库脚本等代码层级清晰便于对照理解MVC分层、ORM映射和前端交互。资源还提供Oracle建库脚本、配置文件与部署相关文件帮助读者快速搭建运行环境从Struts控制器的请求分发到Hibernate的数据持久化再到JSP页面的动态展示可直接参考完整实现。已有95人学习浏览适合入门到进阶的Java Web开发者参考学习。1. 这是一套什么系统JSPStrutsHibernateOracle的老牌组合如果你接到的是一份名为“通用的在线考试系统(jspstrutshibernateoracle)130220.zip”的压缩包那这套代码大概率是2013年前后打包的Java Web项目。它的价值不在代码量而在于用当时最主流的SSH架构把在线考试这条业务线完整跑通了题库管理、手动/自动组卷、考生在线答题、客观题自动判分、成绩统计与导出。后端用Struts管请求跳转Hibernate管数据库表映射数据落在Oracle里。这个组合在今天依然能启动、能演示、也能二次开发。适合两类人一类是拿JSP做毕业设计选题的学生另一类是接到学校或培训机构旧系统维护、改造任务的工程师。下面按落地顺序讲清楚这套系统的内部结构、本地启动方式、改造成自己项目的路径以及那些十有八九会踩到的坑。2. 拆开“jspstrutshibernateoracle”四个组件在考试系统里各管什么2.1 Struts从登录到交卷所有请求都走它整套系统的请求流转全都挂在Struts上。标题只写了struts没写1还是2但无论你拿到的是Struts 1.x的ActionServlet还是2.x的StrutsPrepareAndExecuteFilter它在考试系统里的职责是一样的接住浏览器发来的.do请求分发给对应的Action处理完业务后按配置文件里的规则跳转到JSP页面。在一个在线考试系统里你会看到这样一组典型的请求路径/login.do - LoginAction - 校验考生账号密码 /listExam.do - ExamListAction - 查询可参加的考试列表 /startExam.do - ExamAction - 生成试卷并进入答题状态 /submitPaper.do - SubmitAction - 交卷、判分、保存成绩Struts的Action里不直接写JDBC而是调Service层接口。Service层再通过Hibernate操作Oracle表。这里的分层逻辑是JSP只负责展示Action只负责接参和跳转真正的业务规则写在Service里。配置上你会看到两个关键文件。如果是Struts 1.x核心是struts-config.xml里面定义了Action的path、form-bean和forward跳转如果是Struts 2.x核心是struts.xml通过包名和namespace组织Action映射。action path/submitPaper typecom.exam.action.SubmitAction namesubmitForm scoperequest forward namesuccess path/result.jsp/ forward nameerror path/error.jsp/ /action这段配置的意思是交卷请求到达SubmitAction如果返回success就跳成绩页返回error就跳错误页。二次开发时如果你要加一个“考生自行查分”的入口照抄一个Action映射就行。2.2 Hibernate题库表、成绩表和对象之间的映射Hibernate在这套系统里的作用是让Java代码不直接写SQL来操作数据库表而是通过映射文件把表记录变成Java对象。考试系统里最核心的几张表在Hibernate里通常对应这几个实体类Question题库表字段包括题干、选项A到D、正确答案、分值Paper试卷表记录一次考试用了哪些题ExamRecord考试记录表记录哪个考生、哪场考试、答了哪些选项Score成绩表记录总分、客观题得分、交卷时间拿题库实体来说hbm映射文件的片段大概是这样的class namecom.exam.entity.Question tableT_QUESTION id namequestionId typelong column nameQUESTION_ID/ generator classsequence param namesequenceSEQ_QUESTION_ID/param /generator /id property namecontent columnCONTENT typestring/ property nameoptionA columnOPTION_A typestring/ property nameoptionB columnOPTION_B typestring/ property namecorrectAnswer columnCORRECT_ANSWER typestring/ property namescore columnSCORE typeint/ /class注意主键生成方式用的是sequence这是Oracle项目的标准做法。如果你拿到的是MySQL版考试系统这里多半是identity或native。这个区别会在你从Oracle迁移到MySQL时第一个跳出来报错。2.3 Oracle序列、存储过程和分页老系统的数据底盘Oracle在这套系统里不只是个存数据的仓库它在三个地方深度参与了业务逻辑。第一是主键。Oracle没有MySQL的AUTO_INCREMENT所有表主键靠序列生成。你在安装脚本里一定会看到一堆CREATE SEQUENCE语句比如SEQ_QUESTION_ID、SEQ_STUDENT_ID。Hibernate映射里配置的sequence参数指的就是这些序列。第二是存储过程。成绩统计、及格率计算这类批量操作老项目经常写成存储过程因为用Java循环统计太慢。常见做法是写一个存储过程接收考试ID内部用游标遍历考试记录把统计结果写入汇总表。第三是分页。Oracle的分页写法跟MySQL完全不同MySQL用LIMITOracle要用ROWNUM嵌套查询SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM T_QUESTION ORDER BY QUESTION_ID ) t WHERE ROWNUM ? ) WHERE rn ?;Hibernate的Oracle方言OracleDialect会自动生成这种分页SQL。如果你把数据库换成MySQL但忘记改方言翻车是必然的。2.4 “通用”二字体现在表结构而不是代码标题里“通用”两个字才是这个项目的真正卖点。所谓通用不是指代码写得特别抽象而是它的表结构设计成了一套可复用的考试模型。核心是五张表学生表、课程表或考试分类表、题库表、试卷表、成绩表。换一个客户不需要改代码只需要清空题库和成绩表重新导入这个客户自己的题目和考生名单系统就能换一层皮上线。这种设计带来的实际好处是拿它做毕设时你不用从零画ER图因为表结构已经替你验证过一遍了。拿到真实项目里改造时你也不用推翻重来加字段、加表都是顺着原结构的扩展而不是重构。3. 把130220.zip变成能跑的系统导入、建库、改配置、部署3.1 解压与IDE导入先别急着改代码文件名里的括号和加号在命令行里是特殊字符解压前要处理一下。我习惯先复制成不带特殊字符的名字再操作cp 通用的在线考试系统(jspstrutshibernateoracle)130220.zip exam-system.zip unzip exam-system.zip -d exam-system find exam-system -maxdepth 2 -type d | sort解压后先看目录结构重点是三个位置src目录下的Java源码、web/WEB-INF下的配置文件、lib目录下的jar包。如果lib目录里同时有Struts 1.x和2.x的jar或者有多个版本的ojdbc14.jar和ojdbc6.jar趁早清理不然后面部署时会因为jar冲突浪费几个小时。用Eclipse导入时选File - Import - Existing Projects into Workspace。用IDEA的话选Open直接指向解压目录IDEA识别到web/WEB-INF/web.xml会自动弹出“Add as Maven Project”或“Configure Web Facet”的提示。这里不用像新建JSP项目那样从零搭骨架但需要手动确认Project Structure里的Web模块已勾选并且Artifact配置了Web exploded或war。3.2 建Oracle库表结构与初始化数据这类Zip包通常带一个sql目录里面是建表脚本和初始数据脚本。Oracle版本理论上11g、12c、19c都能跑但要注意字符集。如果服务器的字符集是AL32UTF8而你本地库是ZHS16GBK导入脚本时中文会直接乱掉。用sqlplus执行脚本的常见做法sqlplus system/oraclelocalhost:1521/XE exam_schema.sql sqlplus system/oraclelocalhost:1521/XE exam_init_data.sql如果脚本里带了DROP TABLE语句执行时会有确认提示追求省事可以提前在脚本头部加SET ECHO ON和SET FEEDBACK ON查看具体报错。建库阶段最常见的报错有两个一个是ORA-01940: cannot drop a user that is currently connected说明有会话占着这个用户另一个是ORA-00955: name is already used by an existing object说明对象已存在直接忽略不影响后续。Oracle的默认端口是1521SERVICE_NAME可以在tnsnames.ora里查。如果你的本机没装Oracle又不打算装完整版数据库退而求其次可以用Oracle XE版本或者直接用远程开发库但要注意Hibernate方言和驱动版本这直接影响第三章第三节的连接配置。3.3 改jdbc.properties和hibernate.cfg.xml数据库连接从本地到云端都靠它数据库连接信息一般集中在一个jdbc.properties文件里结构大概是jdbc.driveroracle.jdbc.driver.OracleDriver jdbc.urljdbc:oracle:thin:localhost:1521:XE jdbc.usernameexam_user jdbc.passwordexam123 jdbc.maxActive20 jdbc.maxIdle10三个必改项是URL、用户名、密码。这里有个血泪经验jdbc:oracle:thin:后面的写法有两种localhost:1521:XE是老式写法指SIDlocalhost:1521/XE是新式写法指SERVICE_NAME。如果你的Oracle是用12c以后的容器数据库搭的用冒号写法容易连不上改成斜杠写法通常就好了。Hibernate侧的hibernate.cfg.xml里除了数据库连接还要关注方言property namedialectorg.hibernate.dialect.OracleDialect/property property nameshow_sqltrue/property property namehbm2ddl.autoupdate/propertyhbm2ddl.auto如果设成updateHibernate启动时会自动比对映射文件和数据库表结构缺字段自动加。这种便利也带着风险数据库字段类型和映射不一致时它会尝试改表结构某些情况下直接报错。建议第一次启动时设成update但表结构稳定后改成none。3.4 部署到Tomcat传统JSP项目打包war的标准姿势老式JSP项目部署不依赖Maven直接打war包丢进Tomcat即可。如果你的IDE里配好了Artifact用IDEA的Build - Build Artifacts - Rebuild生成war。命令行手动打包也行jar -cvf exam-system.war .然后把这个war复制到Tomcat的webapps目录下启动Tomcatcp exam-system.war /opt/tomcat/webapps/ /opt/tomcat/bin/startup.sh tail -f /opt/tomcat/logs/catalina.out访问路径通常是http://localhost:8080/exam-system/。起不来就看日志典型日志错误是ClassNotFoundException或NoClassDefFoundError这多半是lib目录下jar缺失或冲突。这里特别提醒一句nginx支持JSP吗不支持JSP必须交给Tomcat这类Servlet容器处理nginx只能反代静态资源或转发动态请求别指望把war直接丢在nginx下能跑。Tomcat版本选择上这套SSH架构的老项目建议用Tomcat 8或9不要一上来用Tomcat 10或11。Tomcat 10把javax.*的Servlet API换成了jakarta.*老项目的代码和依赖jar全是按javax编译的直接跑会大面积报错。4. 改造成自己的系统题库、组卷、成绩统计的三条二开路线4.1 扩展题库字段加难度、章节、知识点拿到这套系统后第一个想改的往往是题库。原表结构通常只有题干、选项、答案、分值这几个字段但实际考试需求会要求按难度、章节、知识点来筛选和组卷。这时候要做的是一次“加字段”手术分三步走。第一步给Oracle表加字段ALTER TABLE T_QUESTION ADD ( DIFFICULTY NUMBER(1) DEFAULT 3, CHAPTER_ID NUMBER(10), KNOWLEDGE_POINT VARCHAR2(100) );第二步改Hibernate映射文件Question.hbm.xml加上对应propertyproperty namedifficulty columnDIFFICULTY typeint/ property namechapterId columnCHAPTER_ID typelong/ property nameknowledgePoint columnKNOWLEDGE_POINT typestring/第三步在题库管理JSP页面上加对应的表单控件。这一步最容易翻车的是hbm2ddl.auto没开update导致加了Java属性但启动报错说不存在字段。我的习惯是改表结构前先备份原表改完用DESC T_QUESTION确认字段都在再改映射文件。4.2 改自动组卷规则随机抽题与防重复自动组卷是这套系统的核心卖点也是二开需求量最大的地方。原始逻辑通常是在Service层里写一个组卷方法根据试卷模板配置的题量和分值从题库里筛题并写入试卷明细表。如果你想实现“从难度为3的选择题里随机抽10道”核心SQL用Oracle的随机排序来实现SELECT * FROM ( SELECT * FROM T_QUESTION WHERE TYPE 1 AND DIFFICULTY 3 ORDER BY DBMS_RANDOM.VALUE() ) WHERE ROWNUM 10;对应到Java Service层常见做法是写一个generatePaper(ExamTemplate template)方法循环每个题型模板执行上面的查询把返回的题目列表逐个插入试卷明细表。这里有一个隐藏坑DBMS_RANDOM.VALUE()在数据量很大时排序会比较慢但考试系统的题库一般几千到几万题性能可以接受。另一个坑是抽题时如果忘记排除已经出过的题目同一场考试里会出现重复题。常见解决思路是在抽题SQL里加一个NOT IN排除当前试卷已选题目用到一个临时表或会话变量。4.3 成绩统计用Oracle存储过程干脏活成绩统计在在线考试系统里属于“晚做必须返工”的功能。前期你也许只统计总分但客户一定会接着要平均分、及格率、分数段分布。与其在Java里循环统计不如直接写一个Oracle存储过程来干这个脏活CREATE OR REPLACE PROCEDURE sp_exam_statistics ( p_exam_id IN NUMBER, p_avg_score OUT NUMBER, p_pass_rate OUT NUMBER ) AS v_total NUMBER; v_pass NUMBER; BEGIN SELECT COUNT(*), NVL(AVG(SCORE), 0) INTO v_total, p_avg_score FROM T_SCORE WHERE EXAM_ID p_exam_id; SELECT COUNT(*) INTO v_pass FROM T_SCORE WHERE EXAM_ID p_exam_id AND SCORE 60; IF v_total 0 THEN p_pass_rate : v_pass / v_total * 100; ELSE p_pass_rate : 0; END IF; END;写好后在Java端用JDBC的CallableStatement调用即可。这里要注意存储过程的OUT参数必须注册类型常见错误是Invalid column type对应解决办法是在调用前写明cstmt.registerOutParameter(2, Types.NUMERIC)。另外这套老系统如果用的是Hibernate 3.x它本身不擅长执行存储过程从数据库连接里拿一个原生Connection来做这种操作是更稳的姿势。5. 部署与运行避坑五个高频问题与对应解法5.1 Could not open hibernate session for transaction现象系统跑着跑着操作某个功能时页面直接报错日志里出现完整的异常描述Could not open Hibernate Session for transaction; nested exception is org.hibernate.exception.GenericJDBCException: Cannot open connection。原因Hibernate在每次事务开始前要从连接池拿一个Session拿不到就抛这个异常。触发场景有两种一是数据库连接池的最大连接数被占满典型表现是考试交卷高峰期几十个考生同时写成绩时出现二是hibernate.cfg.xml里的连接配置本身连不上比如数据库服务重启了、密码被改了。解决先确认数据库还活着用sqlplus手动登录一遍。数据库没问题的话把连接池的最大连接数调大。如果在Spring配置里用的是BasicDataSource调整这两个参数property namemaxActive value50/ property namemaxWait value10000/maxWait是拿不到连接时的最长等待毫秒数设成10000表示10秒内拿不到连接直接失败避免线程无限挂起。5.2 Oracle驱动和JDK版本打架现象Tomcat启动时报java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver或者启动不报错但首次访问数据库时抛AbstractMethodError。原因lib目录下的ojdbc版本和JDK版本不匹配。老项目经常带着ojdbc14.jar这是JDK 1.4时代的产物放在JDK 8环境下部分新特性用不了。还有种情况是lib里同时存在多个ojdbc版本比如ojdbc14.jar和ojdbc6.jar同时在类加载器加载了旧版本。解决把lib目录下的老驱动全部移除只保留一个与JDK匹配的驱动。JDK 8用ojdbc8.jarJDK 11以上用ojdbc10.jar或ojdbc11.jar。Oracle 19c的驱动完全兼容老库数据库版本是11g或12c也没问题。5.3 中文乱码的三条线现象页面上显示的考生姓名是问号或者往题库里存中文后查出来是乱码。原因乱码有三个可能来源。第一JSP页面本身没声明UTF-8编码第二表单POST提交时请求编码不是UTF-8第三Oracle数据库字符集不是AL32UTF8。三条线必须全通只改其中一条没用。解决JSP页面头部确认有这行% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%请求编码在web.xml里加一个过滤器filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter数据库层面用SQL查一下当前字符集SELECT VALUE FROM NLS_DATABASE_PARAMETERS WHERE PARAMETER NLS_CHARACTERSET;如果不是AL32UTF8且数据库里已经存了业务数据强行改字符集风险很大。这种情况下最稳的方案是导出数据、重建库、再导入。如果只是本地开发环境直接重建库最省事。5.4 Struts 2.3.37的Jar冲突现象Tomcat启动时日志报Unable to load configuration或ClassNotFound有时候报BeanUtils相关的错误。原因如果你的项目用的是Struts 2.3.37lib目录里同时出现了xwork-core-2.3.x.jar和struts2-core-2.3.x.jar或者混入了更高版本的commons-lang3就会因jar冲突导致配置文件解析失败。Struts 2和xwork是强绑定关系版本必须严格一一对应。解决检查lib目录删掉多余的xwork-core和重复的Struts核心jar。Struts 2.3.37对应的是xwork-core-2.3.37.jar库里只保留这一套。如果你不确认对应关系直接打开struts2-core的jar里的META-INF/MANIFEST.MF看它依赖的xwork版本。用的时候也注意别把高版本Struts的jar混进来老系统里Action类的写法在2.5版本下也会报不兼容错误。5.5 Tomcat版本太高JSP直接编译失败现象把war部署到Tomcat 10或11后访问首页返回404或500catalina日志里出现org.apache.jasper.JasperException: Unable to compile class for JSP或The type javax.servlet.http.HttpServletRequest cannot be resolved。原因Tomcat 10开始把Java EE的javax.*迁移到了jakarta.*老项目所有JSP页面和依赖的jar都是按javax写的IDE里能编译不代表Tomcat能跑。Spring 4.x和Struts 2.x的旧版本都无法在Tomcat 10下正常工作。解决不折腾老代码直接用Tomcat 8.5或9.0跑这套系统。如果你必须用Tomcat 10那不只是换jar所有Java代码里的import javax.servlet.*要改成import jakarta.servlet.*JSP里的% page importjavax.servlet.*%也要全改。对一个接手的zip包来说这个改造量比换Tomcat版本大得多不值得。6. 考前并发验证与Oracle分页优化让老系统扛住整场考试把系统跑通只是开始真正要面对的是“整场考试同时在线”的场景。这里分享一个考前必做的验证技巧和一处必做的性能优化。考前验证用JMeter模拟并发。不要只测登录要把完整链路串起来登录管理系统获取考试列表进入考试页面然后提交答卷。线程组设置50个并发用户每个用户循环2次。重点关注两个指标接口响应时间在交卷阶段是否出现断崖式上涨Oracle数据库的会话数是否打满。压测期间同时开着catalina.out和Oracle监听日志如果出现ORA-12520: TNS:listener could not find available handler说明数据库连接数被耗尽了这时把Tomcat的数据库连接池maxActive从20调到100并把maxWait设置成5000。另外如果压测时有大量线程阻塞在数据库连接上别急着加连接数先看是不是Hibernate的SessionFactory配置里漏了连接释放设置常见做法是在hibernate.cfg.xml里关闭current_session_context_class的线程绑定问题换成每事务一个Session的模式。分页优化要动Hibernate方言。老系统默认用OracleDialect分页查询生成的是三层嵌套ROWNUM语句在百万级成绩表上做翻页会明显变慢。一个稳妥的优化是用行号分页配合查询条件把HQL里的分页逻辑改成本地SQLSELECT * FROM ( SELECT ROWNUM rn, t.* FROM ( SELECT SCORE_ID, EXAM_ID, STUDENT_ID, SCORE FROM T_SCORE WHERE EXAM_ID ? ORDER BY SCORE DESC ) t WHERE ROWNUM ? ) WHERE rn ?;执行计划里确认走的是INDEX RANGE SCAN而不是FULL TABLE SCAN如果走了全表扫描就要给EXAM_ID建索引。老考试系统的瓶颈往往不在代码本身而在数据库访问路径上。最后一个实操习惯每次修改配置文件或JSP后重启Tomcat前先备份原文件。这个zip包在没有版本管理的环境里经常被改得面目全非遇到改坏的情况一份.bak文件就是后悔药。我现在的习惯是拿到这类zip包先看lib目录和hibernate.cfg.xml再看web.xml配置比代码更能暴露一个项目能不能跑。这套SSH考试系统虽然老但它把JSP、Struts、Hibernate、Oracle四样东西串在一起的思路放到今天的Spring Boot项目里依然成立。希望帮到你。本文还有配套的精品资源点击获取