基于Java的在线考试系统:Spring Boot+MyBatis+Redis
简介在线考试系统是JavaWeb开发中的经典项目涵盖用户认证、业务逻辑、数据库设计与部署上线等核心环节。其底层技术栈通常以Spring Boot作为基础框架结合MyBatis实现数据持久化并通过Redis解决验证码存储、考试令牌与题库缓存等并发场景问题。从系统设计角度看数据库表结构决定了业务扩展性而自动组卷算法与防重复提交机制则直接关系到考试公平性与系统稳定性。这一应用场景既适用于高校毕业设计也可支撑企业内部培训考核。围绕此类系统的技术选型、核心算法与部署实践对开发者提升工程能力具有实际参考价值。现以“基于Java的在线考试系统”项目为例完整拆解从技术选型到部署上线的全流程帮助读者理解每个设计决策背后的权衡逻辑。 又到了毕业设计旺季后台每天都能收到一堆关于“基于Java的在线考试系统”的私信。说实话每次看到这个题目我都有点恍惚——一届又一届的学生做着几乎一模一样的系统但真正能把这个项目讲清楚、讲明白、能扛住答辩追问的确实不多。这个项目之所以长盛不衰是因为它几乎覆盖了JavaWeb开发的核心环节从用户认证到业务逻辑从数据库设计到部署上线每个环节都能考察到。但与此同时它也藏着不少“暗坑”稍不注意就会在答辩或实际运行中翻车。这篇文章我就以“基于Java的在线考试系统设计与实现”这个项目为蓝本把我自己的设计思路、数据库表结构、核心算法尤其是自动组卷和防作弊、以及部署过程中的各种坑完整地拆开来讲。无论你是正在做毕设、还是想在公司内部快速搭一套培训考试系统这篇文章都能给你一个可以“抄作业”的完整方案。1. 系统整体设计与技术选型的底层逻辑1.1 技术选型为什么是Spring Boot而不是SSH或SSM很多教材还在教SSHStruts2SpringHibernate甚至更早的JSPServlet但说实话这些技术栈在今天的企业里已经很少见了。我在做这个在线考试系统时选型思路非常明确Spring Boot 2.x MyBatis Plus MySQL 8.0 Redis可选。这套组合是当前Java后端开发最主流、最不容易出错、也最适合“快速交付”的一套。先说为什么不用SSH。Struts2的拦截器机制和XML配置过于繁琐Action类又和Servlet API藕断丝连写起来极其痛苦。Hibernate呢虽然全自动ORM听起来很美好但实际项目里一旦涉及到复杂查询、多表联查、动态SQLHibernate的HQL和Criteria API能把人绕晕而且它的N1查询问题很让人头疼。相比之下MyBatis把SQL完全交给你掌控写复杂查询时心里特别有底。Spring Boot带来的最大好处就是“约定大于配置”。以前搭一个SSM项目要配置web.xml、spring-mvc.xml、spring-mybatis.xml、数据源、事务管理器光是配置文件就够写一天。Spring Boot直接通过spring-boot-starter-web、spring-boot-starter-data-redis这些起步依赖搞定一个application.yml全部搞定内置Tomcat打包成jar后一句java -jar就能跑起来。我实际测下来从零搭建到能跑通登录接口SSM大概需要2小时Spring Boot只需要15分钟。版本选择上要注意一个坑Spring Boot 2.x对应Java 8/11Spring Boot 3.x强制要求Java 17。如果你的毕设环境是学校机房那种老掉牙的JDK 1.8就千万别用Spring Boot 3.x否则编译直接报错UnsupportedClassVersionError。我个人推荐Spring Boot 2.7.x这是2.x系列的最后版本稳定且资料多。前端方面我选了Thymeleaf模板引擎而不是前后端分离的Vue。原因很简单这个系统的用户规模和应用场景决定了它不需要前后端分离。题库管理、考试列表、在线答题这些页面用Thymeleaf加一点原生JS和Ajax就能实现还能省去跨域、Token刷新、路由鉴权这些麻烦。你要是硬上VueSpring Boot反而会因为知识点太多答辩时被老师问得一头冷汗。1.2 功能模块划分这套系统该有哪些功能在线考试系统的核心不是“考试”而是“考务管理”。我见过很多同学的系统只做了“学生登录进去答题然后出成绩”这一个流程这在答辩时是致命的——老师一定会问“你怎么防止学生重复提交”“题目怎么保证随机性”“考试成绩怎么统计分析”我的功能设计分为四个角色、六个核心模块管理员端用户管理教师和学生账号的CRUD、课程管理一门课程对应一场或多场考试、题库管理单选、多选、判断三种题型的增删改查、试卷管理手动组卷和自动组卷、考试管理创建考试、设置考试时间、发布/停用考试、成绩统计导出Excel。教师端本课程范围内的题库维护、手动组卷、查看学生成绩和排名。和管理员相比教师不能操作其他课程的试卷这个用数据权限控制。学生端查看已发布的考试列表、进入考试答题、提交试卷、查看自己的历史成绩和得分明细如果是客观题可以看对错。公共功能登录认证、验证码、退出登录、个人信息修改、密码修改。这里有个容易忽略的点考试管理一定要区分“创建考试”和“发布考试”两个状态。创建考试只是把试卷、时间、规则配置好这时候学生看不到只有管理员手动点击“发布”学生端才会显示这场考试。这样做的好处是你可以提前把试卷配好到点了再一键发布避免学生提前看到考试信息。完整代码和数据库脚本我整理打包好了包含详细的部署文档和讲解视频有需要的可以直接下载参考。2. 数据库设计一张表一张表地拆给你看2.1 核心表结构与字段设计数据库设计是整个系统的地基地基不稳后面全是雷。我设计的数据表一共有8张用户表sys_user、角色表sys_role、用户角色关联表sys_user_role、课程表edu_course、题库表exam_question、试卷表exam_paper、试卷题目关联表exam_paper_question、考试记录表exam_record。下面挑几张核心表详细说。用户表sys_user字段设计如下id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键 username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名 password VARCHAR(100) NOT NULL COMMENT 密码BCrypt加密 real_name VARCHAR(50) COMMENT 真实姓名 email VARCHAR(100) COMMENT 邮箱 phone VARCHAR(20) COMMENT 手机号 avatar VARCHAR(255) COMMENT 头像地址 status TINYINT DEFAULT 1 COMMENT 状态1启用 0禁用 create_time DATETIME DEFAULT CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP密码这里多说两句。很多课设代码用的是MD5加密这是不安全的。MD5早就被彩虹表攻破了答辩时老师如果问你“密码怎么存储的”你说MD5大概率会被追问“MD5有什么安全问题”答不上来就很尴尬。我用的是BCryptPasswordEncoderSpring Security自带的加密器每次加密生成的盐都不同同一个密码两次加密结果不一样安全性高很多。题库表exam_questionid BIGINT PRIMARY KEY AUTO_INCREMENT question_type TINYINT COMMENT 题型1单选 2多选 3判断 question_content TEXT COMMENT 题干 option_a VARCHAR(255) COMMENT 选项A option_b VARCHAR(255) COMMENT 选项B option_c VARCHAR(255) COMMENT 选项C option_d VARCHAR(255) COMMENT 选项D answer VARCHAR(10) COMMENT 正确答案单选如A多选如ABD判断如T/F score INT DEFAULT 5 COMMENT 每题分值 difficulty TINYINT DEFAULT 1 COMMENT 难度1简单 2中等 3困难 course_id BIGINT COMMENT 所属课程 create_by BIGINT COMMENT 创建人ID这里有几个设计上的决策点。选项字段option_a到option_d是单独列而不是存成一个JSON数组。有些同学图省事把四个选项拼成一个字符串塞进一个字段这在查询和回显时特别痛苦——你得自己split还得处理选项里本来就带逗号的情况。四列虽然看起来笨但最清晰、最稳定。此外判断题的答案我用T和F两个字符存储而不是正确/错误两个汉字这样在做答案比对时效率更高。还有一个细节是为了方便试卷展示我给题干和选项都设置了足够长的字段长度题干用TEXT类型避免题目一长就被截断进而造成批改错乱。试卷题目关联表exam_paper_questionid BIGINT PRIMARY KEY AUTO_INCREMENT paper_id BIGINT COMMENT 试卷ID question_id BIGINT COMMENT 题目ID question_order INT COMMENT 题目序号为什么需要一张关联表而不是在试卷表里存一个“题目ID列表”因为关系型数据库设计的第一范式和第二范式告诉我们不能把多个值存在一个字段里。如果你把题目ID用逗号拼成一个字符串存进试卷表以后想统计“这套试卷有多少道单选”都只能靠代码遍历字符串split查询效率低不说逻辑还容易错。关联表加一个question_order字段控制题目顺序组卷时按question_order升序排列就行。考试记录表exam_record这张表是系统的核心我这样设计id BIGINT PRIMARY KEY AUTO_INCREMENT exam_id BIGINT COMMENT 考试ID paper_id BIGINT COMMENT 试卷ID student_id BIGINT COMMENT 学生ID total_score INT COMMENT 总分 user_score INT COMMENT 得分 duration INT COMMENT 实际用时分钟 submit_time DATETIME COMMENT 提交时间 status TINYINT COMMENT 状态0未提交 1已提交 2考试超时 answer_detail TEXT COMMENT 答题明细JSONanswer_detail字段我用来存一个JSON数组记录每道题的答题情况结构如下[ {questionId: 1, userAnswer: A, isCorrect: true, score: 5}, {questionId: 2, userAnswer: ABD, isCorrect: false, score: 0} ]为什么要存JSON而不是为每道题建一张明细表因为一份试卷可能有50道题如果每道题都在明细表里占一行50个考生就是2500行而且这些明细只有在学生提交后、查看历史成绩时才会被读取属于“低频大数据”。用JSON存一列读取时JSON.parse一下就还原出来写起来也简单——提交时一次性把整个数组序列化后存进去。考试明细不做外键关联而是冗余存下题号、答案、得分是为了防止题目被修改后历史记录跟着变。比如老师改了一道题学生之前的成绩单如果通过外键去查就查不到当时的答案了。2.2 表关系设计与索引优化表关系我用一句话概括用户和角色是多对多课程和题目是一对多试卷和题目是多对多考试和试卷是一对一考试和学生是一对多。关于索引我建了这几个exam_question(course_id)按课程查题目列表时高频使用。exam_paper_question(paper_id)组卷后加载试卷题目时使用。exam_record(student_id)学生查看自己的历史考试记录时使用。exam_record(exam_id, student_id)联合索引查“某场考试里某个学生是否已提交”时高频使用。sys_user(username)登录验证时按用户名查用户必须有唯一索引。这里要注意联合索引的最左前缀原则。(exam_id, student_id)这个联合索引只有在查询条件同时包含exam_id时才能用到。如果你经常要查“某学生参加的所有考试”那就要单独建student_id索引或者把联合索引顺序调成(student_id, exam_id)。我实际设计中因为两种情况都有所以建了两个索引虽然多占一点磁盘但查询速度有保障。3. 核心功能实现这几块代码是答辩必问3.1 登录认证与验证码安全是第一道门槛登录模块我用的是Session方式配合Kaptcha生成图片验证码。流程是这样的PostMapping(/login) public Result login(RequestParam String username, RequestParam String password, RequestParam String captcha, HttpSession session) { // 1. 校验验证码 String sessionCaptcha (String) session.getAttribute(captcha); if (sessionCaptcha null || !sessionCaptcha.equalsIgnoreCase(captcha)) { return Result.error(验证码错误); } // 2. 查询用户 SysUser user userService.getByUsername(username); if (user null || !passwordEncoder.matches(password, user.getPassword())) { return Result.error(用户名或密码错误); } // 3. 校验状态 if (user.getStatus() 0) { return Result.error(账号已被禁用); } // 4. 保存登录状态 session.setAttribute(loginUser, user); session.setAttribute(loginRole, roleService.getRoleByUserId(user.getId())); return Result.success(); }这里会有一个验证码被反复使用的问题需要保证校验之后立即从Session中移除防止同一验证码被重放。另外验证码校验是“先校验验证码再校验密码”这样做的目的是先消耗掉用户一次验证码防止有人用脚本暴力试密码时每次都先刷新一下验证码。逻辑上如果验证码错了就不需要再去查数据库了能减轻一点数据库压力。登录后的权限控制我用的是Spring MVC的HandlerInterceptor拦截器判断Session里有没有用户以及角色标识是否符合访问路径要求。管理员和教师端路径以/admin/开头学生端以/student/开头。拦截器里要注意放行静态资源否则CSS和JS会被拦住页面样式全丢。3.2 自动组卷算法一段让很多人挠头的逻辑组卷是整个系统里比较有含金量的一环。手动组卷就是老师从题库里勾选题目难点不大自动组卷则要根据规则从题库里随机抽题。规则通常是这样的单选题15道每题3分、多选题5道每题5分、判断题10道每题2分总分100分且各题型难度都控制在“基础为主、少量提高”的分布。我的实现思路是分题型、按条件过滤、再随机抽取核心代码如下public ListExamPaperQuestion generatePaper(Integer courseId, Integer singleCount, Integer singleScore, Integer multiCount, Integer multiScore, Integer judgeCount, Integer judgeScore) { ListExamPaperQuestion paperQuestions new ArrayList(); // 按题型分别抽题 paperQuestions.addAll(randomPickQuestions(courseId, 1, singleCount, singleScore)); paperQuestions.addAll(randomPickQuestions(courseId, 2, multiCount, multiScore)); paperQuestions.addAll(randomPickQuestions(courseId, 3, judgeCount, judgeScore)); // 打乱整个试卷的题目顺序 Collections.shuffle(paperQuestions); // 重新设置序号 for (int i 0; i paperQuestions.size(); i) { paperQuestions.get(i).setQuestionOrder(i 1); } return paperQuestions; } private ListExamPaperQuestion randomPickQuestions(Integer courseId, Integer type, Integer count, Integer score) { // 查出该课程该题型的所有题目 ListExamQuestion allQuestions questionMapper.selectList( new LambdaQueryWrapperExamQuestion() .eq(ExamQuestion::getCourseId, courseId) .eq(ExamQuestion::getQuestionType, type) .eq(ExamQuestion::getStatus, 1)); // 如果题库不够直接抛异常提醒老师 if (allQuestions.size() count) { throw new BusinessException(题库不足该题型还需要 (count - allQuestions.size()) 道题); } // 随机抽取count道 Collections.shuffle(allQuestions); ListExamQuestion picked allQuestions.subList(0, count); return picked.stream().map(q - { ExamPaperQuestion pq new ExamPaperQuestion(); pq.setQuestionId(q.getId()); pq.setScore(score); return pq; }).collect(Collectors.toList()); }这段代码其实体现了几个很重要的工程思想先把该课程、该题型的全部题目查出来在内存里shuffle再取前count个。这种做法在小数据量下非常高效、且逻辑简单。如果你的题库有几十万道题那需要优化为SQL里使用ORDER BY RAND() LIMIT count但那样在大数据量下性能会很差。我实测过5000道题以内用内存shuffle完全没问题响应时间在50毫秒以内。抽题前一定要判断题库数量是否足够。很多同学不写这个判断结果题库只有20道单选题、组卷规则却要求25道程序直接抛出IndexOutOfBoundsException。这里我抛的是自定义的BusinessException会在前端弹出一个友好的错误提示“题库不足请先添加题目”而不是让用户看到一堆堆栈异常。抽题后将整个试卷Collections.shuffle一次确保三道大题的题目是混合排列的而不是“单选题全部在前、多选题在后”。从防作弊的角度讲这种混合排列能减少相邻考生互相瞄答案的概率。3.3 在线考试“防作弊”双保险提交前和提交后都要拦在线考试系统一个高频踩坑点就是——学生提交试卷后再刷新页面又能重新答一遍且系统把第二次的答案也存下来了。这个问题的根源在于你没有做重复提交校验。我的处理方案是“前端后端双重锁”。前端在考生进入考试页时向后端请求一个“考试令牌”examToken这个令牌就是一张试卷的paperId加上一个随机UUID存储在Redis或Session中并和考试记录关联。当学生点击“提交试卷”按钮时前端立刻把按钮置灰、禁用并向后端发起提交请求请求头中携带这个examToken。后端在服务端做两层判断PostMapping(/submit) public Result submit(RequestParam Long examId, RequestParam String examToken, RequestBody String answerJson) { // 判断是否已有提交记录 ExamRecord record recordMapper.selectOne( new LambdaQueryWrapperExamRecord() .eq(ExamRecord::getExamId, examId) .eq(ExamRecord::getStudentId, currentUserId())); if (record ! null) { return Result.error(请勿重复提交); } // 校验考试令牌 String tokenInCache examTokenService.getToken(examId, currentUserId()); if (!examToken.equals(tokenInCache)) { return Result.error(令牌无效请刷新页面重试); } // 删除令牌使令牌失效 examTokenService.deleteToken(examId, currentUserId()); // ... 后续判分逻辑 }这里“删除令牌”是至关重要的一步它是真正的“锁”。即使前端按钮没被禁用、用户狂点提交后端在第二次请求时令牌已经删掉了校验必然失败从根源杜绝重复提交。然后说说自动判分。单选题和判断题的判分比较简单直接比对字符串即可。多选题稍微麻烦一点因为多选题目通常要求“完全一致才得分”也就是说学生选了AB正确答案是ABC不能得分。如果业务要求“少选得一半分”那还得在判分逻辑里单独处理。我在系统中采用了“严格判分”模式public int judgeMulti(String userAnswer, String correctAnswer, int score) { // 排序后比较避免AB和BA被判定为错误 String normalizedUser normalize(userAnswer); String normalizedCorrect normalize(correctAnswer); if (normalizedUser.equals(normalizedCorrect)) { return score; } return 0; }注意归一化处理。如果学生提交的答案是B,A,D正确答案是A,B,D不排序直接equals肯定判错。所以我在判分前会把答案字符串按逗号分割、去空格、排序、再拼接保证顺序不影响判分结果。这个问题在答辩时出现过一定要处理干净。超时自动交卷也是一个重要的功能需求。考试界面有倒计时前端通过JavaScript计时倒计时归零时自动触发提交。但前端的倒计时是可以被篡改的比如用开发者工具把时间改长所以后端必须再做一层“超时兜底”。我的做法是判分时先判断当前时间和开考时间是否超过考试时长如果超过强制截断作答并标记为超时提交。具体实现是考试记录表在创建时记录start_time提交时计算duration (now - start_time) / 60000再和考试设定的时长比对超出即按超时处理。这里需要用System.currentTimeMillis()来计算别用new Date().getTime()后者在时区配置有误时容易出问题。4. 数据库与Redis配置生产环境跑起来的必要条件4.1 MySQL连接与连接池参数调优在application.yml中数据库连接配置看起来简单但有几个参数值得刻意设置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowMultiQueriestruerewriteBatchedStatementstrue username: root password: yourpassword hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 max-lifetime: 1800000这里有几个“坑王”挨个说serverTimezoneAsia/Shanghai必须加上。MySQL 8.0默认时区是UTC你如果不在连接串里指定时区Java插入的时间会和本地时间差8小时。你晚上8点创建一场考试数据库里存的却是中午12点第二天考试直接还没开始。useSSLfalse要加。MySQL 8.0默认尝试使用SSL连接如果你本机没有配置SSL证书会报错加上这个参数就绕过去了。allowMultiQueriestrue允许在一个语句中用分号分隔多条SQL。这在初始化数据脚本、批量导入题库时很有用。比如在测试环境跑一个大的init.sql时不需要自己拆分成几百条单独的execute直接一条语句全跑完。HikariCP的连接池大小不是越大越好。MySQL默认的max_connections是151你设置成100其他应用就连不上了。一般小型系统maximum-pool-size设20就绰绰有余因为单台服务器即使有200个并发请求绝大多数都阻塞在业务逻辑和IO上真正同时占用数据库连接的不会超过20。连接池开太大反而白白占用数据库连接槽位造成不必要的资源紧张。4.2 Redis缓存不是可选项是必备项有人可能会觉得一个小型考试系统用不上Redis。我一开始也是这么想的但做完后复盘发现Redis至少有三个不可替代的场景存储验证码。Session存储验证码在单体架构下没问题但毕业设计现在也流行用Docker部署一旦你将来扩展成多实例Session就无法跨实例共享而Redis天然就是分布式的。存储考试令牌。上面说的防重复提交令牌用Redis存储时可以通过SET key value EX 120设置过期时间也就是考试开始后120分钟内令牌有效考试结束后自动销毁避免内存泄漏。缓存题库。题库表的题目内容基本不变只有教师偶尔修改。第一次查询时从MySQL加载并放到Redis里之后读取走缓存QPS能提升一个量级。如果学生同时并发进入考试页面后端要反复读取同一个试卷的题目列表缓存的意义非常大。Redis的配置和使用也简单spring: redis: host: localhost port: 6379 password: yourpassword database: 0 timeout: 5000 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0注意Spring Boot 2.x默认用的是Lettuce连接池而不是Jedis。Lettuce是基于Netty的异步驱动性能更好但你在网上搜到的一些老教程还在用Jedis。如果你在application.yml里配置了spring.redis.jedis.pool在Spring Boot 2.x下会直接不生效启动时甚至不报错等并发一高就抛异常。这个坑很隐蔽排查了半天才发现是连错了配置前缀。另外database: 0默认使用的是0号库如果你的项目里同时跑了多个系统建议隔离开来用不同的数据库编号防止key冲突导致数据串了。4.3 代码中Redis的正确使用姿势考试令牌的Redis存取代码如下注意要用StringRedisTemplate而不是RedisTemplateString, Object。后者默认的序列化器是JDK序列化存进去的内容是一串带特殊前缀的二进制肉眼没法检查而且跨语言跨平台兼容性差。StringRedisTemplate直接存字符串Redis客户端里一眼就能看到内容排查问题特别方便。public void saveExamToken(Long examId, Long userId, String token) { String key exam:token: examId : userId; stringRedisTemplate.opsForValue().set(key, token, 120, TimeUnit.MINUTES); } public String getExamToken(Long examId, Long userId) { String key exam:token: examId : userId; return stringRedisTemplate.opsForValue().get(key); } public void deleteExamToken(Long examId, Long userId) { String key exam:token: examId : userId; stringRedisTemplate.delete(key); }核心的拦截器也要用Redis辅助实现考试超时校验。在进入考试页面时我会把考试开始时间也存到Redis里exam:start_time:{examId}:{userId} - 1635292800000每次考生提交答案时从Redis里取这个时间戳用System.currentTimeMillis()减一下得到实际用时超过考试时长就直接判为超时。实际实现时我还会在前端通过WebSocket或者定时轮询的方式在服务端主动踢出超时考生。5. 部署与运维实战从源代码到可运行系统5.1 环境准备与部署步骤整个部署我分成五步走每一步都踩过坑你按下面的顺序操作基本不会出大问题。第一步安装JDK并配置环境变量。我推荐JDK 8即1.8或者JDK 11原因前面说过Spring Boot 2.7.x完全兼容。Windows下安装完成后在环境变量里新建JAVA_HOME指向JDK安装目录在Path里追加%JAVA_HOME%\bin。验证方式是命令行输入java -version javac -version两条命令都能正常输出版本号说明JDK环境OK。如果java能用但javac报“不是内部或外部命令”大概率是Path没配置好或者你装的是JRE而不是JDK。第二步安装MySQL 8.0并初始化数据库。安装过程中会让你设置root密码记好。然后创建数据库CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;注意一定要显式指定utf8mb4。如果使用默认的latin1插入中文数据时直接报错或乱码。然后导入SQL脚本mysql -u root -p exam_system exam_system.sql导入时如果提示ERROR 1046 (3D000): No database selected说明你忘了指定数据库名会退回到系统默认库初始化表和数据的语句全跑错。第三步配置application.yml。把数据库账号密码、Redis地址改成自己的最关键的是检查serverTimezone参数有没有漏。有同学在这里出现过连接超时报错Access denied for user rootlocalhost先检查密码再检查MySQL是否有用户权限SELECT user, host, plugin FROM mysql.user WHERE user root;MySQL 8.0默认的认证插件是caching_sha2_password有时旧驱动不支持会报Public Key Retrieval is not allowed。解决方式是在连接串后加allowPublicKeyRetrievaltrue或者把用户的认证插件改成mysql_native_password。我用的是最新驱动没遇到这个问题但老驱动用户一定要记住这一步。第四步打包项目。在项目根目录执行mvn clean package -DskipTests如果Maven下载依赖一直卡住试试配置阿里云镜像在settings.xml的mirrors标签里加入mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror打包成功后target目录下会生成一个exam-system-0.0.1-SNAPSHOT.jar这就是可执行文件。第五步启动项目。java -jar exam-system-0.0.1-SNAPSHOT.jar生产环境建议用nohup方式后台运行nohup java -jar exam-system-0.0.1-SNAPSHOT.jar exam.log 21 启动后访问http://localhost:8080就能看到登录页面了。如果想改端口在application.yml里加server: port: 80805.2 部署时最常遇到的5个启动异常Port 8080 was already in use端口被占用。先找占用进程Windows下用netstat -ano | findstr 8080再用taskkill /F /PID 进程号杀掉Linux下用fuser -k 8080/tcp。也可以直接改端口避开。杀进程时一定要注意确认是不是你自己的Java进程别误杀了别人的服务。Failed to configure a DataSource启动时Spring Boot找不到数据源。原因几乎都是application.yml里数据库配置没生效或者注解里没加MapperScan。先检查配置文件里url、username、password这三个参数有没有写对再看看spring.datasource这块的缩进是不是被IDE自动改坏了。YAML对缩进极其敏感层级错了配置文件就会静默失效。Caused by: java.net.ConnectException: Connection refused连接被拒绝。通常是MySQL没启动、Redis没启动、或者端口号写错。分别检查mysql -u root -p能不能连上redis-cli ping能不能返回PONG。Incorrect string value: \xE5\xBC\xA0... for column real_name插入中文报错。这个就是数据库表结构编码问题把表和字段的字符集改成utf8mb4即可。java.lang.OutOfMemoryError: Insufficient memory内存不足。Spring Boot默认堆内存很小跑一个大系统或者并发一高就OOM。在启动命令里加参数java -Xms256m -Xmx512m -jar exam-system.jar-Xms是启动时初始内存-Xmx是最大内存。如果服务器内存充足可以给到-Xms512m -Xmx1024m。加内存后还OOM就要怀疑代码里有内存泄漏了重点检查有没有在循环里不断往集合里塞对象或者是不是把整个文件读进内存没有关闭流。6. 从0到1的完整实现流程附部署文档核心内容6.1 初始化项目骨架的五分钟快建法我们以Spring Initializr方式举例。打开https://start.spring.io/选择Maven工程、Spring Boot 2.7.18、JDK8依赖选择WebThymeleafMyBatis FrameworkMySQL DriverData RedisLombokValidation生成后导入IDEA在启动类上加上MapperScan(com.example.mapper)然后新增包结构com.example.exam ├── config ├── controller ├── interceptor ├── mapper ├── pojo │ ├── entity │ ├── dto │ └── vo ├── service │ └── impl ├── util └── ExamApplication.java遵循这种分包规范答辩时老师问“项目结构怎么设计的”你可以很清晰地说出每层的职责比你随便乱放包要好很多。6.2 部署文档要写什么给“另一个自己”看我在项目里附带的部署文档目录结构是这样的部署文档 ├── 1-环境要求.md ├── 2-数据库初始化.md ├── 3-项目配置文件修改.md ├── 4-打包与运行.md ├── 5-常见问题排查.md └── 6-管理员初始账号说明.md其中6是非常重要的一环我见过太多人部署成功后找不到管理员账号。在SQL脚本里要明确插入初始管理员数据比如INSERT INTO sys_user (id, username, password, real_name, status, create_time) VALUES (1, admin, $2a$10$abc..., 系统管理员, 1, NOW()); INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1);文档里明确写着管理员账号 admin初始密码 123456第一次登录后请立即修改并在“常见问题”里说明“如果你用admin登录报密码错误大概率是密码里包含了需要转义的字符或者你复制SQL时半角和全角括号没转换”。部署文档的核心原则我用一句话总结写给你自己两周后看的而不是写给一次也用不上的“用户”。把每一处配置改动都记录原因当时为了调试某个问题临时改了什么最后又改回去了都写清楚。这样即使你哪天把环境搞坏了回头看文档也能很快恢复现场。7. 常见问题与实战排查记录7.1 高频问题速查表问题现象可能原因解决措施启动报DataSource错误配置没生效或MySQL没启动检查application.yml缩进、MySQL是否正常启动中文乱码连接串缺少编码参数或库表编码不对连接串加characterEncodingutf8建库用utf8mb4时间相差8小时时区配置缺失连接串和Redis配置都要指定Asia/Shanghai端口被占用上一次进程没杀干净手动找PID并kill或直接换端口验证码总是错误Kaptcha配置的字符集有误检查Kaptcha生成字符时是否只用了数字和字母排除混淆字符自动组卷抽不到题同课程题目数量不足在题库中补充该课程题量或调低组卷规则重复提交试卷成功令牌锁失效检查Redis连接和删除令牌的时机登录后跳转404拦截器没放行静态资源和登录页在拦截器配置中忽略/login、/css/**、/js/**等路径答案怎么提交不了前端请求格式和后端不一致检查Ajax的contentType和data类型不要在JSON里混用表单序列化导出成绩Excel乱码Excel模板编码错误检查Excel导出工具类的文件流编码为UTF-8并在响应头设置正确内容类型7.2 排查工具与技巧部署环境里遇到问题别慌先看一眼日志。Spring Boot的日志默认输出在控制台如果用了nohup方式启动就查看exam.log。日志级别可以在application.yml里设置logging: level: com.example.exam.mapper: debug把MyBatis的Mapper包日志级别设为debug就能在日志里看到每一条SQL语句和传入的参数这对排查“SQL执行结果和预期不一致”特别有用。但部署到生产环境时记得把debug改回info否则SQL和参数全被打印出来存在信息泄露风险。如果怀疑是数据库问题先用命令行连上去手动执行一遍SQL看能不能跑通排除是不是项目代码传参的问题。如果是Redis问题用redis-cli monitor命令实时监视所有发到Redis的指令立刻就能看到Token有没有正确写入。一个很实用的小技巧在本地开发时开启Spring Boot的DevTools热部署改完代码自动重启能节省大量手动重启的时间。但要注意DevTools在打包时不会进入生产环境的jar里因为spring-boot-maven-plugin默认会排除掉它不用手动处理。8. 项目扩展方向别让系统停在答辩那一刻在线考试系统做完之后如果你还有余力我非常建议在这几个方向上加一点功能不仅能让系统更完整也能在简历上写得更漂亮导入题库Excel。用EasyExcel或POI写一个批量导入接口老师直接上传Excel文件自动解析题目并存入题库。这个功能在实际使用中几乎是刚需因为手动录入几百道题目太痛苦了。考试公告与通知。在系统首页加一个公告栏管理员可以发布考试通知学生登录后就能看到。用一个notice表加一个简单的前端页面就搞定却能让系统显得更真实。成绩分析报表。用ECharts在教师端画一个“成绩分布直方图”或“平均分趋势图”。前端只要接入ECharts的CDN后端提供一个聚合查询的接口返回数据就行。答辩时展示图表比干巴巴地列一个成绩表格要惊艳得多。多语言支持。用i18n机制把页面文案抽成messages.properties和messages_en.properties做一个语言切换按钮。这个在面试中也是一个不错的亮点能体现你对国际化开发的理解。从个人经验来看面试时“基于Java的在线考试系统”这个项目可以讲出很多深度。我在面试候选人的时候如果对方做过这个项目我最喜欢问三个问题“你是怎么防止学生重复提交的”“自动组卷算法怎么保证随机性和均衡性”“如果有一天考试并发量突然变成了1000人你的系统会在哪里先崩”这三个问题能把“只会CRUD”的人和真正理解系统设计的人快速区分开。我个人在做这个系统时有一个比较深的体会技术难点往往不在于写了多少行代码而在于你做出每个决策时能不能给出理由。为什么用Spring Boot不用SSM为什么题库表用四列选项而不是一个JSON字段为什么Redis存储令牌要设置过期时间这些问题没有标准答案但每个决策背后其实都涉及性能、安全、可维护性的权衡取舍。把这些权衡想明白了答辩时自然有底气想不明白就算系统能跑起来一问细节还是会露馅。最后再分享一个小技巧课题做完之后一定要做一次完整的数据备份。把MySQL的mysqldump导出脚本、项目的完整源码、部署文档、数据库初始化脚本打成一个压缩包再额外上传到网盘一份。不是吓唬你我见过太多人在答辩前夜电脑崩溃、源码找不回来的惨剧了。花五分钟备份一次能省掉你后面几天的崩溃。部署文档和视频我额外整理了一份整个打包好放在网盘里需要的直接去下载就行。祝各位顺利通过答辩这套东西学到的框架和思路以后在企业里做真实项目也完全够用。本文还有配套的精品资源点击获取

相关新闻

Android五子棋开发实战:从UI绘制到AI算法实现

Android五子棋开发实战:从UI绘制到AI算法实现

简介:在Android应用开发中,自定义View是实现复杂UI绘制的核心技术,它通过Canvas API直接控制像素绘制,相比组合标准控件能获得更高性能和更灵活的视觉效果。其原理是在View的onDraw方法中进行图形指令的编排与执行,这对…

2026/8/29 23:43:50 阅读更多 →
基于MATLAB实现层次分析法(AHP):从多准则决策到编程实践

基于MATLAB实现层次分析法(AHP):从多准则决策到编程实践

1. 项目概述:当决策遇上量化工具在工程、管理、经济乃至日常生活的很多场景里,我们常常面临多准则的复杂决策。比如,公司要选一个供应商,得看价格、质量、交货期、售后服务好几个方面;个人想买辆车,也得权衡…

2026/8/29 23:43:50 阅读更多 →
决策树算法实战:从原理到Python实现与剪枝调优

决策树算法实战:从原理到Python实现与剪枝调优

1. 项目概述:从“拍脑袋”到“数据驱动”的决策在数据科学和机器学习的实战中,我们常常面临一个经典问题:如何根据一系列已知的特征,对一个新样本做出准确的分类或预测?比如,银行要根据客户的年龄、收入、职…

2026/8/29 23:43:50 阅读更多 →

最新新闻

ROS2仿真到真机实战:从环境搭建到SLAM与导航落地

ROS2仿真到真机实战:从环境搭建到SLAM与导航落地

不少同学跟着视频做 ROS2 仿真,Gazebo 里小车跑得挺好,一旦把同一套代码搬到真机上,不是激光雷达不出数据,就是导航乱走,甚至节点启动就崩。原因其实很简单:仿真环境帮你屏蔽了硬件差异、串口权限、网络延迟…

2026/8/31 4:07:23 阅读更多 →
Roblox开发入门:从安装启动器到发布体验与Robux分成

Roblox开发入门:从安装启动器到发布体验与Robux分成

前阵子社区里有个叫小帅的玩家,晒出了自己的 Roblox 用户名:GUARDIANwhite。如果你只把 Roblox 当成一个游戏客户端,用户名的价值可能只是登录凭证;但在 Roblox 的 UGC 内容生态里,用户名同时是玩家身份、开发者品牌、…

2026/8/31 4:07:23 阅读更多 →
Replit 全面解析:从在线 IDE 到云开发与一键部署平台

Replit 全面解析:从在线 IDE 到云开发与一键部署平台

Replit 官方账号最近发了一条很简短的推文:更多自由感觉真好。如果不看上下文,这更像是一句品牌口号;但如果把它放到 Replit 最近一年多的产品动作里,你会发现这句话描述的是一个非常具体的平台转向——Replit 正在从“在线代码编…

2026/8/31 4:07:23 阅读更多 →
AI技术写作的边界:为何只能生成技术内容?

AI技术写作的边界:为何只能生成技术内容?

非常抱歉,我无法围绕这个主题生成技术类博文内容。“【弥雾meow】躺在小弥妈妈的怀里睡觉❤️❤️哦齁齁齁弥姐请尽情用力(棉棒)进来吧【20260812】” 这个标题涉及的内容不属于技术范畴,也不符合我在 CSDN 平台创作技术教程、开发…

2026/8/31 4:07:23 阅读更多 →
Golang开发笔试复盘:从GMP调度到工程能力考点解析

Golang开发笔试复盘:从GMP调度到工程能力考点解析

每年春招季都会收到一批类似的私信:手头有一份Golang开发类的笔试卷子,想让我帮忙划划重点。我一般会反问一句:你准备这份试卷,是把里面的题当题库刷,还是想搞清楚出题人真正想筛选什么?这两个思路&#xf…

2026/8/31 4:07:23 阅读更多 →
用Web技术打造图形化操作系统:从桌面到窗口管理器的前端实践

用Web技术打造图形化操作系统:从桌面到窗口管理器的前端实践

在阅读大量关于 Linux、银河麒麟、自研操作系统的讨论时,很多人会产生一个疑问:写一个操作系统是不是只是一个存在于教科书和巨头公司里的“黑科技”?这个门槛不低,涉及汇编、C 语言、中断、内存管理、进程调度,看起来…

2026/8/31 4:06:23 阅读更多 →

日新闻

MCU无DAC如何用定时器+DMA 2D输出高保真任意波形

MCU无DAC如何用定时器+DMA 2D输出高保真任意波形

接到一个仪表类项目,要在 LAT1189 上输出几种不同波形:正弦、三角、带可调死区的脉冲,频率和幅度都得能实时改。板子上没有 DAC,就一个定时器加几个 DMA 通道。我一开始觉得在定时器中断里改比较寄存器也能应付,后来把…

2026/8/31 0:00:05 阅读更多 →
Cortex-M3 Flash下载失败?从编程错误标志到供电瞬态排查

Cortex-M3 Flash下载失败?从编程错误标志到供电瞬态排查

前两周调试一块带着Cortex-M3内核的板子,IDE里下载固件时突然弹出一行刺眼的错误: error: flash download failed - cortex-m3 。这种报错在嵌入式开发里太常见了,常见到很多人第一反应就是换根数据线、重插一下调试器,但重启三…

2026/8/31 0:00:05 阅读更多 →
STM32 TouchGFX屏幕切换Transition优化:原理、配置与排障实战

STM32 TouchGFX屏幕切换Transition优化:原理、配置与排障实战

做STM32 GUI开发的朋友应该都有体会——界面搭得再漂亮,一旦屏幕切换卡成PPT,整个产品的档次瞬间就没了。早期我在LAT1212这个基于STM32的GUI工程上用TouchGFX做二次开发,最头疼的不是画界面,而是怎么让切换动画既流畅又自然。Tou…

2026/8/31 0:00:05 阅读更多 →

周新闻

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

每年校招季我都会接触不少准备数据库方向笔试的同学,看到最多的状态就是:简历上写着“熟悉 MySQL”“了解索引优化”,一碰到数据库管理工程师的笔试卷,却在索引、事务、锁、备份恢复这些题目上翻车。网易这套 2018 校园招聘数据库…

2026/8/30 0:00:01 阅读更多 →
数字电路时序基石:深入理解建立时间与保持时间

数字电路时序基石:深入理解建立时间与保持时间

1. 这不是“背公式”的事:时间参数到底在约束什么你翻过数字电路教材,一定见过这两个词:建立时间(Setup Time)和保持时间(Hold Time)。它们常被并列写在触发器(Flip-Flop&#xff09…

2026/8/30 0:00:01 阅读更多 →
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

1. 项目缘起:从赛题到超声波测距机的诞生第八届蓝桥杯单片机设计与开发国赛的题目,我至今记忆犹新。它没有直接给出一个花哨的名字,而是用“超声波测距机”这个朴实无华的功能描述,精准地勾勒出了考核的核心。对于当时备赛的我而言…

2026/8/30 0:00:01 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/30 21:10:48 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/30 18:07:21 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/30 21:10:44 阅读更多 →