十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用SpringBoot写业务代码时,如何保持结构清晰

用SpringBoot写业务代码时,如何保持结构清晰 代码又堆起来了。你打开那个叫OrderServiceImpl的类滑动鼠标滚轮四千行从createOrder到cancelOrder中间夹杂着validateUser,checkStock,sendNotification, 还有一段注释写着“TODO: 这段逻辑以后要优化”的垃圾代码。你很清楚这不是一个人的错是长期在Controller里写校验、在Service里堆业务、在Utils里塞私活的结果。结构清晰不是天赋是反人性的自律因为偷懒很容易而让代码保持可读性需要持续对抗熵增。我们得承认SpringBoot的快速开发模式是熵增的加速器。自动配置、依赖注入、CRUD Repository让一个刚毕业的实习生也能在一周内把接口跑通。但跑通和写好之间隔着一条由“职责边界”和“依赖方向”划出的鸿沟。很多人以为分了Controller、Service、Mapper三层就叫清晰实际上只是把垃圾从厨房搬到了卧室——分层是起点不是终点。那个被误解的“三层架构”SpringBoot官方教程里给的模板就是Controller-Service-Mapper于是大多数人把这当成宗教。商品查库存Controller里先起一个Result包装然后调ServiceService里Transactional然后调Mapper。看起来没错。但当业务复杂起来你就发现Service层开始吞噬一切参数校验、状态流转、短信推送、消息队列、权限判断、甚至是Excel导出。它变成了一个无所不包的“万能中间层”你的业务逻辑并不在某一个明确的地方而是像雾一样弥漫在整个Service类里。为什么会这样因为三层架构只定义了“分层”没定义“层内如何组织”。更关键的是它默认了业务逻辑就天然归属于Service。于是每个Service方法都在做三件不同性质的事一是判断“这个业务能不能做”规则校验二是执行“这个业务要改哪些数据”数据变更三是处理“做完之后要通知谁”副作用。这三件事纠缠在同一个方法里你拆不出、也测不动。真正的健康结构第一要务是让“决策”与“执行”分离。业务决策是规则比如“订单金额满100才能优惠”“库存不足不能下单”数据执行是CRUD比如“插入订单记录”“扣减库存数字”副作用是协调比如“发送短信”“调用支付网关”。它们不该混在同一行代码里。要想做到这一点你得重新审视一个老掉牙的词事务脚本。很多人对事务脚本嗤之以鼻说它过时了。但SpringBoot项目里90%的写法就是事务脚本——方法A开事务做一堆操作提交。问题不在事务脚本本身而在脚本里的每个步骤是否具备清晰的语义和边界。你可以用事务脚本但你不能让脚本里出现“判断用户是否VIP”和“用String.format拼消息文案”这种八竿子打不着的代码挨在一起。业务逻辑到底应该放在哪里这是一个永恒的问题放在Entity里放在Service里还是另开一个什么DomainService我的建议是先别急着选边站。业务逻辑应该放在“离数据最近且最不会变”的地方。如果一条规则只跟某一个实体本身的状态有关比如Order判断自己是否可取消那它就该是Order实体的方法。如果一条规则牵扯到多个实体比如下单要同时校验用户、商品、优惠券那它才应该被提升为协调性质的“领域服务”。在SpringBoot里我们往往把Entity当成一个纯数据容器加一堆getter/setter业务规则全部写在上层Service里。这就导致实体退化成了数据库表的投影而Service退化成了一堆if-else组成的“决策树”。你问这些规则是否被复用没有。你问这些规则是否可测试得先Mock三个Repository。贫血模型助长了贫血Service而贫血Service又反过来让Entity更加无脑这是一个恶性循环。别误解我并不是让你把整个项目改成DDD。你可以在一个方法里完成80%的操作但你可以让那20%真正体现业务含义的代码住在它该住的地方。比如说订单取消逻辑里有一段“如果订单已支付且发货前退款原路返还”这段规则完全可以抽到Order实体里的isRefundable()方法中。Service只需要调用order.isRefundable()而不是自己写if (order.getStatus() 2 order.getShipTime() null)。这两种写法前者是问业务后者是翻数据库。你写的每一行代码要么在表达业务要么在操作数据——混在一起就是腐败的开始。用“用例”而不是“层”来组织包结构想象一下你刚接到一个需求“新增一个优惠券领取接口”。你打开项目源码发现包结构是controller,service,mapper,model,config。你该怎么办你进入controller新建CouponController进入service新建CouponService进入mapper新建CouponMapper最后在model里放一个Coupon实体。这个流程很顺畅但当你需要理解“领取优惠券”这个完整业务时你必须在五个包之间来回跳而且每个包里都有几十个同级别的类你不知道哪个类跟哪个类有关联。更好的组织方式是用业务用例为顶层维度来分包。比如coupon/receive/下面专门放接收优惠券这个用例的Controller、Request、Service、Converter。而coupon/下面再放这个聚合根共有的Coupon实体、CouponRepository接口。这样当你看到coupon/receive时你就知道这里面的东西都是为“领取”这个动作服务的。高内聚不再是一句口号而是包结构所强制约束的事实。这在SpringBoot里完全可行而且非常简单。你可以把RestController、Service、Repository这些注解照常使用只是不再按照“技术类型”分文件夹而是按照“业务能力”分模块。注意这不是“微服务拆分”而是“代码组织层面的聚合”。一个receive包内Controller只管HTTP协议转换Service只管应用逻辑协调而领域规则放在Coupon或CouponDomainService里。调用关系从controller - service - repository变成了“包内自治”。依赖方向永远从外层向内层内层不知道外层是谁——这就是清淅的骨架。控制器的厚度只做协议翻译Controller经常被骂“胖”或者被夸“薄”但很多人对厚薄的定义很模糊。你的Controller里如果出现了MapString, Object当参数然后手动get(name)再强转那它已经胖得离谱了。一个健康的Controller应该只做三件事解析HTTP参数、调用一个应用服务方法、把结果转换并封装成HTTP响应。其他事比如参数校验、业务异常捕获、脱敏、幂等控制都不应该放在Controller里。有朋友会说不是有Valid注解吗参数校验放在DTO上不就行了吗对但我想提醒你区分“输入格式校验”和“业务规则校验”。NotBlank(message用户名不能为空)是格式校验写在DTO里没问题。而“用户名长度必须在6到20之间”其实也是格式校验也可以。但“该用户名已被注册”或者“用户等级不够不能领券”就是业务规则了这类校验必须委托给Service或领域方法。把业务规则混进DTO的注解里是另一种隐藏的行贿行为因为你会以为校验已经做了实际上它只覆盖了表面。还有一个被忽略的厚度问题响应装配。很多项目里Controller直接返回Entity让Jackson去序列化懒加载属性结果N1问题频出。又有项目用了一个叫BeanUtils.copyProperties的万能工具把Entity和VO互相拷贝但字段名一对不齐就静默出错。我建议你给每个用例定义清晰的“请求对象”和“响应对象”并且在Controller里用一个专门的Assembler类来装配。装配逻辑也是一种业务逻辑它决定了什么能露出去、什么不能。让“变化点”显性化用策略替代万级if业务代码里最脏的不是循环和判断而是暗含在if-else里的隐含策略。比如运费计算“普通用户10元VIP用户5元全场满99包邮”这三个规则如果写成三个if嵌套在Service里那么后续增加“保税仓商品额外加关税”时你只能继续往这个if堆里加条件。比较先进的做法是定义一组ShippingFeeCalculator策略接口然后每个策略一个Spring Bean通过MapString, ShippingFeeCalculator注入。这样新来的规则就是一个新文件不需要改动原有代码。这时候SpringBoot的依赖注入成了你的得力助手。你可以在一个CalculatorRegistry里根据上下文的特征选择策略也可以用Order注解控制优先级。结构清晰的项目往往能看到“多态”而不是“分支”。分支是线性思维的结果多态是面向对象的初心。如果你发现一个Service方法里超过十个if-else而且每个if都是关于同一维度比如状态机、会员等级、渠道来源那就果断考虑拆策略。但请务必注意策略模式不是为了炫耀而用。如果你的分支只有两个且将来大概率不会增加那一个if就够了。过度抽象是另一种混乱。关键是要识别“变化点”——那些业务上预期会频繁追加条件的地方。识别的方法是看你的equals和containsKey出现了多少次。十个状态判断那不如把状态流转画成一张表用StateMachine或者Map来驱动。规则越多越需要一个显式的“规则集合”结构而不是散落的if。事务的艺术别把长流程炖成一锅粥很多Service方法上顶着一个大大的Transactional仿佛要保护一切。但你有没有想过事务的隔离性和原子性是有代价的如果一个方法里既操作了数据库又调用了外部HTTP接口那么整个事务会长时间持有数据库连接而那一次外部调用往往要两秒钟。这期间你的数据库连接池在哭泣。事务应该只包裹需要原子性的数据变更而不是包裹整个业务流程。拆分事务边界是对结构清晰的一大贡献。比如下单用例“校验优惠券”无状态、“扣库存”需事务、“创建订单”需事务、“发送消息通知”非事务。你可以把扣库存和创建订单放在两个独立的事务方法中用TransactionTemplate控制或者在Service内部通过自调用绕过代理时小心使用。很多老码农会告诉你Spring事务代理在同类内部方法上不生效于是他们干脆把整个方法加Transactional图省事。这恰恰是结构不清晰的前兆。事务边界应该和业务用例的原子性边界重合而不是和方法的物理长度重合。更具体地说当一个Transactional方法内出现以下任何一项时你就该醒了调用远程HTTP、发送MQ消息、循环里逐条单查数据库、对集合进行复杂的内存计算。这些都意味着要么事务过长要么你执行了本不属于这一“变更单元”的操作。一旦你意识到事务是限界你自然就会把副作用通知、审计、发布事件挪到事务提交之后去执行——这需要事件机制也就是Spring的TransactionalEventListener。这个监听器能让你的代码从“时序混乱”走向“因果清晰”。事务提交后发事件事件处理器再干脏活主流程就干净了。警惕工具类与“万能Service”的合谋几乎每个项目都会有一个CommonUtils、BizUtils或者OrderUtil里面躺着各种“不想归类”的方法。这些方法往往形迹可疑有的从线程上下文里取当前用户有的做日期格式转换有的将订单号加密有的把List转成树。当这些方法被超过三个地方使用时它们就变成了隐式的共享依赖。你每多一个工具类就多一个公共厕所谁都能进谁也不负责打扫。我并不是说所有工具方法都必须有归宿。像StringUtils,ObjectUtils这种纯静态无状态的方法放工具类无伤大雅。但一旦某个“工具方法”引用了ApplicationContext、或者依赖了某个Mapper、或者需要从Redis里读取配置那它就不是工具而是被垃圾包装的Bean。这时候你要做的是给它一个明确身份它是UserContext还是OrderNoGenerator给它起个正经类名放入对应的业务包中并注入它而不是调用静态方法。一个类的名字就是它的契约叫“Util”意味着它没有契约。万能Service更是如此。OrderService是一个业务入口但如果你发现它还承担了InventoryService的职责、UserService的职责那说明你的服务粒度错了。记住不要用“Service”掩盖集合体的性质。如果一类业务有自己的数据和规则就单独建一个服务甚至一个包。不要怕类多怕的是类里的职责多。类多但职责单一代码就像书架上一排排整齐的书类少但每本都是论文集你每次找内容都得翻目录。结构清晰是写给未来同事的情书你可能会说道理我都懂但项目里别人都那么写我也只好随大流。这大概就是“破窗效应”在代码库里的写照一旦有人开始在一个Service里堆2000行下一个人的心理门槛就会降低到3000行。如果所有人都觉得“反正会乱我也乱写一下”那这个项目就再也无法拥有清晰的状态了。反过来如果你在每次提交代码时都坚持让新增的类只服务于一个变更原因拒绝往已经烂掉的Service里再塞一段逻辑那么即使历史包袱重你至少没有继续加剧它。保持结构清晰没有银弹。SpringBoot给了你快速启动的能力但没给你免于思考的豁免权。你需要做的事情说到底很朴素把业务规则从数据操作里捞出来把编排逻辑从业务规则里剥下来把协议转换从核心逻辑里摘出去。你可以通过包结构强制依赖方向通过策略模式管理变化点通过事件机制隔离副作用通过DTO限定可见数据。你会为此多写几个类多拆几个方法多花半小时思考这个if该放在哪个层——但这半小时的思考能为你和你的同事省下未来无数个凌晨三点的排查时光。代码终究是写给人类看的只是机器碰巧能运行而已。当你下一次打开一个Service准备新加一个方法时先问自己这个功能属于这个类吗这个方法里的每一步都在同一抽象层级吗如果答案是否定的就别急着写先拆。每一次“凑合一下”都是在为下一片混沌投下重磅炸弹。结构清晰从来不是某一次重构的成果而是无数次小决策的组合。今天你少写一个Utils方法明天你少在一个方法里加一个分支后天你为一个计算规则新建了一个策略类——这些微小的坚持才真正定义了代码的最终形态。你不需要发明什么架构也不需要引入什么牛逼的框架。你只需要在每次提交之前用指尖划过那些新增的代码想象一下一个刚入职的同事顺着调用链一路看到这里他会不会觉得这条路是通的、亮的、干净的。如果是那你的SpringBoot项目就已经具备了最昂贵的东西——清晰的结构它是不依赖任何人天赋的可以持续积累的资产。
返回列表