
实训结束了趁记忆还热乎赶紧把这段经历沉淀下来。先说结论这期东软实训给我的感觉不像学校里的课程设计更像提前经历了一遍企业级项目的完整节奏——从需求文档评审到代码入库从接口联调到上线部署每一步都有严格的规范卡着。3月4号这个节点对我们这批学员来说既是一个阶段性的验收也是一次从“会写代码”到“懂工程”的思维转换。这篇文章我打算按照实训模块的推进顺序把每个环节里我认为最有价值的东西拆开来讲既记录我自己的收获也希望能给后面参加类似实训的同学一些参考。内容上会覆盖实训内容的整体设计、核心技能点、实操过程中踩过的坑、常见问题排查以及我对架构设计和工程化的一些体会。如果你也在准备或正在经历企业级项目实训这篇文章应该能帮你少走不少弯路。1. 实训模块的整体设计思路1.1 实训定位从知识灌输到项目实战的切换东软这个实训项目最核心的目标其实不是教你某个具体框架怎么用而是帮你建立一套完整的工程化思维方式。学校里我们写代码往往拿到题目就开始撸表结构随手建代码一层层往上堆能跑起来就算成功。但实训第一天带我们的项目经理就定了一条铁规矩先想清楚再动手所有开发行为必须围绕需求和设计文档展开。整个实训周期被划分为需求分析、概要设计、详细设计、编码实现、测试交付五个阶段。每个阶段都有明确的输入和输出物阶段之间还有评审环节。这种做法我一开始觉得繁琐后来才意识到企业里开发一个系统不会让你像写课后作业那样自由发挥所有决策都需要有据可循。实训的定位就是提前让你适应这种“带着镣铐跳舞”的节奏。特别值得一说的是实训基地的项目并非玩具型Demo而是从真实企业需求裁剪下来的业务场景。比如我们组拿到的是一个工单管理系统的简化版本虽然简化了但核心链路是完整保留的工单创建、任务分配、状态流转、超时提醒、报表统计。这套链路背后涉及的角色权限、流程状态机、消息通知机制都是实际工作中天天要打交道的东西。做完这个项目再看那些教学型的CRUD示例确实会觉得太单薄了。1.2 为什么选择企业真实业务场景作为载体实训选型背后有很现实的考量。如果只做一个教学用的图书管理系统学生确实能快速上手Spring Boot加MyBatis但很难理解为什么一个看似简单的系统里需要搞出那么多张表、那么多层抽象。企业场景完全不同它的业务逻辑足够复杂复杂到你不得不去思考解耦、复用、扩展性。拿工单状态流转来说这不是一个简简单单的update语句就能解决的。工单的状态有新建、已分配、处理中、已解决、已关闭状态之间不是随便跳的哪些角色能把工单从A状态迁到B状态迁移的时候要不要记录操作日志要不要触发通知这些都是业务规则。如果不在设计阶段把状态机画清楚编码阶段就等着不断返工吧。这种真实业务场景的好处还在于它能逼着你去做取舍。实训中期项目经理给我们追加了一个需求工单要支持批量导入。有人说直接解析Excel然后循环insert就行了但后来评审时发现如果导入的数据里有十万条加上校验逻辑这个接口很可能会超时。于是大家开始讨论分批处理、异步导入、消息队列这些方案。虽然我们在实训里最终只选择了分批插入加异步通知的简化方案但这个过程让我们见识到了一个看似简单的功能放在真实业务里会牵扯出多少考量。1.3 实训目标拆解技术、流程、协作三维度我们这次的实训目标并不是单一的技术提升而是围绕三个维度展开的我个人觉得这个设计比单纯敲代码更有价值。技术维度上要求掌握企业级开发中常用的技术栈Spring Boot作为基础框架、MyBatis作为持久层、MySQL作为存储、Redis用来做缓存和分布式锁、Vue作为前端框架。这些技术单项拆开我们基本都学过但把它们整合到一个项目里就完全是另一回事了。比如Spring Boot的自动配置省去了大量XML配置但出了问题排查起来也更隐蔽你不知道它到底帮你做了什么。流程维度上强调按软件工程规范推进项目。每个阶段都要有对应文档代码必须提交到Git仓库并遵循分支管理规范前后端联调需要先定义好接口文档。这些流程上的东西之前在学校里几乎没有实际操作过实训算是把这一课补上了。协作维度上实训采用小组制四人一组角色有组长、后端开发、前端开发和测试。这个分工不是摆设因为最终评分里有一项就是组内协作情况。代码冲突怎么解决接口变更怎么同步进度落后怎么协调这些问题的处理方式直接决定了项目能不能按时交付。2. 实训中的核心技能拆解2.1 Spring Boot项目工程化不只是能跑这次实训里我对Spring Boot的理解加深了不少。以前写Spring Boot项目无非就是建个Controller、Service、Mapper三层然后启动起来看看Swagger文档能不能通。但在实训的项目规范里光是工程结构就有讲究。我们遵循的是maven多模块结构父子模块划分得清清楚楚。父模块只管理依赖版本子模块按照业务域划分比如说common模块放公共工具类和统一返回结果system模块管用户角色权限workorder模块管工单核心业务。这种拆法的好处是各模块之间职责清晰后续要扩展或者复用都方便。我后来复盘时觉得模块划分这步确实值得多花时间因为一旦中途发现模块边界不清晰改起来的成本非常高。统一返回格式也是实训里反复强调的点。我们封装了一个Result类定义了code、message、data三个字段成功返回200业务异常返回自定义的错误码。这个设计看似简单但实际联调时才体会到它的好处前端拿到响应先看code不用去猜各种异常情况调试效率提升了不止一倍。异常处理这块我们使用了RestControllerAdvice来做全局异常处理。一开始有同学不理解说每个方法里try-catch不就行了但真正实践下来try-catch会导致代码里充斥着大量异常处理逻辑分不清主次。用全局异常处理器把业务异常和系统异常分开处理代码干净日志也容易排查。提示如果你在实训或者实际项目中负责搭建工程框架务必把统一响应、全局异常、参数校验这三件事放在最开始做。这三件事看起来琐碎但后面的所有功能开发都会依赖它们前期不搭好后期到处打补丁。2.2 数据库设计从范式到反范式的权衡数据库这块是我这次实训收获比较大的一个方面。学校教的是范式设计规范、标准、不冗余。但实训的项目里真按照严格的第三范式来设计表结构有些查询会变得非常复杂。我们组在设计工单列表查询时就遇到了这个问题。工单表本身存储了工单的基本信息但列表页需要展示创建人姓名、分配人姓名、处理人姓名这些信息如果全部通过关联查询去取一个列表要关联三四张表数据量一大性能肯定出问题。后来项目经理给了我们一个思路在工单表里冗余存一个创建人姓名字段虽然违背了范式但列表查询只需要查单表速度和写法都简单很多。当然冗余字段也不是乱加的只有查询频繁、更新概率低的字段才适合这么做。索引设计是另一个重点。说实话我之前写表结构时基本不主动建索引都是用主键查询。实训里有个场景按照工单状态和优先级筛选工单数据量测试时只有几千条感觉不到差异但按照项目预期一年可能要积累几十万条工单。我们给状态和优先级字段建了联合索引再用慢查询日志验证效果立竿见影。这个过程让我理解了为什么企业里做开发都要时刻绷着一根性能的弦。事务和锁的问题实训里虽然没有特别复杂的场景但我们在实现工单分配这个功能时遇到了并发问题。两个组长同时给同一个处理人分配了不同的工单结果在处理人看来工单的待办列表出现了错乱。排查下来发现就是工单分配时没有加锁。后来我们用了Redis分布式锁才解决具体细节我在后面的问题排查章节里细说。2.3 前后端协作接口文档先行的价值实训进行到联调阶段时我们组经历了一次不小的教训。前端同学和后端同学各自开发约定接口时只是口头说了说结果真正联调时发现字段名对不上、数据类型不一致、有的接口缺少必要参数光这些对齐就花了两天时间。后来带教老师给了我们一个模板要求必须按照接口文档来定义所有前后端交互。从此以后联调效率有了质的提升。具体来说接口文档需要定义清楚请求路径、请求方式、请求参数名称、类型、是否必填、说明、响应结构code、message、data里data的具体格式、错误码含义。我们用Swagger来做自动生成配合注解把接口信息写清楚前端直接通过Swagger页面查看接口定义。这个做法在企业里非常普遍实训里算是一次提前体验。接口版本管理这个问题也是联调时意识到的。项目中途有一次后端同学要改一个接口的字段结果前端还在用旧字段名。如果没有版本管理改接口这事就得靠群里吼很容易出问题。后来我们约定接口变更要提前在群里通知并且改动前先发一版变更说明。虽然这只是一个实训项目的小约定但让我养成了接口变更必同步的意识这对后面进入真实团队工作非常重要。3. 实操过程与关键环节记录3.1 需求评审一个需求文档引发的讨论实训开始后我们做的第一件事不是打开IDEA写代码而是坐下来读需求文档。带教老师给我们发了一份工单管理系统的PRD文档要求组内逐条评审把有疑问的地方标注出来。刚开始大家都很懵觉得文档写得挺清楚的有什么好评审的。结果仔细读下来问题还真不少。比如工单的紧急程度分为低、中、高、紧急四个等级文档里只说了不同等级的处理时限不同但没有明确超过时限应该怎么处理。是自动升级提醒还是人工介入又比如工单关闭后是否允许重新打开文档里没有说明就产生了歧义。这些问题如果不揪出来做到一半再返工代价非常大。这次评审让我明白了一个道理对需求的疑问不能在脑子里过一下就完事必须落到文档里通过正式渠道去确认。在评审中老师一直强调换位思考“如果你是使用这个系统的一线客服人员你希望这个操作是三步完成还是五步完成”这种思维方式对我影响还挺大的。写代码的人有时候容易陷入技术视角觉得功能实现了就行。但真正做产品用户使用体验是非常关键的。我们后来在实现工单批量操作功能时专门去考虑了勾选、批量分配、确认提示这些细节虽然技术上不复杂但用户体验完全不一样。3.2 编码规范让代码走在可维护的路上实训过程中我们执行了一套编码规范包括命名规范、注释规范、校验规范。这些东西在学校里基本没人在意但实训里它是会被检查的。比如说类名要用名词方法名要用动词POJO里不能出现基本类型要用包装类型因为基本类型有默认值在接收前端传参时容易出现NPE问题。我们被要求写单元测试这个当时确实挺痛苦。测试代码量甚至比业务代码还多而且写的时候要想各种边界情况参数为null怎么办字符串超长怎么办数据不存在怎么办。但写完之后我改代码时确实有底气了跑一遍测试就知道有没有改坏别的功能这比手动点接口测试效率高得多。代码评审环节也让我学到很多。刚开始做评审时大家都比较含蓄不好意思提意见。后来老师要求每次评审必须至少提出三个问题在这种压力下大家开始认真看别人的代码。我记得有一次评审我发现某个同学的SQL查询在循环里执行这种写法性能隐患很大于是建议改成一次性查询后在内存里组装。这个问题放到面试里可能就是所谓的“是否了解N1问题”的考点。实训里通过代码评审发现这类问题印象比面试时背书深刻多了。3.3 版本控制与分支管理Git的正确打开方式实训第二天就开始使用GitLab了而且带教老师强调所有代码必须通过合并请求合入主干分支禁止直接push到master。说实话以前自己项目里都是一个人在一个分支上随便提交哪来的分支管理意识。实训里经过这段时间的磨合才真正理解了分支管理的意义。我们的分支策略比较简单master分支保证稳定可发布develop分支作为集成分支每个功能从develop切出feature分支开发完成后合回develop经过集成测试后再从develop合并到master。这种策略跟企业里用的Git Flow很像虽然简化了一些但核心思想是一致的。实际执行中碰到最痛苦的事情就是代码冲突。我们组有两个同学同时改了工单实体类一个新增了字段另一个修改了字段类型合到一起时冲突提示让人头疼。老师给了我们建议一是尽可能拆分任务边界避免两个人同时修改同一文件二是提交代码前先pull最新代码并解决冲突不要等到合并时一次性面对大范围冲突。有了这两条策略后面的协作确实顺畅了很多。3.4 联调与测试质量保障的最后一道防线前后端联调阶段虽然前面说过因为接口文档问题折腾过但把规范和流程理顺之后联调本身还是很顺利的。我们用Postman批量测试接口再配合Swagger文档核对字段前后端同学对着同一份接口定义沟通成本最低。测试这块实训里引入了基本的测试观念开发自测不是随便点两下就算完而是要覆盖正常流程、异常流程、边界值三种情况。比如登录接口正常情况是用户名密码正确返回token异常情况是密码错误返回提示边界情况是用户名为空或密码为空时的处理。我们组在测试时还真发现了一些问题比如用户名的长度没有做校验数据库字段是50个字符但前端输入框没有限制如果输入100个字符插入数据库时就会报错。类似这样的问题只有按照测试用例来测才会暴露。注意如果你以后在工作中遇到前端传参导致后端数据库异常大概率就是后端没有做参数校验。开发接口时务必对入参加上校验注解这是实训教会我的最实用的习惯之一。4. 常见问题与排查技巧实录4.1 循环依赖引发的启动失败实训刚开始集成Spring Boot和Spring Cloud的各组件时我们遇到了项目启动失败的问题。错误信息提示某个Bean创建失败看起来是循环依赖。网上搜一下就能找到解释但在实训中我发现真正的问题在于代码设计层面Service层互相调用太随意没有遵循单向依赖的原则。排查过程是这样的先看日志里的Bean创建栈找到循环依赖的入口然后理清楚A依赖B、B依赖C、C依赖A的链路最后通过重构把公共逻辑下沉到底层Service中或者抽取出独立的服务接口打破了循环。这个过程中我意识到循环依赖不只是改个配置就能糊弄过去的设计合理的依赖关系才是治本之策。4.2 Redis分布式锁在并发场景下的应用前面提到工单分配时的并发问题这里详细讲一下我们的解决方案。最初的代码是这么写的查询工单状态如果状态为待分配则把工单的处理人设为当前操作人。但这段代码在多线程并发执行时会出现两个请求同时查到工单状态为待分配然后都执行更新导致同一张工单被分配给两个人。解决思路是加锁。刚开始我们想到的是数据库层面用select for update来做行锁但老师说这种场景在分布式环境下要考虑分布式锁。于是我们引入了Redis的SETNX命令来实现简单的分布式锁在分配工单前先获取锁获取成功的线程才允许执行分配逻辑执行完毕后释放锁。同时给锁设置了过期时间防止线程执行过程中崩溃导致死锁。这段实操经历让我对并发的理解有了质的提升。之前在学校里学多线程只是理论层面知道线程安全问题但真正遇到实际业务场景才发现并发问题不是靠理论就能完全预见的必须结合具体场景来分析。分布式锁虽然代码量不大但里面的细节很多锁的粒度怎么定义过期时间设多长锁续期怎么做都是值得思考的问题。4.3 线上问题排查从日志到定位的过程演练实训中间有一次测试同学反馈工单列表加载非常慢。我们一开始以为是网络问题后来经排查发现是查询没有走索引。解决办法是在时间字段上建索引并把查询条件里的函数去掉改成范围查询。这次的排查过程很有意义因为我们是从现象出发逐步缩小范围最后定位到问题的根因——这个链路将来在工作中一定是家常便饭。我还总结了一个排查问题的心法不要盲目猜要看数据。问题发生时要先收集信息错误日志、请求参数、返回结果、数据库当前状态。根据信息做第一轮分析提出假设然后用下一步的数据验证或推翻假设。这个过程就像侦探破案逻辑要严密每一步都要有依据。实训培养了我的排查思维我觉得这是比具体技术更重要的收获。4.4 常见错误速查实训中容易踩的坑我把实训过程中的一些常见错误整理了一下方便后边参加实训的学员参考。这些问题其实也是实际开发中高概率遇到的提前知道有好处。错误类型具体现象根源分析解决方案空指针异常调用getter时报NPE从Redis中取出的对象为null未做判空使用Optional或增加判空逻辑并发覆盖更新工单状态被旧数据覆盖更新时未带版本号或状态条件使用乐观锁更新时校验版本号数据库连接耗尽系统卡顿、接口超时数据库连接池配置过小调整最大连接数并排查慢SQL前端传参格式错误后端接收不到参数JSON里字段名与Java属性不一致使用JsonProperty或规范命名内存溢出服务崩溃重启列表查询一次性加载全部数据改造为分页查询控制查询条数缓存与数据库不一致查询结果与数据库不一更新数据库后未主动更新缓存采用先更新数据库再删除缓存的策略这张表的每一条在实训中都有对应的真实场景。比如说缓存与数据库不一致我们是在实现工单状态更新时遇到的问题因为工单详情查询走了Redis缓存但状态更新直接改了数据库结果详情页读到的还是旧状态。这种问题很隐蔽光看日志根本不好发现需要具备“缓存一致性”的思维才知道去排查。5. 关于架构与工程化的延伸思考5.1 为什么说“大厂经验”离我们并不远很多人一说起架构设计就觉得那是架构师的事离自己很远。但实训过程中我发现在写每一个功能模块时都在做架构决策只是自己没有意识到而已。比如选择在哪个层做参数校验、要不要用DTO而不是直接暴露实体类、接口设计成同步还是异步这些都是架构师每天在思考的问题。我们实训项目里有一个很小的例子工单创建接口最开始直接接收一个大的JSON对象所有字段一次性传上来。后来需求变更创建工单时要支持附件上传如果继续走同一个接口传输效率会非常差。后来我们把接口拆成两步第一步上传附件获取附件ID第二步创建工单时引用附件ID。这个调整涉及到了接口设计原则也涉及到了数据流设计。我当时就在想这不就是架构设计里常说的“单一职责”吗只是学校里的教学代码从来不会让你体会这一点。5.2 从实训项目到真实项目的差距实训项目和真实企业项目相比还是有差距的。最大的差距是用户量和数据量。实训里的系统最多模拟几千个用户但真实的线上系统动辄几万甚至几十万并发。在这种量级下很多在实训中不会暴露的问题都会显现出来比如说缓存穿透、缓存雪崩、数据库连接池耗尽、接口幂等等。但实训的价值在于它给了一个应对复杂问题的思维框架知道发现问题时从哪里开始排查知道设计时应该考虑哪些潜在风险。另一个差距是业务的复杂性。实训项目虽然用了企业场景但毕竟不是真实业务很多坑没有踩到。比如权限系统我们只做了简单的角色判断但真实企业的权限系统可能涉及组织树、数据权限、操作权限、字段权限等多层设计。虽然没做过那么深但实训过程中至少让我知道了权限设计是有套路的这些认知算是很宝贵的积累了。5.3 如何把实训收获迁移到长期能力中实训结束之后我有意识地把实训里学到的东西归纳整理形成了自己的知识体系。我做了几件事第一把实训项目里的关键代码和文档整理成个人项目放到GitHub上作为后续面试的作品集第二把实训中遇到的问题和解决方案写成了博客方便复盘第三针对实训中暴露出来的薄弱环节比如算法、并发编程做定向补强。这些做法推荐大家尝试。实训的核心收获如果只停留在脑子里过一两个月就淡了。但如果你把过程中的思考沉淀成文档和代码它就成了可以长期复用的资产。尤其是实训中那种“自己踩坑又爬出来”的经历复盘的价值远高于那些顺顺利利完成的部分。6. 实训过程中的软技能沉淀6.1 技术沟通怎么把问题说清楚实训对沟通能力的锻炼也是一个重要的隐性收获。在项目过程中向老师汇报问题、向组员解释技术思路、在评审会上陈述设计选择这些都是很好的沟通训练。刚开始汇报问题时我总是描述得很含糊“接口报错了查一下。”结果被老师反问哪个接口报什么错请求参数是什么日志信息是什么从那次以后我汇报问题时都会按照“现象、影响范围、初步排查、需要什么帮助”的结构来组织效率高了很多。这种能力在开发工作里太重要了。技术沟通不是把人拉过来看你的屏幕而是要在短时间内让对方抓住问题的核心。实训正好给了我们反复练习的机会虽然不是专门的沟通课但确实是在实战中打磨出来的。6.2 项目复盘持续改进的底层能力实训的最后一天我们做了项目复盘。老师给了我们一个复盘框架做得好的、做得不好的、下一步需要改进的。每一部分都要求具体到例子不能只写空话。这种复盘方式给了我很大启发因为它不是简单做总结而是引导你去挖掘问题背后的深层原因。比如我们组复盘时发现进度延后的关键原因是需求评审阶段过于乐观低估了工作量。这个结论如果只是在心里想可能就只停留在“低估了”这个层面。但结合具体例子复盘就会发现其实是因为在评审时没有把每个功能点的实现细节都想清楚。下次做估算时我就会把功能点拆得更细每个功能点给出明确的完成标准后再估算。这种改进是真正能落实到行动中的。7. 个人心得与升级建议实训当中有几个点是我自己觉得做得比较好、值得保持的。首先是主动求助的意识。实训过程中遇到问题不要死磕太久先自己尝试解决如果半小时没有进展就带着已有信息去问老师或同学。这比坐那硬扛一上午效率高得多。其次是带着目标去学。每次实训开始前先想清楚这次实训你希望获得什么是某项具体技术还是工程流程经验还是团队协作体验目标明确之后整个实训过程中你会更有意识地去吸收相关内容。如果想在实训中获得更多的成长这里还有几条建议可以参考。一是不要满足于“功能实现”每次做完一个功能想一想能不能做得更优雅、更高效、更可维护二是多做记录不要等到实训结束后才去回忆每天花十分钟记录当天解决的问题和学到的知识点最后汇总起来就是一篇高质量的实训总结三是多关注别人的实现方式组员的代码、其他组的方案、老师的讲解代码每一条都可能隐藏着你没有掌握的知识点。实训是一次信号很密集的学习经历是真的把课堂知识放到项目里检验了一遍。对我而言3月4日这个节点项目跑通的那一刻带教老师点头的那一下组员击掌的瞬间都是整个实训里最珍贵的画面。希望后面参加实训的同学也能珍惜这种难得的实战机会获得属于自己的收获。