Ohhnews

分类导航

$ cd ..
foojay原文

迈向持久化的Spring PetClinic:识别适合持久执行的工作流候选

#spring petclinic#持久化执行#temporal#工作流分析#预约提醒

概述

持久执行是一种运行代码的方式,使其进度能够承受故障。普通程序将状态保存在内存中:如果进程崩溃、机器重启,或多步骤操作中途网络调用超时,状态就会丢失,工作半途而废。

Temporal 作为持久执行平台,会将流程的每一步持久化到其事件历史(Event History)中——这是一个持久化、仅追加的事件日志——因此在任何故障后,工作都能从中断处精确恢复,仿佛崩溃从未发生。这使得编写长期运行的多步骤流程(例如等待数天的定时器、调用不可靠的外部服务,或绝不能重复执行的步骤)成为可能,代码保持普通线性结构,而平台则保证它们可靠且精确地执行一次。

什么是一个流程适合持久化的候选者?

但什么是一个流程适合持久化的候选者?最能从持久化中受益的流程具有以下特点:

  • 长期运行(等待分钟、天或周)
  • 多步骤(涉及多个可能独立故障的系统)
  • 和/或必须精确执行一次(绝不能重复扣款或重复发送通知)

一个简单的单步骤数据库写入操作,无论提交与否,从持久执行中得不到任何好处——它已经是原子性的。

有趣的问题在于,实际应用程序将前三类流程隐藏在哪里。

Spring PetClinic 作为探索工具

为了具体回答这个问题,我们以经典的 Spring PetClinic 示例应用程序为例,并审查其源代码。

Spring PetClinic 是一个教科书式的 CRUD 应用,因此严格来说,其中没有任何内容需要今天的持久化——但其领域(预约就诊、注册宠物、处理付款)充满了那些长期运行、多步骤、易出错的流程,无论是潜藏在代码中,还是只需一个明显的扩展。目标是找到这些接缝,对它们进行排序,并选择一个起点。

后续章节按顺序进行:第1步下载源代码进行分析,第2步进行两次持久化分析(一次使用普通的标准 Claude Code,另一次基于 Temporal),第3步得出结论。

结果是在 Spring PetClinic 的上下文中得到了一个非常明确的建议:首先使就诊预约 + 提醒持久化。它获胜有三个原因。

  1. 代码中已经存在接缝——VisitController 今天就将未来的日期持久化,因此有一个自然的位置可以插入,几乎不需要搭建框架。
  2. 就诊本质上是长期运行的:提醒必须在预约前一天(可能几周后)触发,这正是内存代码无法存活、但持久执行可以轻松处理的持久等待。
  3. 它在单个易于理解的流程中实践了持久化的所有三个用途——持久定时器(休眠直到提醒到期)、可重试的副作用(发送确认、通知兽医)和精确一次语义(即使崩溃也不会重复发送提醒)。

其他候选者也是真实的,但它们的风险较低(所有者 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 事务已经提供了原子性。

因此,有用的问题不是“今天什么是持久化的”,而是**“领域在何处自然地需要一个多步骤、易出错或长期运行的流程,而持久化可以保护它?”** 其中一些接缝已经存在于代码中;其他则是领域暗示的明显扩展。

基于当前代码的候选者

  1. 预约就诊——owner/VisitController.java:97processNewVisitForm)。今天这只是 owners.save(owner)。但“预约就诊”是该应用最明显的现实工作流:在生产中,它会扩展为发送确认、安排就诊前一天的提醒、并通知指定的兽医。这个提醒是一个持久定时器——流程休眠直到 visit.getDate() 减一天,然后执行操作。这是最强的候选者:它长期运行(数天到数周),跨越可能独立失败的副作用,并且需要通知的精确一次语义。

  2. 创建/更新宠物——owner/PetController.java:107processCreationForm)和 owner/PetController.java:186updatePetDetails)。目前只是一个 saveAndFlush。在现实的诊所扩展中,会向外部系统(芯片注册、保险提供者、疫苗接种记录服务)注册宠物。一旦涉及第二个系统,这就变成了一个分布式多步骤流程,存在部分失败的风险——非常适合每步重试和 saga/补偿路径。

  3. 注册所有者——owner/OwnerController.java:77processCreationForm)。同样的模式,优先级较低:欢迎消息加上 CRM 同步将本地保存变成一个值得持久化的流程。

来自自然领域扩展(代码中尚未实现)的候选者

  1. 兽医参考数据同步/缓存预热——system/CacheConfiguration.javavet/VetController.java。vets 查询被显式缓存,这暗示了数据可能是昂贵或可刷新的。一个用于预热/刷新缓存,或从外部 HR 系统同步兽医和专科数据的定期作业,就是一个周期性的持久流程。

  2. 就诊计费/支付(新功能)——该领域最大的缺失持久化候选者。支付捕获 → 发票生成 → 收据递送是经典的 saga:一个外部支付网关调用(可重试,必须精确一次),随后是依赖步骤,如果后续步骤失败则需要补偿。

优先级排序

