1. 现实问题:功能清单不等于可交付系统
一个博客系统看起来只需要文章增删改查、标签、登录和评论,但真正落地还要处理草稿与公开状态、权限、唯一 slug、事务、缓存、上传、日志、测试、迁移和部署。如果从“先把所有页面写完”开始,架构问题会在最后一周一起爆发。
综合项目的目标不是堆功能,而是把一条最小可交付切片走通:管理员创建草稿、编辑、发布,访客读取公开文章;每一层有清楚的对象、SQL、错误、测试和运行证据。
2. 最小可运行示例:一条发布用例的项目骨架
请求 POST /api/drafts/{id}/publish
-> SecurityFilterChain:确认身份与角色
-> ArticleController:绑定 id,调用 Service
-> ArticleService:检查作者、状态、幂等条件
-> ArticleMapper:条件更新 article.status
-> AuditMapper:写发布审计(同一事务)
-> TransactionManager:commit
-> 事务提交事件:失效详情/列表缓存
-> Controller:返回 ArticleView 和 200
核心表至少包括 article、article_tag、tag 和 article_audit。article 用 status 与 deleted_at 形成公开边界,slug 有唯一索引,audit 记录 actor、action、created_at。缓存只存读取 DTO,不把数据库实体和权限状态混成一个可变对象。
3. 调用链与对象变化
HTTP 字节进入过滤器,Bearer token 变成 Authentication;路径 id 变成 long,Controller 不再接触原始 JSON。Service 从认证上下文和数据库读出 ArticleRecord,领域方法把 DRAFT 变成 PUBLISHED,Mapper 将状态和版本绑定到 SQL。受影响行数为 0 时表示状态竞争或资源不存在,而不是继续写 audit。
事务连接持有 article update 和 audit insert,commit 后数据库成为新事实;提交事件让 Redis 删除相关 key,访客下一次请求回源并得到新 ArticleView。日志贯穿 request id、article id、actor、数据库耗时、缓存结果和 commit 结果,但不记录 token、正文秘密或数据库密码。
4. 为什么这样设计
分层的理由是变化和失败边界:Controller 变化来自 HTTP,Service 变化来自业务规则,Mapper 变化来自 schema/SQL,Cache adapter 变化来自 Redis,Security 变化来自认证协议。领域模型不应该知道 JSON、Redis key 或 Nginx 头。每层都可以被替换或单独测试,但不追求为了分层而制造空文件。
先做模块化单体比过早拆微服务更适合学习项目:一个进程和一个数据库事务更容易证明一致性,模块边界仍可通过包和接口建立。读写模型、缓存和事件只在有真实压力或外部副作用时引入;没有指标支持的复杂度就是维护成本。
5. 项目落点:按迭代切片完成而不是一次写完
第一迭代完成文章表、草稿创建、发布和公开读取;第二迭代加入标签、分页和缓存;第三迭代加入 JWT、上传、审计和部署。每次迭代都带迁移脚本、单测、集成测、接口 smoke test、日志字段和回滚说明。Git commit 保持一个可解释变化,避免“功能、重构、格式化”混成不可回滚大提交。
验证清单包括:JDK 21 编译,Boot 4.1 上下文启动,创建—发布—读取真实数据库流程,重复发布得到稳定结果,Redis 不可用时降级,未授权请求得到 401/403,上传大小/路径校验生效,Docker 健康检查通过,Nginx 反代与回滚镜像可验证。
复盘要记录事实和推断:哪个请求在多少毫秒完成,哪条 SQL 扫了多少行,缓存命中率是否改善,失败时回滚到什么 SHA。不要用“感觉更快”“应该没问题”替代数字、日志和可重复命令。
6. 易错排查
- Controller 直接改 Entity:权限、状态和持久化边界混在一起,先拆 DTO/Service/Mapper。
- 先删缓存再写库:事务失败会产生不必要 miss;使用提交后失效或补偿策略。
- 只测 MockMvc:SQL、索引、事务和容器配置未被证明,补真实数据库集成流。
- 迁移不可回滚:先采用向后兼容字段和双写/回填,再删除旧字段;记录数据库版本。
- 线上无法定位:补 request/commit SHA/镜像 digest/traceId/数据库迁移版本和健康证据。
7. 一页复习
综合项目的主线是资源契约 → DTO → Service 规则 → SQL/事务 → 缓存失效 → HTTP 响应 → 测试和部署证据。架构取舍围绕真实变化和失败边界,工具不会替你完成一致性。能从一次请求追到一行 SQL、一个对象变化和一次提交,再用测试/日志复现,才算把路线学成工程能力。
综合项目应先写一张边界表:文章 API 接收 DTO,认证链提供 actor,Service 决定状态迁移,Mapper 读写表,事务决定事实何时提交,事件触发缓存失效,前端只接收稳定 View。标签、上传和评论属于可迭代模块,不应在第一条发布切片中把所有外部依赖塞进一个 Controller。
项目文件可以按模块化单体组织为 web、application、domain、persistence、infrastructure 和 config;接口放依赖方向的内侧,实现放外侧。数据库迁移、测试夹具、Dockerfile、Nginx 和 CI 同样是项目的一部分,不能只提交 Java 源码。每个模块写输入、输出、错误和拥有的状态,代码审查才有依据。
把真实流程作为一条验收记录:创建草稿得到 id,读取确认 DRAFT,发布返回成功并写 audit,公开读取得到 PUBLISHED,重复发布返回冲突,未授权草稿返回 401/403,Redis 故障时回源,容器 health 通过,Nginx 反代成功。每个节点保存 HTTP、数据库、日志或命令证据,不能只截最终页面。
排错按第一次偏离定位:状态错查领域迁移,数据错查 SQL/事务,响应错查 DTO/转换器,缓存旧查提交后失效,部署错查镜像/配置/端口,权限错查 Security 和 Service 归属。复盘还要标记哪些结论未验证、哪些指标不足以及下个迭代的最小补强,不把“以后优化”当成当前完成。
练习是删除缓存、断开数据库、重复提交、修改 schema 和回滚镜像各做一次故障演练,记录输入、预期、实际和恢复命令。最终用一页文档说明为什么选择模块化单体、事务数据库和旁路缓存,以及什么数据量/团队/故障证据出现后才考虑拆分服务。
综合项目的输入输出应形成一条验收表:创建草稿输入 DTO/actor,输出 id 和 DRAFT;编辑输入版本,输出新版本或冲突;发布输入 actor/status,输出 PUBLISHED、audit 和提交后的缓存失效;公开读取输出 View,草稿读取输出 403/404;部署输入 SHA/镜像/config,输出 health 和真实 HTTP。每个节点都有拥有状态的层。
架构文件可以保持模块化单体:web 处理协议,application 编排用例,domain 表达状态与值对象,persistence 负责 SQL/事务,infrastructure 负责 Redis/文件/邮件,config 负责 Boot 装配。接口朝内依赖,适配器朝外实现;如果一个包同时知道 JSON、SQL 和 Redis key,说明边界已经泄漏。模块化不等于拆成多个进程。
测试和部署要证明同一条流程:单测证明状态规则,Mapper 集成测证明 SQL,MockMvc 证明 HTTP,安全测试证明 401/403,缓存测试证明提交后失效,Docker/Nginx smoke 证明运行环境。日志至少能按 traceId 追到 actor、article、SQL 耗时、缓存结果和 commit;健康端点只作为运行探针,不能替代业务流程。
排错按第一次对象变化偏离:请求字段错查 DTO,状态迁移错查 domain,行数错查 SQL/约束,回滚错查代理/连接,旧值错查缓存版本,权限错查 Security+资源归属,部署错查镜像/配置/端口。每个修复都保留复现命令和回归测试。练习是轮流模拟重复发布、数据库断连、Redis 超时、上传失败和回滚,记录恢复后的最终状态。
进阶附录:下一轮演进的判断表
当文章量、读写比例、团队人数和故障模式发生变化,再评估读写分离、消息队列、搜索、对象存储和多模块拆分。每个演进都写收益、成本、失败补偿、观测指标和回滚方案。架构不是终点图,而是随着约束变化持续修订的决策记录。
本课按「Java 21 + Spring Boot 4.1 博客系统综合工程实践」的学习范围组织,正文与示例均为本站原创整理。