我用 Spring Boot 做了一个“单体应用”,它救了我半年

十月 17, 2025 / Starry / 16阅读 / 0评论/ 分类: 默认分类

去年 Q2,我接了一个内部“物料小仓库”系统:入库、出库、盘点、报表,还要给前端和钉钉机器人用。三周上线,一个人维护。复杂度不小,但我第一时间选择了单体应用。理由很简单:

  • 一个进程、一份代码、一套部署,迭代更快。

  • 团队就我一个人,微服务的治理成本不值当。

  • 边界划清楚,未来真要拆也不难。

第一周:先跑起来

我用 Spring Boot 3 + JDK 17,从 start.spring.io 生一个骨架。依赖选了 Spring Web、Validation、Spring Data JPA、H2(开发)和 MySQL(生产),再加 Lombok、DevTools、Actuator。

我定了目录和分层约定:

  • controller:对外 API

  • service:业务逻辑

  • repository:数据访问

  • domain:实体与枚举

  • dto:接口收发的数据结构

  • config:配置、拦截器、全局异常

  • support:通用工具、统一返回等

两条铁律贯穿始终:

  • DTO 和 Entity 不混用,接口形状与数据库形状分离。

  • 改数据的逻辑都走 service,controller 只负责入参校验和调度。

当晚我用 H2 跑通了最小的 CRUD(以“物料”为例),这条主链路很关键:请求进来 → 校验 → 服务 → 持久化 → 返回。后面的复杂功能,都是在这条路径上加分岔。

第二周:让它像个产品

  • 统一异常与返回:加 RestControllerAdvice,把校验错误、业务错误、系统错误统一成可读的错误体,前端同事不再抱怨提示乱。

  • 分页与查询:约定 page/size/sort 参数,统一分页返回模型,前后端不再“各造一套”。

  • 登录与权限:接公司 OAuth,后端做简单的基于角色鉴权。敏感接口用注解标识,拦截器做拦截。

  • 多环境配置:开发用 H2,测试/生产用 MySQL。用 profiles 管理,敏感信息走环境变量。生产把 jpa.hibernate.ddl-auto 设为 validate,避免自动改表。

一个夜里的一行日志救了我
上线后用户反馈“偶发超时”。我在拦截器里加了 requestId 和接口耗时日志,很快定位到导出报表扫全表、内存拼 Excel。临时方案:分页流式写入、限制时间范围、改异步生成并通知。教训是:观测性别等上线后再补,越早越好。

第三周:部署、监控与热路径优化

  • 打包与容器:Maven 打包,最简 Docker 镜像,统一时区与编码,用环境变量传 profile 和数据库连接。

  • Actuator 指标:打开 health、info、metrics、http.server.requests,观察 p95/p99,结合线程池与 GC 指标区分“资源瓶颈”还是“代码问题”。

  • 热路径优化:

    • 热门详情做短缓存,防穿透。

    • 大分页改游标分页(where id > ?),避免 offset 大导致慢查询。

    • 用实体图或 join fetch 干掉 N+1。

我踩过的坑与解法

  • 事务失效:类内互调不走代理,@Transactional 不生效。把事务边界提到外层服务,或用编程式事务。

  • H2 与 MySQL 差异:大小写、索引命名、时间函数不同。开发用 H2 的 MySQL 模式仍不保险,最后用 Testcontainers 在 CI 跑真 MySQL。

  • 时间序列化错乱:统一 LocalDate/LocalDateTime,Jackson 配时区,数据库保存 UTC。

  • CORS 不生效:拦截器拦了 OPTIONS 预检。放行预检或用 CorsRegistry 全局配置。

  • 批处理回滚混乱:导入分批次、用业务幂等键(比如 sku),加唯一索引兜底,失败记录单独存证,避免“一错全滚”的业务灾难。

半年后:单体也能“模块化”
虽然没拆微服务,但我刻意做了模块边界:

  • 物料、出入库、报表、通知分别成包,模块间只通过 service 接口交互。

  • 公共模型尽量少,DTO 清晰定义,避免“隐性耦合”导致未来难拆。

  • 事件驱动小步走:如“出库成功”写一条 outbox 事件,定时器投递到消息通道(最初甚至只是表轮询),通知模块订阅处理,降低耦合。

如果重来,我会更早做的三件事

  • 更早写集成测试:至少用 @SpringBootTest 跑通“入库-出库-库存校验”主链路,配合 Testcontainers。

  • 更早抽统一返回与错误码:别等前端反馈不统一再补。

  • 更早做只读缓存与报表离线化:读多写少的接口天然适合读扩展。

我认同的小原则

  • 能不用微服务就不用,先把单体做到清晰、可测试、可观测。

  • 错误信息要对人友好,包含关键上下文(如“SKU 已存在:ABC-001”)。

  • 幂等要在服务层兜住,数据库用唯一索引背书。

  • 日志可检索、带 requestId、别泄露敏感信息。

  • 没有银弹,复杂度只是转移;把它放到你能管理的地方。

最小可用清单(可以直接套)

  • 技术栈:Spring Boot 3 + JDK 17 + Spring Web + Validation + Spring Data JPA + MySQL + Lombok + Actuator

  • 基础设施:全局异常、统一返回体、拦截器计时与 requestId、CORS、统一分页模型

  • 开发与测试:DevTools(本地热重启)、Testcontainers(集成测试)

  • 部署与配置:Docker 镜像、环境变量外部化、profiles 管理

  • 可观测:Actuator 指标、接口耗时日志、错误告警(钉钉/Slack)

结尾:
这套单体应用,我一个人维护了半年。它没有“高大上”的名字,但它稳定、可读、能迭代。后来团队变大,一些模块真拆成了独立服务,边界与契约早已清晰,拆解几乎没有痛感。对我来说,单体不是与微服务对立,而是在成本可控的前提下,把业务做对、做稳、做久。希望这段经历,能帮你少踩坑,尽快把产品交到用户手里。

#文章(8)

文章作者:Starry

文章链接:https://iseeyou1.icu/archives/wo-yong-spring-boot-zuo-liao-yi-ge-dan-ti-ying-yong-ta-jiu-liao-wo-ban-nian

版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0 许可协议,转载请注明出处!


评论