Skip to main content

BLOGE 0.9.8-RC1 在线版 · 事实校验 2026-09-15 · English

第 33 章 —— 迁移与对比

承诺: 读完本章后,你将能先写出约束,再决定是否选择 BLOGE;你会明确至少三种“不应选择 BLOGE”的情况,完成一份包含依据、反事实和第一步迁移实验的短 ADR。


学习目标

  1. 把选型问题改写成部署、所有权、恢复语义、验证治理和生态五类约束。
  2. 用硬停止条件淘汰不合适的候选,而不是给功能勾选表加总分。
  3. 区分“功能存在”“团队能运营”“证据足以发布”三种完全不同的结论。
  4. 为合适的候选设计可撤销的迁移第一步,不以全量改写验证选型。
  5. 写出一份短 ADR:选择、依据、反事实条件、迁移探针和退出标准。

前置条件

源示例

文件展示内容
BpmnToDslExample.java端到端:翻译 BPMN XML → .bloge DSL → 编译 → 执行
bloge-bpmn-transformer/README.md翻译管道、支持范围、诊断信息和已知限制
order-process.bloge标准订单处理 graph —— 并行拉取、分支、韧性
OrderProcessingDslExample.javaDSL 编译、注册表接线和从 Java 执行
order-saga.blogeSaga 风格的补偿:每个节点上的 compensate
payment-wait.bloge长时间运行的 await,带事件关联和超时
batch-order-processing.blogeforeach 并行扇出处理集合
SpringTicketTriageService.java在 Spring 服务中注入 GraphEngineList<Graph>
ADR-001核心零依赖决策
ADR-002虚拟线程优于 CompletableFuture
ADR-004受 HCL 启发的 DSL 优于 YAML/JSON

为什么这很重要

你已经花了十六章学习如何构建、测试和运行 BLOGE 工作流。现在一个更难的问题来了:这个具体项目应该用 BLOGE 吗?

团队很少从零开始。他们已经有:

  • 业务方法中嵌入了编排逻辑的现有服务。
  • 使用 Spring Batch 处理步骤导向批处理的任务。
  • 在可视化建模器中编写的 BPMN 流程图。
  • 或者正在评估需要独立服务器集群的 workflow-as-code 平台。

本章给你一个结构化的方式,将 BLOGE 实际做的事与那些替代方案实际做的事进行对比——基于你可以在仓库中指出的架构属性,而不是营销宣传。


心智模型

把编排工具想象为坐落在两个轴上:

Diagram: 33-migration-and-comparison figure 1

纵轴——工作流形状可见性。 上半部分在模型(XML、DSL 或可视化图)中捕获工作流结构。下半部分将结构完全嵌入代码中。

横轴——部署拓扑。 左侧需要独立的服务器集群。右侧嵌入到你现有的应用进程中。

BLOGE 位于右上象限:一个声明式的 DAG 模型,作为库运行在你的 JVM 内部,默认不需要外部基础设施。

这并非普遍最优——而是一组特定的取舍:

属性BLOGE 的选择文档位置
核心零外部依赖bloge-core 仅依赖 java.baseADR-001
虚拟线程执行每个节点运行在自己的虚拟线程中ADR-002
受 HCL 启发的 DSL块导向、支持表达式、方便注释ADR-004
基于检查点的持久化通过 bloge-durable 可选启用;存储可插拔第 13 章README
可嵌入引擎GraphEngine 是一个无状态的库对象第 21 章
节点级韧性Retry → Timeout → Fallback 链,按节点配置ADR-007

三个编排模块将平台从纯图编排扩展到更多模式:

模块新增能力
bloge-agent-extLLM 驱动的 agent 循环:agent DSL、工具调度、记忆策略和流式输出
bloge-session-durableDurableSessionManager——可崩溃恢复的多轮 session,带 checkpoint 持久化
bloge-state-durableDurableStateMachineManager——可崩溃恢复的状态机,带 checkpoint 持久化

先约束,再比较

