Technical Note

Spring Boot 项目别只会启动,先把边界搭清楚

从 Controller、Service、Mapper、配置和异常处理几个维度,整理一个 Java 后端项目的生产化骨架。

  • Spring Boot
  • Java
  • Architecture

很多 Spring Boot 入门教程会从“创建项目、启动接口、返回 Hello World”开始,这当然必要,但真正进入业务系统后,更重要的问题是:代码边界是否清楚,失败是否可观测,配置是否能被环境接管。

我更倾向把 Spring Boot 项目拆成几条稳定边界。

我的判断: Spring Boot 项目最容易被低估的是边界感。目录结构不是重点,重点是请求、业务、外部依赖、持久化和异常能不能各自站住。

Web 层只做协议适配

Controller 不应该堆业务逻辑。它适合做参数接收、权限注解、基础校验和结果包装。真正的业务规则放到 Service,数据库访问放到 Mapper 或 Repository。

这样做有两个好处:接口协议变化时不影响核心业务,业务规则也更容易写测试。

Service 层表达业务动作

Service 方法名最好接近业务动作,而不是简单叫 save、update。比如批量创建广告时,可以拆成 previewBatchCreate、commitBatchCreate、resubmitFailedCreative。名字越具体,后面排查问题越省心。

配置要能区分环境

连接池、Redis、MQ、第三方 API 地址、超时时间、开关项,都不要写死。开发、测试、生产环境的配置应该独立,并且敏感信息交给环境变量或配置中心。

异常要分层处理

参数错误、业务冲突、第三方接口失败、数据库异常,不应该全部抛成一个 RuntimeException。至少要在统一异常处理里区分错误码和日志等级,否则线上报错会变成一锅粥。

实际场景:批创接口为什么不能写成一个大 Service

在真实业务里,一个“提交批量创建”接口很容易膨胀。它要读取模板、校验参数、生成预览、创建项目、创建广告组、分配素材、分配标题、生成名称、调用媒体接口、保存本地关系、统计成功失败,还要支持失败重提。如果全部写在一个 Service 方法里,短期能跑,长期没人敢改。

我更倾向把这类流程拆成几层。Controller 只接收请求和做权限入口;Application Service 编排流程,比如 preview、commit、resubmitFailedItems;Domain Builder 负责把批创配置转换成项目、广告、创意等本地对象;Media Client 负责和头条、广点通、快手、B 站等外部 API 交互;Repository/Mapper 只负责持久化。这样每层失败时,问题位置比较清楚。

预览和提交也应该分开。预览阶段只生成本地可检查的结构,告诉运营将创建多少项目、多少广告、素材和标题如何分配、命名规则是否符合预期。提交阶段才真正调用媒体接口。这个边界能避免用户配置错误时直接污染媒体后台,也方便在前端展示“这一批会生成什么”。

异常处理上,批创类接口不能只返回一个失败。参数校验失败要告诉用户缺哪个字段;媒体接口失败要保留媒体返回码和可读原因;部分成功要告诉用户哪些成功、哪些失败;系统异常要记录 traceId,方便后端排查。异常结构清晰,前端才能做出可恢复的交互。

配置也要外置。媒体 API 地址、超时时间、限流间隔、批量大小、重试次数、任务超时、是否开启某个媒体能力,都不应该写死。业务变化最快的地方通常不是代码逻辑,而是外部媒体规则和运营策略;配置化可以减少发版频率,也更利于灰度。

Spring Boot 项目能启动只是第一步。真正的生产化,是让每个复杂需求都能找到清晰边界:请求入口在哪里,业务编排在哪里,外部依赖在哪里,失败如何表达,配置如何接管,日志如何追踪。边界清楚,后续加媒体、加字段、加补偿才不会变成灾难。

如果项目早期没有拆清楚,也不一定要一次性大重构。可以先从新增需求开始按边界写,把旧代码外面包一层更清晰的入口;再把最容易出问题的外部调用、状态流转、异常处理抽出来。后端工程化不是为了目录漂亮,而是为了下一次需求变化时,修改点更少,风险更可控。

参考资料