
为什么要分层
Spring Boot 开箱即用,但不会自动帮你划边界。常见坏味道:
- Controller 里
if/else业务几十行; - Service 返回
Map<String,Object>裸奔; - Repository 被多个 Service 复制粘贴 SQL。
分层的目的是:每层只回答一类问题,改 UI 协议不动领域规则,换存储不动业务。
经典三层职责
| 层 | 回答的问题 | 禁止事项 |
|---|---|---|
| Controller | HTTP 怎么进、怎么出 | 不写事务、不拼 SQL |
| Service | 业务规则与用例编排 | 不感知 HttpServletRequest |
| Repository | 数据怎么读写 | 不写业务 if/else |

依赖方向
依赖只能 自上而下(或经由接口反转):
Controller → Service → Repository → DB
DTO/VO 在 Controller 与 Service 之间转换;Entity 尽量不出 Service 边界(对外用 DTO)。

包结构示例
com.example.blog
├── controller # AdminPostController, PublicPostController
├── service # PostService + impl
├── repository # PostRepository (JPA / MyBatis)
├── domain # Post, Category 实体
├── dto # CreatePostRequest, PostListItem
├── config # Security, Redis, Jackson
└── exception # 统一异常与 @ControllerAdvice
横切关注点放哪
- 鉴权:Spring Security Filter +
@PreAuthorize,不在 Controller 手写 token 解析。 - 事务:
@Transactional标在 Service 入口方法。 - 审计日志:AOP 或事件监听,避免散落在每个 Controller。
- 缓存:Service 层
@Cacheable,key 设计随用例走。
与 NestJS 单体对照
本博客后端是 NestJS 模块化单体(modules/blog、modules/agent),思路一致:Controller 薄、Service 厚、Infrastructure 隔离。Java 团队可把 Nest 的 module 近似映射为 Spring 的 package + @Configuration。
小结
Spring Boot 分层不是教条,而是 变更成本最小化:协议变、规则变、存储变,三者尽量解耦。先画依赖图再写代码,比「先跑起来再说」少还很多技术债。
参考:Spring 官方文档 · 作者工程实践总结。