贷款平台刚完成业务正确性验证。架构委员会问的不是“BLOGE 有多少功能”,而是: 在项目的非协商约束下,BLOGE 能否成为一个可运营、可验证、可退出的选择?

图:约束先行的选型漏斗

先写硬约束,再评估偏好,最后才设计迁移实验。任何候选违反硬约束,就停止; 不要用其它功能分数把它“平均回来”。

约束贷款平台的问题BLOGE 事实决策含义
部署拓扑能否新增独立 workflow 集群?引擎嵌入 JVM;durable store 可选禁止新集群时仍可候选
模型所有权谁审阅和修改流程?.bloge 是代码友好的显式 DAG业务只接受 BPMN 时是冲突
恢复语义需要 checkpoint 还是完整历史重放?BLOGE 保存 checkpoint,不提供通用历史重放平台强依赖 replay 时停止
验证治理发布是否必须绑定 Scenario、证据与源码身份?RC1 提供 Scenario/Policy/Fixture、source-bound evidence 与 claims能支持门禁,但仍需组织 Owner
生态边界是否必须原生运行多语言 worker?当前主运行时是 JVM硬性多语言要求时停止

图:选择 BLOGE 与停止选择的边界

这张表不产生“BLOGE 得 82 分”的伪精度。它产生三个结果:STOPPROBEADOPTPROBE 表示关键事实仍未知,必须用小实验取得证据;它不是委婉的通过。

三种明确不应选择 BLOGE 的情况

  1. 流程模型由非研发团队在 BPMN 平台中直接拥有。 如果可视化协作、人工任务 收件箱和既有流程服务器是组织合同,迁移会破坏所有权,而不是只替换语法。
  2. 恢复合同要求完整历史重放。 如果审计或灾备必须从事件历史重建每一步, checkpoint 模型不能凭“也能恢复”替代 replay 语义。
  3. 多语言 worker 是硬约束。 如果同一编排必须原生调度 Go、Python 与 TypeScript worker,JVM 内嵌模型会把适配成本转嫁给团队。

还有一个更朴素的停止条件:只有三个顺序调用、没有等待、分支、独立恢复或共享 治理需求时,普通方法更便宜。架构能力不是免费的;额外模型必须换来真实复杂度下降。

一个完成的贷款选型 ADR

贷款平台的硬约束是“不新增独立集群”“JVM 内运行”“每次发布绑定批准的 Scenario 与源码证据”;完整历史重放和多语言 worker 不是当前要求。因此结论是 PROBE BLOGE,而不是直接全量采用。

ADR 字段本例内容
选择auto-approve 单路径做两周可撤销探针
依据JVM 内嵌、显式 DAG、RC1 验证与 source-bound evidence 符合硬约束
反事实若必须引入历史重放或非 JVM worker,转向 server-based workflow 平台
第一迁移步只把读取型风险检查迁成 operator;放款 effect 留在旧路径
退出标准graph contract、业务 Scenario、恢复演练任一失败,停止扩面

这个 ADR 的价值不是“批准 BLOGE”,而是把下一步变成可证伪的实验。一次绿色 graph test 不能替代业务 Scenario;一次 PASS 也不能替代 source binding 和发布治理。


第一个可运行示例

最常见的迁移场景是将手写的 Java 编排方法替换为 BLOGE graph。下面按步骤完成这次迁移。

迁移前:手写胶水代码

// 典型的过程式编排,埋在一个服务方法里
public OrderResult processOrder(String userId, List<String> productIds) {
// 1. 顺序执行——但这两个调用实际上互不依赖
User user = userService.fetchUser(userId);
List<Product> products = productService.fetchProducts(productIds);

// 2. 重试逻辑和业务代码纠缠在一起
BigDecimal total = null;
for (int attempt = 0; attempt < 3; attempt++) {
try {
total = pricingService.calculate(user, products);
break;
} catch (Exception e) {
if (attempt == 2) throw e;
Thread.sleep(100 * (attempt + 1));
}
}

// 3. 分支逻辑藏在一个 if 语句里
CreditResult credit = creditService.check(user.id(), total);
if (credit.approved()) {
return orderService.create(user, total);
} else {
return orderService.reject(user.id(), credit.reason());
}
}

