尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

SpringBoot项目分层实践,让业务代码更清晰

SpringBoot项目分层实践,让业务代码更清晰 你接手过多少个“看起来能跑一改就炸”的SpringBoot项目代码堆在Service里方法体几百行日志和业务混在一起DTO和实体互相乱塞改一个字段要牵连三张表。分层的初衷是让代码边界清晰现实中却往往变成了包结构上的层层嵌套、逻辑上的混沌一锅。别再自欺欺人了分层的核心不是把类放进不同的package而是把变化的方向和节奏隔离在不同的维度里。边界不是包名是依赖规则很多团队以为建了controller/service/repository包就算分好层了。结果Controller里直接注入了MapperService里偷偷操作HttpServletResponseRepository返回了Map然后强转成对象。这叫什么这叫形式主义分层。真正的分层必须用依赖方向来约束上层只能依赖下层的接口下层绝不能反向依赖或旁路依赖。在SpringBoot项目里最直接的落地方式就是强制禁止Controller访问Repository也不允许Service出现Repository以外的数据访问对象。如果你发现Service里既调Repository又调FeignClient既写Redis又发MQ那这个Service其实已经变成了一个“垃圾中转站”它不是业务层而是集成层。要打破这种局面首先要承认一个事实Controller是HTTP协议翻译官Repository是数据存储的适配器而Service是业务规则的执行者。它们各司其职的前提是彼此不知道对方的具体存在。比如Controller只接收DTO调用Service接口返回一个VO它不需要知道这个业务是查MySQL还是调外部API。而Repository只负责聚合根或实体的持久化它不需要理解“下单”是什么意思只知道“插入一条订单记录”。这个规则一旦被打破任何微小的改动都会沿着错误依赖链爆炸式传播。Service层的“瘦身手术”从业务编排到领域服务最常见的病态Service长这样先验证参数再查库存接着扣减然后生成订单发消息通知最后返回结果。所有步骤的代码都是过程式顺序编写中间夹杂着try-catch和事务注解。代码倒是能跑但没有任何结构——业务规则被淹没在实现细节里阅读代码的人不得不把每一行挑出来拼装脑内流程图。对此我建议把Service拆成两层应用服务Application Service和领域服务Domain Service。应用服务负责用例编排、事务边界、权限校验、事件发布领域服务则专注于单一业务规则的不变量。比如“下单”这个用例应用服务里会调库存领域服务校验可扣减、调订单领域服务创建待支付订单、调支付网关发起支付——每个领域服务只处理自身模型内的逻辑不关心谁调用它。SpringBoot的容器管理正好支持这种分层把ApplicationService和DomainService都注册成Bean通过接口注入。还有一个要命的习惯Service层直接使用实体类作为入参和出参。这导致实体被JSON序列化、被数据库代理、被业务逻辑胶水反复污染。实体应该只存在于Repository和领域层之间Controller和ApplicationService之间流动的应当是DTO或Command对象。不要嫌DTO类多字段复制麻烦。用MapStruct或Record就能轻松转换带来的收益是接口稳定、内部实现可随时摇摆。你不可能今天改一下实体字段明天让前端接口也跟着变。Repository层不是SQL堆砌是持久化策略的封装有些代码仓库里Repository接口和MyBatis的Mapper混在一起方法名叫selectByUserIdAndStatus返回一个Map。你管这叫数据访问层这充其量是SQL的换行格式。Repository的第一条戒律它必须面向领域模型提供资源获取和存储方法而不是暴露查询条件。好的Repository接口里应该是findOrderById、save(Order order)、delete(Order order)这种有业务语义的操作。至于是用JPA还是MyBatis是走MySQL还是TiDB是单表查询还是多表关联那是持久化实现的事不应该传染到接口签名上。实际的复杂性在于企业项目里大量“查询”操作并不严格属于某个聚合根。比如一个订单列表页需要显示订单号、用户名、商品名、支付状态——跨了四张表。如果硬要按聚合根来设计Repository会变得非常别扭。我的建议是把“读写分离”贯彻到Repository设计里。写操作走基于聚合根的Repository读操作走专门的QueryRepository或ViewObject查询接口。这不是破坏分层而是承认查询场景拥有自己独立的副驾驶。于是在代码里你可以定义一个OrderQueryRepository它只做查询返回特定的DTO视图。它和OrderRepository放在同一个数据库访问模块里但逻辑上属于不同的“流”。SpringBoot的多数据源配置、MyBatis的namespace隔离都可以为此服务。这样一来写模型和读模型各自演进查询优化不会破坏业务封装的稳定性。Controller层磨平HTTP的棱角你见过Controller里写HttpServletRequest request然后手动从request.getParameter里取值的吗都2025年了别再让业务代码和Servlet容器藕断丝连。Controller层的唯一职责是把HTTP请求转换为业务入参并把业务结果翻译成HTTP响应。它应该无业务逻辑无事务无异常处理。参数校验可以在Controller层预执行但校验逻辑不止是“非空”和“长度限制”更包括那些跨字段的业务规则——这些规则依然要下沉到Service层去判断。比如一个创建订单的接口Controller只做四件事接收请求体、调用一个ApplicationService方法、捕获调用过程中的异常并映射为一个状态码、返回响应结构。至于订单金额怎么算、库存怎么扣Controller一概不知。有人担心多一层会造成性能损耗实际上一次方法调用纳秒级开销远小于你背锅的代价。分层牺牲的微末性能换来的是团队协作时心智上的巨大宽裕。Controller层还负责一个隐藏角色协议版本的管理。随着前端迭代接口可能面临v1/v2共存。如果Controller和Service混成一个类版本升级就会像动手术一样危险。保持Controller轻量、独立你就可以轻松新增一层Controller而让底层ApplicationService保持原样不动——这对线上兼容是救命的。事务边界分层中最容易翻车的暗礁SpringBoot里一个Transactional放在Service方法上就万事大吉了吗如果你把事务注解放在Controller或Repository上那就是灾难。事务的本质是“业务用例”的原子性它天然属于应用服务层。需要注意事务方法不能通过this调用否则Spring代理失效事务内不能做远程调用、发消息、执行重IO操作——因为那会拉长数据库连接占用。好的分层会倒逼你把长事务拆成短事务把外部交互挪到事务提交后的事件监听里。假设你要“支付成功”后发短信通知用户。如果这个动作放在事务里万一短信服务超时订单就回滚了用户支付成功却看到订单未支付合理的设计是应用服务里开启事务只做订单状态更新、写入支付流水然后提交事务之后通过Spring的事件机制收到PaySucceededEvent再异步发送短信。这样分层之后事务内只碰数据库事务外搞副作用清晰且靠谱。异常处理不要让catch块污染业务代码分层不意味着每一层都去try-catch。很多开发者习惯在Controller里包裹大段try-catch返回一个统一的Result对象。这确实能把异常信息结构化但问题是业务代码里那些throw new BusinessException是经过深思的领域信号而不是普通的程序bug。应该在Controller层设置一个全局异常处理器由它负责将不同类型的异常翻译为HTTP状态码和错误消息。Service层和领域层只负责抛出异常不负责决定前端看到什么文字。SpringBoot的RestControllerAdvice和ExceptionHandler正是为此设计的。你可以在handler里处理BusinessException、ValidationException、DataIntegrityViolationException、AccessDeniedException等。业务层永远不要返回ResultCode.FAIL作为对象来表意而是直接抛异常。这样调用者要么拿到完整的业务结果要么拿到明确的异常分支不存在“成功返回对象里还藏着一个错误码”这种怪胎。分层越彻底异常的类型和来源就越清晰。Repository层抛DataSourceException应用服务层捕获后转换为业务异常或直接上抛领域层抛DomainRuleViolationExceptionController层统一响应。这种层层“翻译”并没有增加多少工作量却能让日志排查从大海捞针变成定点轰炸。模块化把分层的思考延伸到多模块工程你还在把所有的Controller、Service、Repository放在一个maven模块里吗对于小项目可以但一旦团队过了十人、业务线超过三条单体模块就会变成家传老宅——每个房间都能改水管谁也不敢乱动承重墙。将分层思想升级为模块化边界即按业务域拆分为独立的Spring Boot Starter或Maven子模块是破解大饼化项目的必经之路。比如用户模块、订单模块、支付模块各自内部仍有controller/service/repository但模块之间只能通过公开的application服务接口或事件交互不能直接窥探对方的内部表结构。这种“模块内分层模块间隔离”同时带来了另一项红利可以独立演进发布。每个模块有自己的版本号、自己的数据库隔离通过不同的datasource配置或schema甚至可以用不同的架构风格。老模块继续用事务脚本新模块尝试ARActive Record或CQRS都没关系。只要模块的对外API保持稳定内部的翻新就不会伤及无辜。不过要提个醒不要因为模块化了就去搞微服务。微服务是分布式系统决策需要网络、容错、部署、度量等条件支撑。在同一个SpringBoot应用里模块化分层是性价比最高的解耦手段你能够得到清晰的边界而不必支付分布式的代价。等到某个模块确实需要独立弹性伸缩再把它抽出去为服务也不迟。实战案例一段订单创建代码的分层重构我们来看一个反例。原代码在OrderService里写死了从request取userId直接使用OrderMapper插入订单又用ProductMapper减库存还调了couponService返回一个优惠价格最后在同一个事务方法里调了FeignClient发送短信。这代码的问题不用我说你都看得见。现在用分层思维重构。Controller接收CreateOrderRequest只做参数校验然后调用OrderApplicationService.createOrder(command)。ApplicationService启动一个短事务它调OrderDomainService.create创建聚合根调ProductDomainService.deductStock核减库存此时传入一个防超卖版本号然后调CouponApplicationService.consumeCoupon。所有领域服务都不感知HTTP和DB细节它们只操作内存对象并返回结果。最后ApplicationService调用OrderRepository.save(order)持久化事务提交后通过ApplicationEventPublisher发布OrderCreatedEvent。短信监听器收到事件后异步发送。这个结构下每个步骤的归属清清楚楚——当你能在30秒内说清一段代码属于哪个层次分层就有了真正的意义。分层不是银弹但没有分层就是裸奔看清楚一点分层也会增加类数量增加抽象带来一定的学习成本。如果团队平均能力一般硬上DDD式的四层架构反而会玩崩。但无论如何Controller-Service-Repository这种最基础的分层是任何想要长期维护的项目的底线。你可以在Service里再分use case可以在Repository里再加specification但绝不能把Controller写成巨型脚本把Service写成上帝类。所以从今天起审视你的SpringBoot代码浏览器按F12那些接口请求它们进到你的项目之后是轻车熟路到达业务核心还是在层层泥潭里来回打转每个类都该有它不可推卸的职责和不可逾越的边界。当你发现改一个需求需要改动Controller、Service、Repository三个地方的同一个逻辑那不是“全栈”那是设计坏味。让业务代码变得清晰从来不是银弹而是从尊重最小的依赖规则开始。每忍住一次跨层调用每坚持一个DTO转换每把事务从Controller里赶出去你的项目就离“清晰的代码”近了一步。别等代码堆到无法回头的长度再做这件事——你已经知道怎么分了现在就动手。
返回列表