Spring Boot在线考试系统实战:从表结构到并发判分全解析

发布时间:2026/10/20 14:26:12
Spring Boot在线考试系统实战:从表结构到并发判分全解析
做在线考试系统这个项目是我带某高校实训项目时被学员问得最多的一套代码。说实话网上能下载的考试系统源码不少但大部分要么是单体JSP老古董要么只实现了最简单的答题交卷离真正能上线的考试系统还差着十万八千里。基于Spring Boot的在线考试管理系统之所以值得写一篇完整拆解是因为它几乎涵盖了企业级开发的所有基本功权限管理、事务控制、数据表设计、并发处理、文件操作、前后端联调。无论你是准备毕业设计、跳槽项目经历还是想系统梳理Spring Boot实战技能这套系统的设计思路都能直接拿走用。这篇文章不是源码粘贴而是把我实际开发这套系统时的完整思考过程写出来从功能模块划分、核心表结构设计到自动判分的边界情况处理再到并发交卷时踩过的坑每一块都是文档里不会细讲、但真正动手必然会遇到的东西。看完你不仅能跑通项目还能知道每张表、每个接口为什么这么设计遇到同样需求时能举一反三。1. 为什么选它作为实战项目从需求到技术栈的匹配1.1 在线考试系统的天然复杂度很多人低估了考试系统的业务复杂度觉得无非就是出一套题、考生选答案、最后算分。真做起来完全不是这么回事。一个完整的在线考试系统要同时面对三类角色管理员需要维护题库、创建试卷、安排考试场次、查看成绩统计考生需要在规定时间内完成答题、应对断网续考、提交试卷阅卷老师需要对主观题进行人工评分并处理申诉。这三条线交织在一起业务状态就变得非常复杂。比如一场考试的生命周期就有未开始、进行中、已结束、已阅卷、已发布成绩五个阶段每个阶段能做什么操作、谁有权限操作都需要在代码层面严格约束。再比如试卷和试题的关系一套试卷可以包含单选题、多选题、判断题、填空题、简答题每种题型的判分逻辑都不一样。这些复杂度天然适合用一个成熟的后端框架承接Spring Boot就是很合适的底座。1.2 Spring Boot为什么是合适的底座选择Spring Boot不是因为它“流行”或者“大家都在用”而是因为它确实解决了考试系统开发中的几个关键痛点。第一是快速整合组件。考试系统必然涉及数据库访问、用户认证、接口文档、文件导入导出。Spring Boot的起步依赖和自动配置机制能把MyBatis Plus、Redis、Swagger、EasyExcel这些组件在极短时间内接入项目省去大量XML配置和兼容性调试时间。第二是生态成熟。像用户登录这块Spring Security或Sa-Token都有现成方案操作日志可以用AOP统一切面实现。遇到问题时能搜到的解决方案数量级完全不一样这对学习者和中小团队来说非常重要。第三是部署友好。考试系统经常要部署在本地服务器或者课程实训环境中Spring Boot内置Tomcat容器一个jar包直接启动避免了外部容器配置的麻烦。我实训时有一台配置很低的旧服务器照样稳定扛住了几百人同时在线答题。当然也有需要自己做权衡的地方。我的建议是如果你是从零开始学用Spring Boot加MyBatis Plus加MySQL的组合是最稳的技术栈足够主流出了问题社区里答案也最多。如果在真实的高并发考试场景下比如几千人同时交卷还要再加Redis缓存预热题库、消息队列削峰但那是后话了本文先聚焦核心架构。2. 功能模块全拆解考生端、管理端、阅卷端2.1 考生端核心流程考生端是考试系统的门面用户感知最直接。我们设计的考生端流程分为六个步骤登录验证、查看考试列表、进入考试、答题、交卷、查看成绩。登录验证这里有个容易被忽略的细节考试系统不仅要验证账号密码还要绑定该考生的身份信息和考试场次。我采用的方式是登录成功后把用户ID、姓名、所属班级存入Redis并生成一个token后续所有接口都从token里解析用户信息而不是每次查询数据库。这样既减少数据库压力也能在服务端方便地做登录状态管理。进入考试前的前端校验尤为重要。考生点击“开始考试”时后端必须做三件事检查当前时间是否在允许的考试时间段内、检查该考生是否已参加过本场考试、检查试卷是否处于可用状态。这三项都是防止考生绕过前端直接调接口作弊的基础防线。答题过程的交互设计也需要后端接口配合。考生每答一题前端会调用一次保存答案的接口。这里我没有使用实时保存到数据库的策略而是先保存到Redis。为什么因为考试过程中考生会频繁切换题目、修改答案如果每次都直接写数据库会产生大量无意义的写操作而且一旦数据库连接池被占满整个考试系统都会卡住。Redis里我用一个hash结构保存考生的答题记录key是“exam:userId:paperId”field是题目IDvalue是考生答案的JSON串。考生每切换一道题前端会先把当前题目答案发给后端缓存后端正向查询时直接读缓存只有交卷时才一次性写入数据库。这套方案在实际测试中表现非常稳定也最大限度降低了数据库压力。2.2 管理端题库、试卷、考试安排管理端的核心是题库管理、试卷管理和考试管理三个模块。题库管理的重点是支持多种题型和灵活的题目属性。每一道题除了题干和答案还要维护所属课程、难度等级、知识点标签、创建时间、最后更新时间。这里我设计了一张subject表存题目一张subject_option表存选项因为不同题型的属性差异较大——选择题有选项判断题只有答案简答题有参考答案和给分要点用统一字段存反而麻烦。试卷管理是最容易踩坑的地方。如果纯手工从题库里一道题一道题选工作量很大而且容易出错。我做了两种组卷方式手工组卷和随机组卷。手工组卷就是管理员从题库列表中勾选题目加入试卷随机组卷则是设置规则比如单选题抽10道、多选题抽5道、总分100分系统自动匹配题目。随机组卷的实现看起来简单但实际要考虑题目不重复、难度比例均衡、每次生成的试卷不一致等问题这个我在第四章细讲。考试管理负责创建考试场次。一场考试要绑定一份试卷、设置开始时间和结束时间、指定参与考试的班级或学生名单、设置考试时长。场次和试卷的关系我设计成一对多一份试卷可以被多个考试场次复用但每个场次绑定唯一一份试卷。这样便于同一套题在不同班级分别组织考试。管理端还有一个不可忽略的能力Excel批量导入题目。手动录入几百道题会让人崩溃我在系统里实现了基于EasyExcel的批量导入功能并提供了标准模板下载。做这一步时有个坑模板中涉及到题型字段必须严格枚举校验否则导入时数据错乱很难排查所以我加了导入错误行的详细日志回显让管理员能快速定位是哪一行哪个字段出了问题。2.3 阅卷端与成绩分析阅卷端是很多人设计时容易遗漏的部分。客观题系统自动判分没有问题但简答题、论述题这种主观题必须人工评分。系统需要为阅卷老师提供一张待阅卷列表点击进入后看到考生答案和参考答案然后录入得分。这里要处理好三个状态未阅卷、阅卷中、已完成。我采用一个独立的score_detail表记录每道题的小分总成绩表只存最终总分。这样即使阅卷过程中出现误操作也能精确回退到某一道题重新评分而不是整场考试重新处理。成绩分析模块对学校来说几乎比考试本身更重要。系统需要统计每场考试的平均分、最高分、最低分、及格率、优秀率还要能把考生成绩按分数段分布展示出来。这些统计如果每次实时用SQL聚合数据量大时性能会很差。我的做法是交卷和阅卷结束时异步更新一份考试成绩统计表查询时直接读取统计结果。这里的异步没有引入消息队列而是用的Spring自带的Async注解加线程池对中小规模系统来说完全够用。3. 数据库设计几张核心表撑起整个考试业务3.1 题库与试卷的关系数据库设计是整套系统的地基表结构一旦设计不合理后期写业务逻辑会处处别扭。我最终的设计一共用了12张表其中核心的几张拿出来重点说。题库方面有两张表题目表subject和选项表subject_option。subject表包含id、subject_name、subject_type枚举1单选2多选3判断4填空5简答、subject_answer、score、difficulty、course_id、create_time等字段。选项表通过subject_id关联题目包含option_content和option_code。这里我特别提醒一点答案字段subject_answer存的是选项编码组合比如单选题存“A”多选题存“A,B,C”判断题存“T”或“F”。统一用这个规则判分逻辑会非常好写。试卷与题目的关联我单独建了一张paper_question表而不是在题目表里加paper_id字段。为什么因为同一道题可以出现在多套试卷里而且在不同试卷中的分值可能不同。paper_question表的核心字段是paper_id、question_id、question_order、question_score。其中question_order控制题目在试卷中的显示顺序question_score覆盖题目的默认分值。这种多对多关联表的设计是试卷系统最核心的扩展点。下面是我建表时的核心DDL片段里面加了必要的索引CREATE TABLE paper ( id BIGINT NOT NULL AUTO_INCREMENT, paper_name VARCHAR(100) NOT NULL COMMENT 试卷名称, total_score INT DEFAULT 0 COMMENT 总分, duration INT DEFAULT 0 COMMENT 考试时长分钟, create_by BIGINT COMMENT 创建人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE paper_question ( id BIGINT NOT NULL AUTO_INCREMENT, paper_id BIGINT NOT NULL, question_id BIGINT NOT NULL, question_order INT DEFAULT 0 COMMENT 题目顺序, question_score INT DEFAULT 0 COMMENT 本题分值, PRIMARY KEY (id), KEY idx_paper_id (paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 答题记录与成绩表的设计要点答题记录表answer_record是交卷时数据落地的核心表。每条记录对应考生对试卷中某一题的作答结果包含exam_id、user_id、question_id、user_answer、is_correct、question_score、actual_score这几个关键字段。这里actual_score和question_score分开是有讲究的question_score是题目满分actual_score是考生实得分客观题两者要么相等要么为0主观题则可能由阅卷老师填写中间值。成绩表exam_score保存考生的总分和考试状态核心字段包括exam_id、user_id、total_score、correct_count、wrong_count、status。status字段标识成绩状态从待判分到已发布有一个流转过程。这里有个非常重要的设计细节为了防止同一考生重复交卷导致成绩表出现多条记录我在exam_id和user_id上建立了联合唯一索引。代码层面虽然也有判断但数据库层面的唯一约束是最后一道防线。我实训时就遇到过学生连点两次交卷按钮接口被重复调用的情况如果没有这个唯一索引成绩数据就背污染了。另外考试场次表exam与成绩表exam_score是一对多关系。exam表存的是考试名称、开始时间、结束时间、关联试卷ID、创建人exam_score通过exam_id关联场次通过user_id关联考生用户。由于一场考试有两个班级同时参加的情况我没有在exam表里直接存班级ID而是单独建了exam_student关联表维护参与关系。这样设计更灵活考试分配学生名单时也能单独操作。CREATE TABLE answer_record ( id BIGINT NOT NULL AUTO_INCREMENT, exam_id BIGINT NOT NULL, user_id BIGINT NOT NULL, question_id BIGINT NOT NULL, user_answer TEXT, is_correct TINYINT DEFAULT 0, question_score INT DEFAULT 0, actual_score INT DEFAULT 0, PRIMARY KEY (id), KEY idx_exam_user (exam_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我见过不少人把考生答案直接拼接成一个大字符串存在一张表里这样虽然表少但后续按题目分析得分率、查看某一道题哪些人答错时会非常痛苦。所以宁可多花一点空间拆成明细表也不要为了省事牺牲系统后续的可扩展性。4. 关键业务逻辑实现组卷、答题、交卷判分4.1 随机组卷的实现思路随机组卷看起来就是个“从题库里随机取题”的问题实际实现时要留意几个坑。首先是题库范围控制。随机取题必须限定在管理员指定的课程范围内而且要主动排除被标记为禁用状态的题目。我的实现是用MyBatis Plus的QueryWrapper组装条件把course_id、status作为过滤条件然后用last(ORDER BY RAND() LIMIT count)做随机抽取。其次是分值控制的精确性。比如满分100分的试卷单选题每题2分抽10道多选题每题3分抽5道判断题每题2分抽10道简答题每题10分抽3道加起来正好100。如果管理员在配置时不改分值那么系统要能自动根据抽题数量计算每题的默认分值保证总分正好等于试卷设置的总分。我的策略是优先使用管理员在组卷条件里填写的每题分值如果没填再用默认规则分摊剩余总分。第三是重复问题。如果同一个题库里的题目数量不太充足两次随机组卷生成的试卷可能包含大量重复题目。我在实现时加了限制每次随机组卷都必须保证同一道题不会被重复抽取。并且extra的SQL里写明了限定条件确保抽题时不带出已被逻辑删除或下线的题。这里给出一段简化版的随机组卷核心代码用MyBatis Plus的条件构造器实现public ListQuestion randomSelectQuestions(Long courseId, Integer questionType, Integer count) { LambdaQueryWrapperQuestion wrapper new LambdaQueryWrapper(); wrapper.eq(Question::getCourseId, courseId) .eq(Question::getQuestionType, questionType) .eq(Question::getStatus, 1) .last(ORDER BY RAND() LIMIT count); return questionMapper.selectList(wrapper); }这里要说明的是ORDER BY RAND()在题目表数据量达到几十万条时性能会明显下降。如果题目数量特别大的场景建议先查出符合条件的ID集合再用随机算法取指定数量的ID最后通过IN查询回表取出完整题目信息。这个优化我在系统后期也做了效果明显。4.2 交卷与自动判分的核心代码交卷判分是整个系统里并发风险最高的环节也是我打磨最久的地方。交卷时分两种情况考生主动点击交卷以及考试时间到了系统强制交卷。先说强制交卷。很多初学者会忽略这个场景以为只要前端倒计时结束自动提交就行。实际上如果考生在最后几秒钟浏览器卡顿或者直接关闭了浏览器前端根本来不及提交。所以后端必须有一个定时任务每分钟扫描一次进行中的考试场次把那些已过结束时间但尚未交卷的考生数据做强制提交。我实现时用了Spring的Scheduled注解每分钟执行一次扫描任务。交卷接口的事务处理核心代码如下要特别注意事务边界和幂等控制Transactional(rollbackFor Exception.class) public ExamScoreVO submitExam(SubmitExamDTO dto) { // 1. 校验考试状态和是否已交卷 ExamScore existing examScoreMapper.selectOne( new LambdaQueryWrapperExamScore() .eq(ExamScore::getExamId, dto.getExamId()) .eq(ExamScore::getUserId, dto.getUserId())); if (existing ! null) { throw new BusinessException(请勿重复交卷); } // 2. 获取Redis中的答题记录 String redisKey exam: dto.getExamId() : dto.getUserId(); MapObject, Object answerMap redisTemplate.opsForHash().entries(redisKey); // 3. 遍历答题记录逐题判分 int totalScore 0; int correctCount 0; ListAnswerRecord recordList new ArrayList(); for (Map.EntryObject, Object entry : answerMap.entrySet()) { Long questionId Long.valueOf(entry.getKey().toString()); String userAnswer entry.getValue().toString(); Question question questionMapper.selectById(questionId); boolean isCorrect checkAnswer(question, userAnswer); int actualScore isCorrect ? question.getScore() : 0; if (isCorrect) { correctCount; } AnswerRecord record new AnswerRecord(); record.setExamId(dto.getExamId()); record.setUserId(dto.getUserId()); record.setQuestionId(questionId); record.setUserAnswer(userAnswer); record.setIsCorrect(isCorrect); record.setQuestionScore(question.getScore()); record.setActualScore(actualScore); recordList.add(record); if (isCorrect question.getQuestionType() ! 5) { totalScore actualScore; } } // 4. 批量保存答题记录和成绩 answerRecordMapper.insertBatch(recordList); ExamScore examScore new ExamScore(); examScore.setExamId(dto.getExamId()); examScore.setUserId(dto.getUserId()); examScore.setTotalScore(totalScore); examScore.setCorrectCount(correctCount); examScore.setWrongCount(recordList.size() - correctCount); examScore.setStatus(questionHasSubjective(recordList) ? 0 : 1); examScoreMapper.insert(examScore); // 5. 清除Redis缓存 redisTemplate.delete(redisKey); return buildExamScoreVO(examScore); }这个逻辑里最容易被忽略的是第3步中的isCorrect question.getQuestionType() ! 5这个条件。主观题在交卷时只能先算0分必须等阅卷老师评分后用update语句把actual_score和total_score改掉。如果一开始就把主观题按0分计入总分阅卷后再去改总分就会非常麻烦。判断答案是否正确的方法checkAnswer按题型分别处理。单选题和判断题直接比较用户答案和标准答案字符串是否相等。多选题则要先对用户的选项顺序做归一化处理再比较因为考生可能选“A,C”而标准答案是“C,A”按字符串比较就会误判。private boolean checkAnswer(Question question, String userAnswer) { String standardAnswer question.getAnswer(); if (question.getQuestionType() 2) { // 多选题排序后比较 String[] userArr userAnswer.split(,); String[] stdArr standardAnswer.split(,); Arrays.sort(userArr); Arrays.sort(stdArr); return Arrays.equals(userArr, stdArr); } return standardAnswer.equalsIgnoreCase(userAnswer.trim()); }多选题少选怎么算分这个规则不同学校差异很大有的是少选给一半分有的是少选不给分。我做了可配置化处理在系统参数表里维护了一个multi_select_half_score字段允许管理员在创建考试时指定是否允许少选得部分分。这个看似细枝末节的需求实际是系统上线后被咨询最多的功能点之一。4.3 主观题人工阅卷的流程设计主观题的判分流程涉及多张表的状态联动。交卷完成后包含主观题的考试成绩状态是“待阅卷”此时成绩表中的total_score只是客观题部分的得分。阅卷老师进入阅卷列表看到的每个考生卡片上应该标识“客观题得分X分待阅题目Y道”。阅卷接口接收阅卷老师给每道主观题打的分数然后逐题更新answer_record的actual_score同时重新累加该考生的总成绩。这里必须放在同一个事务里避免出现题目分数改了但总成绩没更新的情况。我就是用了Transactional保证两个操作要么都成功要么都回滚。还有一个细节已经完成阅卷的考试是否可以修改申诉重判我在系统里增加了“申请重判”的功能入口。考生如果觉得分数不对可以提交申诉管理员重新打开阅卷页面修改某道题的得分系统会自动重算总分并记录操作日志。这个功能虽然简单但对提升系统实用性帮助很大我实训时不止一个老师专门提起这个需求。5. 实际开发中踩过的坑与规避方案5.1 并发交卷导致成绩数据错乱这个问题是我在压力测试时发现的。当时用脚本模拟了100个用户同时交卷结果出现了部分考生成绩表里出现两条记录的情况还有个别考生答题记录丢失。排查下来发现问题出在“先查重再插入”这个操作不是原子的。两个请求同时走到查重逻辑数据库里都没有记录然后同时执行插入就双双成功。虽然我设计了联合唯一索引但MyBatis Plus的默认insert没有对冲突做处理异常被吞掉了导致其中一条记录插入失败但事务却没有回滚干净。修复方案是双管齐下代码层面加同步锁确保同一考生同一场考试的交卷请求是串行的数据库层面把联合唯一索引加上万一代码漏了数据库也会直接报错而不是产生脏数据。ALTER TABLE exam_score ADD UNIQUE KEY uk_exam_user (exam_id, user_id);这段经验给我的教训是涉及幂等操作的接口永远不要只靠代码层判断数据表约束和唯一索引才是最后的兜底。尤其是考试系统这种数据正确性要求极高的场景宁可多设计一层约束也不要心存侥幸。5.2 多选题选项顺序导致的判分误判第二个印象深刻的坑来自多选题判分。当时测试人员反馈选“A,B”的考生被判错但标准答案确实是“A,B”。问题出在某些前端框架会把多选题的选项数组序列化成JSON顺序可能因为用户点击的先后顺序而变化。比如用户先选B再选A存储的答案是JSON数组[“B”,“A”]和标准答案[“A”,“B”]即使内容相同字符串比较也无法通过。这个问题暴露了我判分逻辑里的一个设计盲区只考虑的标准答案的字符串形态没有对用户的作答做统一的归一化处理。最终的修正在checkAnswer方法里增加了一段排序逻辑将用户答案的选项编码拆出来排序后再比较。也是从这次之后我在设计所有数据格式时都会下意识问一句这个内容可能存在多种等价表示吗如果有就必须做归一化。5.3 前端倒计时与后端交卷时间不一致考试系统的倒计时功能是最容易引发争议的地方。第一个版本以浏览器端倒计时为准结果有学生用浏览器调试工具改掉了本地时间在考试结束后还能继续答题。第二个版本改为完全以后端时间为准又出现了前端倒计时因为网络延迟而跳秒考生觉得时间不准。我的最终方案是混合模式后端在进入考试接口时一次性返回考试截止时间的时间戳前端倒计时基于这个时间戳做本地递减而不是从服务器反复获取。这样即使中间出现网络波动倒计时也不会漂移。同时后端每个保存答案的接口都会校验当前请求的时间是否已超过截止时间一旦超时就拒绝保存并强制交卷把时间作弊的路径完全堵死。这样实现后考生如果断网重连倒计时能准确恢复到剩余时间同时后端也始终是最终裁决者。这也涉及一个业务设计原则凡是涉及考试公平性的关键决策点都必须以后端校验为准。6. 拿到源码和文档后怎么用部署、运行与二次开发建议6.1 环境准备与快速启动很多人下载了源码之后第一步就卡在环境搭建上。这套Spring Boot在线考试管理系统的标准运行环境是JDK 8或以上、Maven 3.6、MySQL 5.7或8.0、Redis 5.0。IDE方面推荐IDEA直接导入Maven工程。启动步骤归纳为四步。第一步是初始化数据库。项目里有一个sql目录按文件名顺序执行即可。注意MySQL 8.0的时区问题。如果你是刚装好的MySQL 8.0建议先在连接串里加上serverTimezoneAsia/Shanghai否则数据库连接会报时区错误。这是新手最容易忽略的坑。第二步是修改配置文件。主要改application.yml里的数据库账号密码和Redis地址。Redis没有密码就留空有密码就填上。如果Redis没启动项目启动时会报连接超时这一点一定要先确认。第三步是启动Redis。Windows下直接启动redis-server.exeLinux下用systemctl启动redis服务。没有Redis的话答题缓存和登录token都无法工作。第四步是启动项目然后访问在线文档地址查看接口文档并验证登录接口。默认账号的密码在README里通常写得很清楚分别用管理员、教师、学生的角色各登录一次确认三个角色的权限区分是否正确。6.2 二次开发的扩展方向跑通只是第一步这套系统的扩展空间很大以下几个方向值得你根据自己的需求继续深挖。第一个方向是题库抽取算法的优化。目前是随机抽取加简单限制如果你是计算机类的考试可能希望按知识点权重抽取比如“二叉树”知识点占比30%、“排序算法”占比20%。这种需求可以在组卷配置里增加知识点权重表然后改为按权重比例分配抽题数量。第二个方向是考试防作弊能力的提升。可以集成摄像头定时拍照、切屏检测、IP限制、异常行为日志分析。这些功能并不需要推翻现有架构考生端在答题过程中定时上传环境信息即可。后端把异常记录保存在独立表中提供给老师作为考试异常判断依据。第三个方向是成绩分析报表的丰富化。现在系统提供的是基础统计你可以扩展雷达图展示考生各知识点掌握情况或者对同一份试卷在不同班级的得分分布做对比分析。这些图表用前端图表库实现起来并不困难关键是后端要把答题明细记录的结构保留好我现在设计的answer_record表正好能支持这种分析。第四个方向是手机端适配。现在的前端页面主要面向PC浏览器如果你要做移动端考试建议直接抽出一套接口给微信小程序或App调用。后端接口设计时已经按无状态JSON方式提供迁移成本很低主要是前端页面要重新写一套。做这个项目的过程中我还有一个很深的感受源码和文档能给你的是已实现的方案但真正让你成长的是把项目跑起来之后自己动手改需求、加功能、排除bug的整个过程。我记得实训班的某位同学拿到源码后第四天就自己加了一个“考试公告”功能虽然只是底部加了一行通知但那个过程里他真正理解了模板引擎渲染、表单提交、数据回显这三个概念之间的联系。这种收获是看任何文档都换不来的。如果你在部署或二次开发中卡住了优先按这样的排查思路走先看控制台有没有异常堆栈定位到具体哪一层报错再看是不是配置文件里的连接地址或账号密码问题最后检查Redis是否启动、数据表是否初始化完整。我发现绝大多数启动失败的问题最后都能归到这三类原因上。把这套排查思路内化成自己的习惯你调试任何Spring Boot项目都会顺手很多。