迈向持久化的Spring PetClinic:识别适合持久执行的工作流候选
概述
持久执行是一种运行代码的方式,使其进度能够承受故障。普通程序将状态保存在内存中:如果进程崩溃、机器重启,或多步骤操作中途网络调用超时,状态就会丢失,工作半途而废。
Temporal 作为持久执行平台,会将流程的每一步持久化到其事件历史(Event History)中——这是一个持久化、仅追加的事件日志——因此在任何故障后,工作都能从中断处精确恢复,仿佛崩溃从未发生。这使得编写长期运行的多步骤流程(例如等待数天的定时器、调用不可靠的外部服务,或绝不能重复执行的步骤)成为可能,代码保持普通线性结构,而平台则保证它们可靠且精确地执行一次。
什么是一个流程适合持久化的候选者?
但什么是一个流程适合持久化的候选者?最能从持久化中受益的流程具有以下特点:
- 长期运行(等待分钟、天或周)
- 多步骤(涉及多个可能独立故障的系统)
- 和/或必须精确执行一次(绝不能重复扣款或重复发送通知)
一个简单的单步骤数据库写入操作,无论提交与否,从持久执行中得不到任何好处——它已经是原子性的。
有趣的问题在于,实际应用程序将前三类流程隐藏在哪里。
Spring PetClinic 作为探索工具
为了具体回答这个问题,我们以经典的 Spring PetClinic 示例应用程序为例,并审查其源代码。
Spring PetClinic 是一个教科书式的 CRUD 应用,因此严格来说,其中没有任何内容需要今天的持久化——但其领域(预约就诊、注册宠物、处理付款)充满了那些长期运行、多步骤、易出错的流程,无论是潜藏在代码中,还是只需一个明显的扩展。目标是找到这些接缝,对它们进行排序,并选择一个起点。
后续章节按顺序进行:第1步下载源代码进行分析,第2步进行两次持久化分析(一次使用普通的标准 Claude Code,另一次基于 Temporal),第3步得出结论。
结果是在 Spring PetClinic 的上下文中得到了一个非常明确的建议:首先使就诊预约 + 提醒持久化。它获胜有三个原因。
- 代码中已经存在接缝——
VisitController今天就将未来的日期持久化,因此有一个自然的位置可以插入,几乎不需要搭建框架。 - 就诊本质上是长期运行的:提醒必须在预约前一天(可能几周后)触发,这正是内存代码无法存活、但持久执行可以轻松处理的持久等待。
- 它在单个易于理解的流程中实践了持久化的所有三个用途——持久定时器(休眠直到提醒到期)、可重试的副作用(发送确认、通知兽医)和精确一次语义(即使崩溃也不会重复发送提醒)。
其他候选者也是真实的,但它们的风险较低(所有者 onboarding),需要发明新功能(计费/支付),或者在核心价值尚未展示之前增加了分布式系统复杂性(外部注册)。就诊预约是看到持久化发挥价值的最短路径。
第1步——下载 Spring PetClinic 源代码
本项目的第一步是下载官方的 Spring PetClinic 示例应用程序。
第2步——分析应用程序中的持久化候选者
将 Claude Code(或类似的 AI)指向你已经拥有的应用程序源代码,并询问它你感兴趣的技术(采用、集成或仅仅学习)适合放在哪里,这是一个自然的起点。
模型读取的是你的真实代码,而不是白皮书,因此你不必猜测新技术如何映射到你已有的东西上。它提出的机会基于你的实际领域和控制流,而不是泛泛的建议。
这正是这一步所做的:它分析 Spring PetClinic 中值得持久化的流程,并且进行了两次分析。
- 2a 使用普通的标准 Claude Code 进行——没有插件或技能安装。它识别候选者,进行排序,并选择就诊预约 + 提醒作为理想的第一个流程。
- 2b 基于 Temporal(通过
temporal-developer技能)重新运行相同的分析,将每个候选者映射到具体的 Temporal 原语,并添加了temporal-spring-boot-starter的接线。
两者得出相同的结论;2b 添加了 Temporal 特定的术语、修正的行号和实现指导。参见 第3节 查看并排比较。
2a——分析:适合持久化的流程
Spring PetClinic 是一个教科书式的 CRUD 应用。每个“流程”都是一个同步 HTTP 请求,对单个数据源执行一次本地数据库事务。就目前而言,代码中没有外部服务调用、没有消息队列、没有多步骤编排、没有后台作业、也没有长期运行的操作。因此,开箱即用时没有任何东西严格需要持久执行——单个 JPA 事务已经提供了原子性。
因此,有用的问题不是“今天什么是持久化的”,而是**“领域在何处自然地需要一个多步骤、易出错或长期运行的流程,而持久化可以保护它?”** 其中一些接缝已经存在于代码中;其他则是领域暗示的明显扩展。
基于当前代码的候选者
-
预约就诊——
owner/VisitController.java:97(processNewVisitForm)。今天这只是owners.save(owner)。但“预约就诊”是该应用最明显的现实工作流:在生产中,它会扩展为发送确认、安排就诊前一天的提醒、并通知指定的兽医。这个提醒是一个持久定时器——流程休眠直到visit.getDate()减一天,然后执行操作。这是最强的候选者:它长期运行(数天到数周),跨越可能独立失败的副作用,并且需要通知的精确一次语义。 -
创建/更新宠物——
owner/PetController.java:107(processCreationForm)和owner/PetController.java:186(updatePetDetails)。目前只是一个saveAndFlush。在现实的诊所扩展中,会向外部系统(芯片注册、保险提供者、疫苗接种记录服务)注册宠物。一旦涉及第二个系统,这就变成了一个分布式多步骤流程,存在部分失败的风险——非常适合每步重试和 saga/补偿路径。 -
注册所有者——
owner/OwnerController.java:77(processCreationForm)。同样的模式,优先级较低:欢迎消息加上 CRM 同步将本地保存变成一个值得持久化的流程。
来自自然领域扩展(代码中尚未实现)的候选者
-
兽医参考数据同步/缓存预热——
system/CacheConfiguration.java与vet/VetController.java。vets 查询被显式缓存,这暗示了数据可能是昂贵或可刷新的。一个用于预热/刷新缓存,或从外部 HR 系统同步兽医和专科数据的定期作业,就是一个周期性的持久流程。 -
就诊计费/支付(新功能)——该领域最大的缺失持久化候选者。支付捕获 → 发票生成 → 收据递送是经典的 saga:一个外部支付网关调用(可重试,必须精确一次),随后是依赖步骤,如果后续步骤失败则需要补偿。
优先级排序
建议
鉴于我们的意图以及 VisitController 已经有一个干净的插入点,就诊预约 + 提醒 是第一个要持久化的理想流程——它以最小的搭建框架实践了持久化的三个用途(持久定时器、可重试的副作用和精确一次执行)。
2b——基于 Temporal 的分析
本节重新运行相同的分析,但基于 Temporal 作为持久执行平台,使用 temporal-developer 技能 获取术语和 Java/Spring Boot 模式。候选者和排序与 2a 相同;以下将每个候选者映射到具体的 Temporal 原语。
为什么今天不需要 Temporal
每个状态变化都是一个同步 HTTP 处理器,包裹着一个针对单个数据源的 JPA 事务:
没有外部调用、队列、定时器、后台作业或多步骤编排。每个提交已经是原子的,因此就 Temporal 而言,这里还没有任何东西需要持久执行。代码中有两个信号显示领域想要持久化:
- 已经存在未来日期的工作。
VisitController.minVisitDate()(VisitController.java:83)强制每次就诊至少提前 1 天,并且Visit携带date——这正是 Temporal 的定时器(Workflow.sleep(...))所服务的。 - 显式缓存暗示可刷新的数据。
CacheConfiguration.java:37创建了一个专用的vets缓存,由VetController.java:70提供服务——这暗示了数据值得通过 Temporal 计划定期同步。
候选者映射到 Temporal 原语
建议
从就诊预约 + 提醒开始,位于 VisitController.java:109(验证之后)。在 Temporal 术语中,它在一个工作流中实践了持久执行的三个用途:
- 定时器——
Workflow.sleep()直到预约前一天,即使 Worker 重启也能存活。 - 可重试的活动——确认邮件 + 兽医通知,每个都有重试策略。
- 精确一次语义——事件历史重放保证提醒不会在崩溃时重复发送。
接线使用 temporal-spring-boot-starter:将 @WorkflowInterface / @ActivityInterface 放在接口上,添加 @WorkflowImpl / @ActivityImpl 实现,并将自动配置的 WorkflowClient bean 注入 VisitController 以在保存后启动预约工作流。
第3步——结论
2a 和 2b 节得出了相同的结论——相同的候选者、相同的排序、相同的首选(就诊预约 + 提醒)。它们仅在框架和来源上有所不同:
两个要点:
- 2b 的行号是准确的。 2a 的行号早于当前检出,与磁盘上的源代码有偏差;2b 针对实际读取的代码进行了修正。
- 分析有意运行两次——2a 用普通 Claude Code 提出问题,2b 通过
temporal-developer技能用 Temporal 具体细节回答。建议在两者之间是稳定的:首先使就诊预约 + 提醒持久化。
原文最初发布于 foojay 上的 Toward a Durable Spring PetClinic。