SSM在线考试系统实战:三层架构、动态SQL与事务控制
2026/9/10 9:46:25 网站建设 项目流程

简介:本资源是一套完整的Java毕业设计项目——基于SSM框架的在线考试系统,面向计算机专业本科生及Java初学者,解决教育场景中考试组织、试卷管理与成绩分析等核心需求。压缩包共7830个文件,涵盖1986个JavaScript前端交互脚本、400个JSP页面、186个JAR依赖库、81个Java业务逻辑类、232个图片资源及340个JSON配置数据,完整呈现B/S架构下前后端协同开发实践;包体大小为155.29MB,结构清晰,含源码、数据库SQL、详细说明文档与演示视频。已有2346人下载学习,读者可直接部署运行,深入理解SSM整合开发、RBAC权限控制(管理员/教师/学生三级角色)、动态组卷、在线阅卷与成绩可视化等关键功能实现细节,是毕业设计选题、课程实训与求职项目复现的高价值参考范例。

1. 这不是又一个“学生交完就扔”的Java毕设——它跑得通、改得动、能上线,SSM三层结构里藏着真实考试业务的闭环逻辑

很多同学拿到“基于SSM的在线考试系统”源码,解压后第一反应是:菜单能点开,登录能进去,但一到“组卷”就卡住,一查数据库发现exam_paper表空着,再翻PaperService.java,发现generatePaper()方法里调用了未实现的RandomQuestionSelector接口——这根本不是完整交付物,而是半成品教学模板。而本项目不同:它包含可直接运行的MySQL建库脚本(含sys_userexam_paperexam_record等12张表)、SSM各层完整映射(Controller层有ExamController处理试卷提交与成绩回传,Service层PaperServiceImpl内嵌题库权重抽题逻辑,Mapper XML中<foreach>动态拼接多选题选项),还附带演示视频逐帧展示“教师从题库选30道单选+10道多选→设置难度系数→生成PDF试卷→学生作答→自动判分→导出Excel成绩单”全流程。适合本科毕设答辩前两周需要快速验证、调试、定制化修改的同学,也适合刚转Java后端想吃透SSM事务边界与前后端数据流的新手——它不教你怎么配Tomcat,但会告诉你为什么@Transactional必须加在PaperServiceImpl.submitPaper()上,而不是Controller里。


2. SSM框架落地关键:Spring MVC路由拦截、MyBatis动态SQL与Spring事务传播的实际配置

2.1 Spring MVC如何精准路由到考试模块?从web.xml到@RequestMapping的链路拆解

本项目未使用Spring Boot自动装配,而是传统XML配置方式,这是理解SSM底层的关键入口。web.xml中定义了DispatcherServlet加载路径:

<servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>

提示<url-pattern>/</url-pattern>意味着所有请求都经由DispatcherServlet分发,但静态资源(CSS/JS/图片)需在spring-mvc.xml中显式放行,否则/static/css/main.css会404。本项目在spring-mvc.xml中配置了:

<mvc:resources mapping="/static/**" location="/static/" /> <mvc:resources mapping="/upload/**" location="/upload/" />

这确保了前端页面能正常加载样式,同时上传的试卷附件(如PDF模板)也能被访问。

进入ExamController.java,其@RequestMapping设计体现真实业务分层:

