Java 后端
Spring Boot 分层架构:别让业务逻辑困在 Controller 里
用一条设备工单创建链路解释分层、依赖倒置与事务边界如何共同保护业务复杂度。
核心洞察:分层不是目录命名游戏。它的价值在于让变化沿着清晰方向传播,而不是让框架细节渗入每一条业务规则。 霓虹港平台收到异常振动告警后,需要创建维修工单。这个动作看似只是一个 HTTP 请求,实际上牵涉权限、设备状态、工单编号、持久化和事件通知。若这些判断都写进 Controller,测试会依赖 Web 环境,规则也无法被其他入口复用。
四层分别保护什么
- Interface / Controller:把 HTTP 输入转换为 Command,完成认证、格式校验和响应映射。
- Application:编排用例,控制事务,调用领域对象和端口。
- Domain:表达不变量,例如“停用设备不能新建工单”。
- Infrastructure:实现 Repository、消息、数据库和第三方接口。 依赖应始终向内。领域层不知道 JPA、Redis 或 Spring 注解;基础设施通过接口适配领域需要的能力。这样,工单规则才能在 REST、定时任务或消息消费入口下保持一致。
一个用例的最小骨架
@Transactionalpublic WorkOrderId handle(CreateWorkOrder command) { Device device = deviceRepository.require(command.deviceId()); WorkOrder order = WorkOrder.open(device, command.reporter(), clock.instant()); workOrderRepository.save(order); domainEvents.publish(new WorkOrderOpened(order.id())); return order.id();}Application Service 不应该填满业务细节。Device 与 WorkOrder 负责规则,Repository 隐藏存储细节,事件发布则把通知、索引更新等非核心副作用从主事务路径中剥离。
DTO、Command、Entity 不是同一种东西
HTTP DTO 为接口服务,可以为了前端方便而扁平;Command 是用例的明确输入;Entity 有身份和生命周期;VO 描述不可变概念,例如告警等级。把它们混为一个类,会导致数据库字段、接口字段和业务规则互相牵制。 尤其不要直接把 JPA Entity 返回给前端。除了泄漏内部结构,还可能在序列化时触发惰性加载,使一个简单列表演变为 N+1 查询。
事务边界应覆盖业务一致性
“创建工单并占用设备”必须同时成功或失败,因此放在一个应用层事务中。发送短信、写搜索索引这类可重试副作用,不应拖慢核心提交;它们更适合由领域事件在事务完成后处理。
| 变化 | 应停留的层 |
| 请求字段格式变化 | Controller / DTO |
| 工单状态转换规则 | Domain |
| 用例编排与事务 | Application |
| MySQL 改 PostgreSQL | Infrastructure |
讨论与反应