问题:

  • 步骤 1 和步骤 2 互不依赖,却被顺序执行。
  • 重试逻辑是手写的,混在业务方法里。
  • 没人能看到工作流的形状,除非逐行读代码。

迁移后:BLOGE graph

同样的工作流,表达为 order-process.bloge

这 46 行 graph 保留完整,因为迁移对比需要一份可复制单元,同时展示并行读取、 fan-in、韧性和两条分支目标。

graph orderProcess {

node fetchUser : FetchUserOperator {
input { userId = ctx.userId }
timeout = 3s
retry = { attempts: 2, backoff: 200ms, strategy: exponential }
}

node fetchProducts : FetchProductsOperator {
input { productIds = ctx.productIds }
timeout = 5s
}

node calcPrice : CalcPriceOperator {
depends_on = [fetchUser, fetchProducts]
input {
user = fetchUser.output
products = fetchProducts.output
}
}

node checkCredit : CreditCheckOperator {
depends_on = [fetchUser, calcPrice]
input {
userId = fetchUser.output.id
amount = calcPrice.output.total
}
retry = { attempts: 3, backoff: 100ms, strategy: jitter }
fallback = { approved: false, reason: "credit service unavailable" }
}

branch on checkCredit.output.approved {
true -> createOrder
false -> rejectOrder
}

node createOrder : CreateOrderOperator {
depends_on = [calcPrice]
input { user = fetchUser.output, price = calcPrice.output }
}

node rejectOrder : RejectOrderOperator {
depends_on = [checkCredit]
input { userId = fetchUser.output.id, reason = checkCredit.output.reason }
}
}

变化之处:

  • fetchUserfetchProducts 并行运行——引擎看到它们之间没有依赖。
  • 重试和超时声明在节点上,而不是写在循环里。
  • 分支是一个一等路由决策,而不是隐藏的 if
  • 工作流的形状可见、可审查、可工具化

当 DSL 保存在文件里时,同样的 registry / engine 接线可以写成这样:

var registry = new DefaultOperatorRegistry();
registry.register("FetchUserOperator", fetchUserOp);
registry.register("FetchProductsOperator", fetchProductsOp);
// … 注册其余 operator …

Graph graph = new GraphLoader(registry).load(
Path.of("bloge-examples/src/main/resources/bloge/order-process.bloge"));
GraphEngine engine = GraphEngine.builder().registry(registry).build();
GraphResult result = engine.execute(graph, new GraphContext(Map.of(
"userId", "user-42",
"productIds", List.of("prod-1", "prod-2", "prod-3")
)));

拆开来看

BLOGE vs 手写编排

