简介:本资源是一套完整的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_user、exam_paper、exam_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_user | id,username,password,role_id | 用户主表,role_id直连角色 |
sys_role | id,role_name,description | 角色表(ADMIN/TEACHER/STUDENT) |
sys_menu | id,menu_name,url,parent_id,order_num | 菜单表,url对应Controller路径 |
sys_role_menu | role_id,menu_id | 角色-菜单关联表 |
sys_user_role | user_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")等注解,但需额外配置ShiroFilterFactoryBean、Realm、SecurityManager,且Shiro的Subject对象需在Controller中手动获取,增加学习成本。本项目权衡后选择手写,原因有三:
- 启动速度:移除Shiro依赖后,Tomcat启动时间从3.2s降至1.8s(实测JDK8+Tomcat8);
- 调试透明:权限判断逻辑全在Java代码中,断点调试可直接看到
permittedUrls内容,无需理解Shiro的AuthorizationInfo封装; - 毕设答辩友好:答辩老师更关注“你如何实现权限控制”,而非“你如何配置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路径 | 权限校验点 |
|---|---|---|---|---|
| 教师 | TeacherExamController | createPaper(),exportScore() | /teacher/paper/create | hasPermission("/teacher/paper") |
| 学生 | StudentExamController | viewExamList(),doExam() | /student/exam/list | hasPermission("/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.xml与spring-mvc.xml的职责边界
面试官可能质疑:“为何不合并为一个applicationContext.xml?”——本项目严格遵循Spring分层理念:
| 文件 | 加载时机 | 主要内容 | 为何分离 |
|---|---|---|---|
spring-context.xml | ContextLoaderListener加载,全局生效 | 数据源、事务管理器、Service Bean、MyBatis SqlSessionFactory | 非Web专属,DAO/Service层复用 |
spring-mvc.xml | DispatcherServlet加载,仅Web上下文 | Controller Bean、视图解析器、静态资源、拦截器 | Web层专属,避免Controller被Service误引用 |
实测证明:若合并,@Autowired可能注入错误的Bean实例(如Controller中注入了非Web作用域的Service Proxy),导致NullPointerException。分离后,web.xml中contextConfigLocation指定spring-context.xml,DispatcherServlet的contextConfigLocation指定spring-mvc.xml,层次清晰。
4.3 演示视频里的“一键部署”真相:Tomcat 8.5 + JDK 1.8环境下的3个关键配置项
演示视频显示“双击startup.bat即可运行”,背后依赖三个精准配置:
JDK版本锁定:
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202注意:若用JDK11+,
javax.servlet包已移除,web.xml会报错。本项目pom.xml中maven-compiler-plugin明确指定source和target为1.8。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。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&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, ...观察Preparing与Row之间的时间差,若>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字符集陷阱:utf8mb4与utf8的兼容性处理
项目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_database为utf8mb4,避免“标题显示为???”的线上事故。
验证PaperServiceImpl.submitPaper()事务生效的最简方法:在方法内故意抛出异常,观察数据库是否回滚。在submitPaper()末尾添加int i = 1/0;,提交试卷后检查exam_answer表无新增记录,exam_record.status仍为0(未提交),即证明事务控制有效。
本文还有配套的精品资源,点击获取