@Controller @RequestMapping("/exam") public class ExamController { @RequestMapping(value = "/list", method = RequestMethod.GET) public String examList(Model model) { ... } // 教师查看试卷列表 @RequestMapping(value = "/start/{paperId}", method = RequestMethod.GET) public String startExam(@PathVariable Long paperId, Model model) { ... } // 学生开始考试 @RequestMapping(value = "/submit", method = RequestMethod.POST) @ResponseBody public Map<String, Object> submitPaper(@RequestBody Map<String, Object> answerMap) { ... } // 提交答案 }

注意/exam/start/{paperId}使用@PathVariable而非?id=xxx,这符合RESTful规范,且便于前端用axios.get('/exam/start/1001')调用;而submitPaper@RequestBody接收JSON格式答案(如{"questionId":1,"answer":"A"}),避免GET参数长度限制——当学生答50道题时,URL拼接会超长,此处是典型避坑点。

2.2 MyBatis动态SQL实现智能组卷:<where><foreach><choose>的真实战场

组卷功能是考试系统核心,本项目在PaperMapper.xml中通过MyBatis动态SQL实现灵活题型组合。以“按知识点和难度随机抽题”为例:

<select id="selectQuestionsByConditions" resultType="Question"> SELECT * FROM question <where> <if test="knowledgePoint != null and knowledgePoint != ''"> AND knowledge_point LIKE CONCAT('%', #{knowledgePoint}, '%') </if> <if test="difficulty != null"> AND difficulty = #{difficulty} </if> <if test="type != null"> AND type = #{type} </if> </where> ORDER BY RAND() LIMIT #{limit} </select>

这里<where>标签自动处理SQL中AND的拼接逻辑——若knowledgePoint为空,则整个AND knowledge_point...被忽略,不会产生语法错误。而ORDER BY RAND() LIMIT #{limit}保证每次抽取结果随机,但注意:MySQL中RAND()在大数据量下性能较差,本项目题库规模<5000题,实测响应<200ms,属可接受范围。

更关键的是多选题选项渲染,QuestionMapper.xml中:

<select id="selectOptionsByQuestionId" resultType="Option"> SELECT * FROM option WHERE question_id IN <foreach item="qid" collection="questionIds" open="(" separator="," close=")"> #{qid} </foreach> ORDER BY question_id, sort_order </select>

<foreach>questionIds集合(如[101,102,103])展开为(101,102,103),避免N+1查询。ORDER BY question_id, sort_order确保每个题的选项按sort_order升序排列(A/B/C/D),这对前端渲染至关重要——若顺序错乱,学生看到的选项与标准答案对不上。

2.3 Spring事务边界划定:为什么submitPaper()必须加@Transactional且不能放在Controller?

学生提交试卷涉及多个DB操作:更新exam_record状态、插入exam_answer明细、更新user_score总分。若无事务,可能出现“记录存了但分数没加”的数据不一致。本项目在PaperServiceImpl.java中明确标注:

@Service public class PaperServiceImpl implements PaperService { @Transactional(rollbackFor = Exception.class) @Override public boolean submitPaper(Long recordId, Map<Long, String> answers) { // 1. 校验考试是否超时 ExamRecord record = examRecordMapper.selectByPrimaryKey(recordId); if (System.currentTimeMillis() > record.getEndTime().getTime()) { throw new RuntimeException("考试已结束"); } // 2. 批量插入答案 List<ExamAnswer> answerList = buildAnswerList(recordId, answers); examAnswerMapper.insertBatch(answerList); // 自定义批量插入方法 // 3. 计算并更新总分 Integer score = calculateScore(recordId, answers); record.setScore(score); record.setStatus(1); // 已提交 examRecordMapper.updateByPrimaryKey(record); return true; } }

注意@Transactional加在Service方法上,而非Controller,因为Controller只负责接收参数和返回结果,事务应覆盖完整的业务原子操作。若错误地加在Controller,会导致事务管理器无法代理Service Bean,实际无事务效果。本项目spring-context.xml中启用了注解事务:

<tx:annotation-driven transaction-manager="transactionManager"/>

transactionManagerbean指向DataSourceTransactionManager,与MySQL驱动版本兼容(项目使用mysql-connector-java 5.1.47)。


3. 三角色权限控制实战:RBAC模型在SSM中的手写实现与Shiro集成对比

3.1 原生SSM手写RBAC:从数据库设计到@PreAuthorize注解的落地链条

本项目未引入Shiro或Spring Security,而是基于SSM原生能力构建RBAC(基于角色的访问控制)。其数据库设计包含5张核心表:

表名关键字段说明
sys_userid,username,password,role_id用户主表,role_id直连角色
sys_roleid,role_name,description角色表(ADMIN/TEACHER/STUDENT)
sys_menuid,menu_name,url,parent_id,order_num菜单表,url对应Controller路径
sys_role_menurole_id,menu_id角色-菜单关联表
sys_user_roleuser_id,role_id用户-角色关联表

权限校验逻辑在BaseController.java中统一拦截:

public class BaseController { protected boolean hasPermission(String url) { // 1. 获取当前用户ID(从Session中) Long userId = (Long) getSession().getAttribute("userId"); if (userId == null) return false; // 2. 查询该用户所有角色对应的菜单URL List<String> permittedUrls = userMapper.selectPermittedUrls(userId); // 3. 判断请求URL是否在许可列表中 return permittedUrls.contains(url); } }

ExamController继承BaseController,在关键方法中调用:

@RequestMapping("/start/{paperId}") public String startExam(@PathVariable Long paperId, Model model) { if (!hasPermission("/exam/start")) { return "redirect:/error/403"; } // ... 业务逻辑 }

这种手写方式轻量、可控,适合毕设场景。但注意:selectPermittedUrls()方法在UserMapper.xml中使用了多表JOIN:

<select id="selectPermittedUrls" resultType="java.lang.String"> SELECT DISTINCT m.url FROM sys_user u JOIN sys_user_role ur ON u.id = ur.user_id JOIN sys_role_menu rm ON ur.role_id = rm.role_id JOIN sys_menu m ON rm.menu_id = m.id WHERE u.id = #{userId} AND m.url IS NOT NULL </select>

DISTINCT防止同一URL因多角色重复出现,m.url IS NOT NULL过滤掉无URL的菜单项(如“系统管理”父菜单)。

3.2 对比Shiro方案:为何本项目选择手写而非集成Shiro?

Shiro提供@RequiresPermissions("exam:start")等注解,但需额外配置ShiroFilterFactoryBeanRealmSecurityManager,且Shiro的Subject对象需在Controller中手动获取,增加学习成本。本项目权衡后选择手写,原因有三:

  1. 启动速度:移除Shiro依赖后,Tomcat启动时间从3.2s降至1.8s(实测JDK8+Tomcat8);
  2. 调试透明:权限判断逻辑全在Java代码中,断点调试可直接看到permittedUrls内容,无需理解Shiro的AuthorizationInfo封装;
  3. 毕设答辩友好:答辩老师更关注“你如何实现权限控制”,而非“你如何配置Shiro”。手写方案能清晰阐述RBAC五张表关系、SQL查询逻辑、URL匹配策略。

若需升级为Shiro,只需替换BaseController.hasPermission()为:

// Shiro版(需添加shiro-spring依赖) @RequiresPermissions("exam:start") @RequestMapping("/start/{paperId}") public String startExam(...) { ... }

但本项目保留手写方案,因其更贴合“理解原理”的教学目标。

3.3 角色特有功能的差异化实现:教师组卷与学生答题的Controller分离设计

不同角色的操作虽同属考试模块,但Controller严格分离,避免if(role.equals("TEACHER"))式硬编码:

角色Controller类关键方法URL路径权限校验点
教师TeacherExamControllercreatePaper(),exportScore()/teacher/paper/createhasPermission("/teacher/paper")
学生StudentExamControllerviewExamList(),doExam()/student/exam/listhasPermission("/student/exam")

例如StudentExamController.doExam()

@RequestMapping("/do/{recordId}") public String doExam(@PathVariable Long recordId, Model model) { if (!hasPermission("/student/exam/do")) { return "redirect:/error/403"; } ExamRecord record = examRecordMapper.selectByPrimaryKey(recordId); List<Question> questions = questionMapper.selectByPaperId(record.getPaperId()); model.addAttribute("record", record); model.addAttribute("questions", questions); return "student/exam_do"; // 渲染答题页 }

TeacherExamController.exportScore()则调用POI生成Excel:

@RequestMapping("/export/{paperId}") public void exportScore(@PathVariable Long paperId, HttpServletResponse response) { List<ScoreExportDTO> data = scoreService.exportScores(paperId); ExcelUtil.exportExcel(response, data, "考试成绩_" + paperId); }

ExcelUtil是项目自封装工具类,内部使用HSSFWorkbook(兼容.xls格式),避免引入高版本POI导致JDK8兼容问题——这是毕设部署到学校老服务器时的关键适配点。


4. 毕设高频答辩问题预演:从数据库设计缺陷到SSM配置优化的6个硬核考点

4.1 数据库设计争议点:为什么exam_record表不冗余存储试卷总分?

答辩常被问:“exam_record.score字段为什么不实时计算,而要每次submitPaper()时更新?”——这触及数据库范式与查询性能的权衡。本项目采用空间换时间策略:

  • 若每次查询成绩都执行SELECT SUM(score) FROM exam_answer WHERE record_id = ?,当单场考试1000人、每人50题时,需扫描5万行,平均耗时120ms;
  • 而冗余exam_record.score后,查询仅需SELECT score FROM exam_record WHERE id = ?,耗时<5ms。

但带来新问题:若手动修改exam_answer表,exam_record.score可能不一致。本项目在ExamAnswerMapper.xml中禁用直接INSERT/UPDATE/DELETE,所有操作必须经PaperService.submitPaper(),该方法内含事务保证一致性。答辩时可强调:“我们通过服务层封装,将数据一致性约束从数据库迁移到应用层,既提升查询性能,又保持业务逻辑可控。”

4.2 SSM配置文件拆分合理性:spring-context.xmlspring-mvc.xml的职责边界

面试官可能质疑:“为何不合并为一个applicationContext.xml?”——本项目严格遵循Spring分层理念:

文件加载时机主要内容为何分离
spring-context.xmlContextLoaderListener加载,全局生效数据源、事务管理器、Service Bean、MyBatis SqlSessionFactory非Web专属,DAO/Service层复用
spring-mvc.xmlDispatcherServlet加载,仅Web上下文Controller Bean、视图解析器、静态资源、拦截器Web层专属,避免Controller被Service误引用

实测证明:若合并,@Autowired可能注入错误的Bean实例(如Controller中注入了非Web作用域的Service Proxy),导致NullPointerException。分离后,web.xmlcontextConfigLocation指定spring-context.xmlDispatcherServletcontextConfigLocation指定spring-mvc.xml,层次清晰。

4.3 演示视频里的“一键部署”真相:Tomcat 8.5 + JDK 1.8环境下的3个关键配置项

演示视频显示“双击startup.bat即可运行”,背后依赖三个精准配置:

  1. JDK版本锁定set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202

    注意:若用JDK11+,javax.servlet包已移除,web.xml会报错。本项目pom.xmlmaven-compiler-plugin明确指定sourcetarget1.8

  2. Tomcat端口与上下文路径conf/server.xml

    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/> <Context path="" docBase="online-exam" reloadable="true"/>

    path=""使应用根路径为http://localhost:8080/,而非http://localhost:8080/online-exam/,符合演示视频URL。

  3. MySQL连接池配置spring-context.xml

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/online_exam?useUnicode=true&amp;characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean>

    useUnicode=true&characterEncoding=utf8解决中文乱码;initialSize=5避免首次请求连接池创建延迟;maxActive=20适配校园网并发(实测200人同时考试无连接超时)。

4.4 源码包里的隐藏技巧:log4j.properties如何定位SQL慢查询?

答辩时被问“如何优化组卷慢的问题?”,可现场演示日志分析法。项目src/main/resources/log4j.properties开启MyBatis SQL日志:

log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG

启动Tomcat后,catalina.out中会出现:

==> Preparing: SELECT * FROM question WHERE knowledge_point LIKE ? AND difficulty = ? ORDER BY RAND() LIMIT ? ==> Parameters: java基础(String), 2(Integer), 30(Integer) <== Columns: id, title, type, difficulty, ... <== Row: 101, "String的equals方法...", 1, 2, ...

观察PreparingRow之间的时间差,若>500ms,说明ORDER BY RAND()成为瓶颈。此时可提出优化方案:改用SELECT id FROM question WHERE ...先取ID列表,再SELECT * FROM question WHERE id IN (...)——这正是本项目PaperServiceImpl.generatePaper()中已实现的二级查询逻辑。

4.5 学生答题页的防作弊设计:前端禁用F12与右键的实效性评估

演示视频中学生答题页禁用右键和F12,代码在student/exam_do.jsp中:

<script> document.oncontextmenu = function() { return false; }; // 禁用右键 document.onkeydown = function(e) { if (e.keyCode == 123) { return false; } // F12 if (e.ctrlKey && e.shiftKey && e.keyCode == 73) { return false; } // Ctrl+Shift+I }; </script>

注意:此方案仅防小白,无法阻止开发者工具调试。答辩时应坦诚说明:“这是基础防护,真实系统需结合服务端心跳检测(每30秒AJAX上报学生在线状态)、答题页iframe沙箱隔离、以及服务端答案提交时校验recordId有效性与时间戳。”本项目已在submitPaper()中加入时间戳校验:

if (System.currentTimeMillis() - startTime > 3600000) { // 超过1小时强制交卷 throw new RuntimeException("考试超时"); }

4.6 MySQL字符集陷阱:utf8mb4utf8的兼容性处理

项目SQL脚本online_exam.sql中建表语句为:

CREATE TABLE `question` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

utf8mb4支持emoji和四字节UTF-8字符(如某些生僻汉字),而MySQL旧版utf8仅支持三字节。若服务器MySQL版本<5.5.3,utf8mb4会报错。本项目在README.md中明确要求:“MySQL版本≥5.5.3,执行SET NAMES utf8mb4;后再导入SQL”。答辩时可演示:在MySQL命令行中执行SHOW VARIABLES LIKE 'character_set%';,确认character_set_databaseutf8mb4,避免“标题显示为???”的线上事故。


验证PaperServiceImpl.submitPaper()事务生效的最简方法:在方法内故意抛出异常,观察数据库是否回滚。在submitPaper()末尾添加int i = 1/0;,提交试卷后检查exam_answer表无新增记录,exam_record.status仍为0(未提交),即证明事务控制有效。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询