维度手写胶水代码BLOGE graph
并发你自己管理线程、线程池、Future引擎根据 DAG 形状调度虚拟线程(ADR-002
韧性手写重试循环、try/catch 降级按节点声明:retrytimeoutfallbackADR-007
可见性读代码;寄希望于注释是准确的.bloge 文件 本身就是 文档;Studio 可以可视化渲染
可测试性在每个测试中 mock 每个服务调用bloge-test 提供 MockOperatorGraphTestRunnerDslTestHelper
持久化自己构建检查点/恢复机制通过 bloge-durable 存储可选启用(第 13 章

手写代码更合适的场景: 如果工作流只是三个顺序调用,没有重试、没有分支、也不需要持久化,那么一个普通方法更简单。当编排的形状足够复杂,需要并发、韧性或可见性时,BLOGE 才能体现出价值。

BLOGE vs 步骤导向的批处理框架

步骤导向的批处理框架(如 Spring Batch)将工作建模为线性的步骤管道,采用块导向的读取、处理和写入模式。

维度步骤导向的批处理BLOGE graph
执行模型Step 1 → Step 2 → Step 3,线性执行DAG:独立节点自动并行运行
迭代块导向的 reader/processor/writerforeach,支持并行或顺序模式(batch-order-processing.bloge
韧性步骤级或块级的跳过/重试策略节点级的 retry → timeout → fallback 链
长时间等待不是主要关注点await,带事件关联和超时(payment-wait.bloge
依赖通常需要框架运行时bloge-core 零外部依赖(ADR-001

步骤导向批处理更适合的场景: 如果你的工作负载是纯 ETL——读取块、转换、写入——没有扇出、没有分支、也不需要等待外部事件,那么批处理框架是为这种模式量身定制的。当批处理步骤需要并行扇出、等待外部信号,或者与非批处理工作流共享编排模式时,BLOGE 才能体现出价值。

BLOGE vs BPMN 导向的工具

BPMN 导向的工具使用基于 XML 的可视化标记来建模工作流,通常由一个独立的执行服务器支撑。

维度BPMN 导向的工具BLOGE graph
工作流标记BPMN 2.0 XML(冗长、面向机器).bloge DSL(支持表达式、方便注释;ADR-004
网关显式的 fork/join 网关元素从 DAG 形状隐式推断:不需要网关节点
部署独立的流程引擎服务器嵌入式库——GraphEngine 运行在你的 JVM 中
迁移路径bloge-bpmn-transformer 将 BPMN XML 翻译为 .bloge DSL(README

bloge-bpmn-transformer 模块提供了一条具体的迁移路径。它读取 BPMN 2.0 XML,通过中间表示进行降级转换,然后输出 .bloge DSL 文本或可执行的 Graph 对象。

支持的 BPMN 元素包括:开始/结束事件、服务/脚本/用户任务、排他网关、并行网关和包容网关、定时器/消息/信号事件、调用活动和子流程结构。不支持的元素会生成诊断信息,而不是默默发明语义。

BPMN 包容网关支持。 BPMN 翻译器支持包容网关(翻译为 branch mode=inclusive)、用户任务元数据保留(assigneecandidateGroupsformKey)、扩展元素解析(<inputOutput> 参数和 <properties>),以及改进的 UEL 到 BLOGE 表达式翻译toString()isEmpty() 等安全方法不再触发 COMPLEX_EXPRESSION 诊断)。

翻译示例来自 BpmnToDslExample.java

BpmnTranslator translator = new BpmnTranslator(OperatorMappingConfig.EMPTY);
try (InputStream bpmn = getClass().getResourceAsStream("/bpmn/order-process.bpmn")) {
TranslationResult<String> dsl = translator.translateToDsl(bpmn);
if (dsl.hasErrors()) {
throw new IllegalStateException(dsl.diagnostics().toString());
}
// dsl.result() 是合法的 .bloge DSL 文本
Graph graph = new GraphLoader(registry).load(dsl.result());
}
诊断代码含义
UNSUPPORTED_ELEMENT翻译器尚不支持的 BPMN 构造
UNMAPPED_OPERATOR没有 operator 映射匹配到该任务
COMPLEX_EXPRESSIONUEL 表达式无法一对一翻译
POTENTIAL_DATA_LOSS翻译可行,但某些 BPMN 意图可能无法完全保留
AMBIGUOUS_TIMER定时器定义使用了无法直接映射到 BLOGE wait 语法的形式

BPMN 工具更适合的场景: 如果你所在组织的业务分析师使用 BPMN 编写并拥有流程模型,且执行服务器已经部署和运维,那么迁移到 BLOGE 可能不值得带来的中断。当开发团队拥有工作流并且需要代码友好的编写体验和可嵌入的运行时时,BLOGE 才能体现出价值。

BLOGE vs workflow-as-code 平台

Workflow-as-code 平台允许你使用通用编程语言(Go、Java、TypeScript)编写工作流,持久化由一个独立的服务器集群通过重放函数历史来处理。

维度Workflow-as-code 平台BLOGE
部署需要专用服务器集群嵌入式库;无需外部服务器
持久化模型历史重放 / 事件溯源基于检查点:包括节点输出、loop / foreach 进度、挂起上下文、部分结果、事件关联和 session 快照(第 14 章,底层机制见第 13 章
工作流形状隐含在代码的控制流中.bloge 文件和 Studio 中可见的显式 DAG 模型
依赖客户端 SDK + 服务器基础设施bloge-core 仅依赖 java.baseADR-001
并发服务器管理 worker 任务分发引擎在本地使用虚拟线程(ADR-002
补偿框架特定的 saga 模式声明式的节点级 compensateorder-saga.bloge

Workflow-as-code 平台更适合的场景: 如果你需要跨 Go、TypeScript 和 Python 的内建多语言支持,或者你已经运维着服务器集群并依赖其历史重放语义,那么该平台的生态系统优势是实实在在的。当你希望避免运维独立集群、团队是 JVM 原生的、或者显式 DAG 模型和可嵌入引擎比跨语言 SDK 支持更重要时,BLOGE 才能体现出价值。


常见陷阱

❌ 用功能清单对比替代架构适配对比

一个常见错误是构建一张功能对照表——"它有重试吗?有分支吗?"——然后选打勾最多的工具。

本章提到的每个工具都支持某种形式的重试、分支和持久化。真正重要的差异在于架构层面

  • 引擎在哪里运行?(嵌入式库 vs. 独立集群)
  • 工作流形状如何表达?(显式模型 vs. 隐含在代码中)
  • 持久化如何工作?(检查点 vs. 重放)
  • 依赖和 JDK 要求是什么?(零依赖核心 vs. SDK + 服务器)

如果你在错误的层面上对比,你会选到一个功能齐全但部署模型完全不适合你团队的工具。


引导式重写

从手写代码迁移到 BLOGE 的四个步骤

拿你代码库中任意一个编排方法,按以下步骤操作:

步骤 1 —— 识别独立的调用。

标记每一对没有数据依赖的调用。在上面的手写示例中,fetchUserfetchProducts 是独立的。

步骤 2 —— 提取 operator。

每个调用变成一个 Operator<I, O> 实现。将它们注册到 DefaultOperatorRegistry

registry.register("FetchUserOperator", (input, ctx) -> {
String userId = ((Map<String, Object>) input).get("userId").toString();
return Map.of("id", userId, "name", userService.fetchUser(userId).name());
});

步骤 3 —— 声明 graph。

编写 .bloge 文件。使用 depends_on 实现显式汇合。使用 branch on 实现条件路由。在调用外部服务的节点上添加 retrytimeoutfallback

步骤 4 —— 验证行为。

使用 bloge-test 中的 GraphTestRunner 来断言节点执行顺序、状态和输出:

var runner = new GraphTestRunner(
registry,
Map.of(
"FetchUserOperator", fetchUserOp,
"FetchProductsOperator", fetchProductsOp
// … 注册其余 operator …
)
);

var result = runner.execute(graph, new GraphContext(Map.of("userId", "u1")));
runner.assertNodeExecuted("fetchUser");
runner.assertNodeExecuted("fetchProducts");
runner.assertNodeSkipped("rejectOrder"); // 授信已通过

从 BPMN 迁移到 BLOGE

步骤 1 —— 翻译。 使用 BpmnTranslator.translateToDsl() 并传入 operator 映射配置。

步骤 2 —— 阅读诊断信息。 修复所有 UNMAPPED_OPERATORCOMPLEX_EXPRESSION 诊断。在 OperatorMappingConfig 中将 BPMN 服务任务 ID 映射到 BLOGE 的 operator 名称。

步骤 3 —— 审查生成的 DSL。 翻译器会生成 // Source: bpmn:serviceTask id="xxx" 注释以便追溯(需启用 generateSourceComments)。

步骤 4 —— 编译并运行。

Graph graph = new GraphLoader(registry).load(dsl.result());
GraphEngine engine = GraphEngine.builder().registry(registry).build();
engine.execute(graph, context);

脑力检查

  1. 心智模型图中的两个轴是什么?BLOGE 位于哪里?(工作流形状可见性 vs. 部署拓扑;BLOGE 位于右上象限:声明式模型 + 可嵌入的库。)

  2. 说出 BLOGE 的三个在 ADR 中记录的架构属性,并解释每个属性的取舍。(零依赖核心 → 一些便利功能需要在适配器模块中重新实现。虚拟线程 → 需要现代 JDK。受 HCL 启发的 DSL → 需要专用解析器,不兼容通用 YAML/JSON 工具。)

  3. 如果翻译 BPMN 文件时收到 COMPLEX_EXPRESSION 诊断,应该怎么做?(手动将 UEL 表达式改写为 BLOGE 路径表达式,或者将逻辑移入 operator。)

  4. 什么时候应该选择 workflow-as-code 平台而不是 BLOGE?(当你需要跨 Go/TypeScript/Python 的多语言 SDK 支持,或者你已经运维着服务器集群并依赖其重放语义。)


实验

实验 —— 写一份可证伪的短 ADR

从你自己的系统选择一个包含至少五个步骤的流程,交付不超过一页的 ADR:

  1. 写出三条硬约束和两条偏好;硬约束必须可判定为满足或不满足。
  2. 至少比较普通方法、BLOGE 和一个 server-based 候选。
  3. 明确一个“不选 BLOGE”的反事实条件,不允许写“视情况而定”。
  4. 设计一个两周内可撤销的迁移探针,限定修改范围和真实 effect 边界。
  5. 写出 STOP / PROBE / ADOPT 结论,以及使结论失效的新证据。

完成标准:另一个读者只看 ADR,就能复述选择、证据、未知项、第一步和停止条件; 不能只看到一张功能勾选表。参考答案与评分量表在 solutions/ch33/adr-review-guide.md,完成前不要打开。


桥接:从变化归因到架构选择

本章消费变化归因的结果来作选择,却不能把设计判断包装成受控因果实验。只有当两次执行共享 Scenario、Policy、Fixture、business runtime 和 source cohort,并且只改变 DSL_GRAPHBLOGE_TOOLCHAIN 一个轴时,comparison receipt 才能说明结果变化来自哪里。ADR 还必须加入组织约束、运营成本和退出条件;这些不由 compareEvidence(...) 自动决定。


实验验收卡

  • 预期与观察: 约束漏斗得到 STOP、PROBE 或 ADOPT,不预设 BLOGE 胜出。
  • 失败与恢复: 加入硬约束使方案失效;恢复为停止或可撤销探针。
  • 证明边界: 证明选择可证伪,不证明 BLOGE 适合所有组织。
  • 练习合同: 贷款系统约束;只改一个约束;交付短 ADR;反事实能改变决定且退出条件明确即停止。

回顾

  • BLOGE 占据一个特定的架构位置:声明式 DAG 模型 + 可嵌入的库引擎,核心零外部依赖。
  • 架构适配(部署模型、持久化语义、工作流可见性、依赖足迹)层面对比工具,而不是功能清单。
  • 手写代码适合简单的顺序调用;当编排的形状需要并发、韧性或可见性时,BLOGE 才能体现出价值。
  • 步骤导向的批处理是为 ETL 量身定制的;当批处理步骤需要扇出、分支或等待外部事件时,BLOGE 才能体现出价值。
  • BPMN 工具在业务分析师拥有模型时表现出色;当开发者拥有工作流并需要代码友好的编写体验时,BLOGE 才能体现出价值。bloge-bpmn-transformer 提供了一条具体的迁移路径。
  • Workflow-as-code 平台在多语言、服务器托管的持久化方面具有优势;当你需要可嵌入的、JVM 原生的引擎且不需要独立集群时,BLOGE 才能体现出价值。
  • 从手写代码迁移遵循四个步骤:识别独立调用、提取 operator、声明 graph、验证行为。
  • 决策只输出 STOP / PROBE / ADOPT;违反硬约束时停止,未知关键事实时做可撤销探针。
  • 短 ADR 必须包含选择、依据、反事实、第一迁移步和退出标准。

下一步

第 34 章 —— 实验中,你将完成端到端的练习,将整个系列中的所有内容——graph、韧性、子图、持久化、可观测性,以及本章的迁移模式——组合成完整的、可运行的项目。


参考链接

Coding Agent: Open the versioned task guide.