Spring Boot 项目结构的几个习惯

用 Spring Boot 做后端时,项目结构看似自由——官方文档也不会强制你用什么目录名。但一个人写和三个人写,完全是两种体验。团队一旦扩大,「约定」比「灵活」更重要:新人clone 下来,应该不用问「这个类放哪」就能开始干活。

下面是我会在项目里坚持的几个习惯。不追求大而全的架构,只解决日常开发里反复出现的摩擦。

分层清晰,职责单一

典型的调用链是 controller → service → mapper/repository,三层不要混用:

  • Controller: 接收请求、参数校验、调用 Service、封装响应。不写业务逻辑,不直接操作数据库
  • Service: 业务编排、事务边界、领域规则。一个 Service 方法对应一个明确的业务动作
  • Mapper / Repository: 数据访问。SQL 或 JPA 查询收敛在这里,不散落在 Service 里拼字符串

违反这条最简单的信号是:Controller 里出现 if/else 业务分支,或者 Service 里直接 return ResponseEntity。看到这种代码,通常意味着该拆了。

包结构按业务划分

除了按技术层分包(controllerservicemapper),我更倾向在每一层里再按业务模块划分子包:

com.example.project
├── user
│   ├── UserController
│   ├── UserService
│   └── UserMapper
├── order
│   ├── OrderController
│   └── ...
└── common
    ├── exception
    └── config

好处是改「用户模块」时,改动范围基本锁在 user/ 目录里,Code Review 和冲突概率都更低。common/ 只放真正跨模块共享的东西,避免变成杂物间。

配置外置,环境可切换

数据库连接、Redis 地址、第三方 API Key、日志级别——全部放进 application.ymlapplication-{profile}.yml,通过 spring.profiles.active 切换。

几条硬规矩:

  • 密码和密钥绝不提交到 Git,用环境变量或配置中心注入
  • 本地开发用 dev profile,测试环境 test,生产 prod——配置差异只体现在 profile 文件里
  • 魔法数字(超时时间、分页大小)也写进配置,不要散落在代码常量里

Spring Boot 的 @ConfigurationProperties 很适合把一组相关配置绑成一个类型安全的类,比满项目 @Value("${xxx}") 好维护得多。

统一异常处理

@RestControllerAdvice 集中处理业务异常、参数校验失败和未捕获错误,返回统一的 JSON 结构:

{ "code": 40001, "message": "用户不存在", "data": null }

业务层抛自定义异常(比如 BusinessException),携带错误码和提示信息;Advice 负责映射成 HTTP 状态码和响应体。前端对接时只需要认一种错误格式,不用猜每个接口返回的字段名。

参数校验用 @Valid + @NotBlank 等注解,校验失败同样走统一处理,返回字段级的错误提示。

DTO 与实体分离

数据库实体(Entity)不要直接暴露给前端。至少准备两层对象:

  • Request DTO: 接收入参,带校验注解
  • Response VO: 返回给前端,只包含需要的字段,隐藏内部 ID 和敏感信息

实体和 DTO 之间的转换可以用 MapStruct 或手写 mapper,别在 Controller 里一个个 set。字段一多,手动赋值迟早出错。

接口文档不要省

集成 SpringDoc(OpenAPI 3)或 Swagger,让接口在开发阶段就能可视化调试。个人项目可以轻量化,但以下不要省略:

  • 每个接口的用途说明(@Operation
  • 请求/响应字段的含义和示例值
  • 错误码对照表

文档不是给别人看的——三个月后的你自己,就是文档的最大受益者。

日志与可观测性

几条实用习惯:

  • 入口日志记录关键参数(脱敏后),出口记录耗时和结果摘要
  • 业务异常用 warn,未预期异常用 error 并打印堆栈
  • 给每个请求加 traceId(拦截器生成),方便串联日志

别在循环里打 info,别把整个对象 JSON 序列化进日志——磁盘和 ELK 都会感谢你。

测试策略

不追求 100% 覆盖率,但以下必须有:

  • Service 层核心逻辑的单元测试(Mock 掉 Mapper)
  • 关键接口的集成测试(@SpringBootTest + 测试数据库)
  • 参数校验边界 case(空值、超长、非法格式)

测试代码也是项目结构的一部分,放在 src/test 下镜像主代码的包结构,找起来不费劲。

好的项目结构不是约束创造力,而是减少重复决策的消耗。

这些习惯不华丽,但能在项目从「能跑」走向「能维护」的过程中省掉大量沟通成本。如果你也在用 Spring Boot 搭后端,挑几条适合自己的坚持下去,比一次性套用某种「标准架构」更实际。