排名流程为何持久化演示工作量
1就诊预约 + 提醒长期运行定时器,多个副作用,精确一次通知低——钩子存在于 VisitController.java:97
2宠物注册 → 外部注册分布式多步骤,部分失败,saga中等
3就诊计费/支付(新)支付 saga,补偿中等
4所有者 onboarding欢迎消息 + CRM 同步
5兽医/参考数据同步定期作业

建议

鉴于我们的意图以及 VisitController 已经有一个干净的插入点,就诊预约 + 提醒 是第一个要持久化的理想流程——它以最小的搭建框架实践了持久化的三个用途(持久定时器、可重试的副作用和精确一次执行)。

2b——基于 Temporal 的分析

本节重新运行相同的分析,但基于 Temporal 作为持久执行平台,使用 temporal-developer 技能 获取术语和 Java/Spring Boot 模式。候选者和排序与 2a 相同;以下将每个候选者映射到具体的 Temporal 原语。

为什么今天不需要 Temporal

每个状态变化都是一个同步 HTTP 处理器,包裹着一个针对单个数据源的 JPA 事务

处理器持久化调用位置
OwnerController.processCreationFormowners.save(owner)OwnerController.java:84
OwnerController.processUpdateOwnerFormowners.save(owner)OwnerController.java:156
PetController.processCreationFormowners.saveAndFlush(owner)PetController.java:126
PetController.updatePetDetailsowners.saveAndFlush(owner)PetController.java:199
VisitController.processNewVisitFormowners.save(owner)VisitController.java:109

没有外部调用、队列、定时器、后台作业或多步骤编排。每个提交已经是原子的,因此就 Temporal 而言,这里还没有任何东西需要持久执行。代码中有两个信号显示领域想要持久化:

  • 已经存在未来日期的工作。 VisitController.minVisitDate()VisitController.java:83)强制每次就诊至少提前 1 天,并且 Visit 携带 date——这正是 Temporal 的定时器Workflow.sleep(...))所服务的。
  • 显式缓存暗示可刷新的数据。 CacheConfiguration.java:37 创建了一个专用的 vets 缓存,由 VetController.java:70 提供服务——这暗示了数据值得通过 Temporal 计划定期同步。

候选者映射到 Temporal 原语

排名流程代码接缝Temporal 适配
1就诊预约 + 提醒VisitController.java:109工作流,使用 Workflow.sleep() 直到 visit.getDate() - 1 天,然后活动用于确认 + 兽医通知。通过事件历史重放实现精确一次。
2宠物注册 → 外部注册PetController.java:126, :199Saga——微芯片/保险/疫苗接种各为一个活动,具有每步重试和幂等补偿。
3就诊计费/支付(新)---经典 Saga:扣款(可重试,精确一次)→ 发票 → 收据,在后续失败时补偿。
4所有者 onboardingOwnerController.java:84工作流:欢迎消息 + CRM 同步作为可重试的活动。
5兽医/参考数据同步CacheConfiguration.java:37 + VetController.java:70周期性 Temporal 计划驱动一个刷新 vets 缓存的活动。

建议

从就诊预约 + 提醒开始,位于 VisitController.java:109(验证之后)。在 Temporal 术语中,它在一个工作流中实践了持久执行的三个用途:

  • 定时器——Workflow.sleep() 直到预约前一天,即使 Worker 重启也能存活。
  • 可重试的活动——确认邮件 + 兽医通知,每个都有重试策略。
  • 精确一次语义——事件历史重放保证提醒不会在崩溃时重复发送。

接线使用 temporal-spring-boot-starter:将 @WorkflowInterface / @ActivityInterface 放在接口上,添加 @WorkflowImpl / @ActivityImpl 实现,并将自动配置的 WorkflowClient bean 注入 VisitController 以在保存后启动预约工作流。

第3步——结论

2a 和 2b 节得出了相同的结论——相同的候选者、相同的排序、相同的首选(就诊预约 + 提醒)。它们仅在框架和来源上有所不同:

方面2a2b
工具普通的 Claude Code——没有插件或技能;将“持久化”视为抽象概念基于 Temporal,使用已安装的 temporal-developer 技能
术语通用:“持久定时器”、“saga/补偿路径”、“定期作业”、“精确一次”具体的 Temporal 原语:工作流、具有重试策略的活动SagaTemporal 计划定时器、事件历史重放
行号引用上游 PetClinic 行(例如 VisitController.java:97PetController.java:107OwnerController.java:77引用实际签出的源代码行(例如 VisitController.java:109PetController.java:126OwnerController.java:84
“为什么今天不需要”散文段落相同观点,但以表格形式列出每个处理器及其精确的持久化调用
实现指导无——停留在分析层面添加具体的 temporal-spring-boot-starter 接线

两个要点:

  1. 2b 的行号是准确的。 2a 的行号早于当前检出,与磁盘上的源代码有偏差;2b 针对实际读取的代码进行了修正。
  2. 分析有意运行两次——2a 用普通 Claude Code 提出问题,2b 通过 temporal-developer 技能用 Temporal 具体细节回答。建议在两者之间是稳定的:首先使就诊预约 + 提醒持久化。

原文最初发布于 foojay 上的 Toward a Durable Spring PetClinic