BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 2 章 —— 你的第一个 Graph
本章通过一个最小可运行示例说明 BLOGE graph 的编写、编译与执行过程,包括 DSL 方式和 Fluent Java API 方式。
学习目标
- 写出一个能够被编译器接受的最小
.bloge文件。 - 从 Java 里加载并执行这个文件。
- 用 Fluent Java API 构建同一个 graph。
- 理解
ctx、input、output与timeout在运行时分别意味着什么。
前置条件
- 第 1 章 —— BLOGE 是什么,以及它为什么存在
- JDK 25+ 且启用
--enable-preview,Maven 3.6+ (参见入门指南)
源示例
| 文件 | 展示内容 |
|---|---|
hello-world.bloge | 最小可运行示例:一个 graph、一个 node、一个输入绑定 |
two-node-chain.bloge | 两个节点组成的链路,演示显式依赖 |
input-bindings.bloge | Conformance fixture:最基础的输入绑定语法 |
为什么这很重要
第 1 章已经交代了 BLOGE 的定义、核心特征,以及它为何不同于手写编排。本章继续把这些概念落到一个最小可运行示例上,使 graph 的编写、装载和执行过程都落到具体结果上。
一个单节点 graph 已经足以把四个最基础的运行时概念放到同一条执行路径里:
- 数据如何进入 graph(context)。
- node 如何拿到数据(input block)。
- 引擎如何调用你的代码(operator)。
- 执行完成后如何读结果。
后面的所有内容,都会建立在这四个概念之上。
环境准备
如果你想动手跟着本章做,请使用现在与本书同仓库的 companion 项目。
-
先安装 standalone companion module 依赖的核心 BLOGE artifact:
mvn -q -pl bloge-core,bloge-dsl,bloge-test -am install -DskipTests -Dspotbugs.skip=true -
运行 companion 示例测试:
mvn -f head-first-bloge-examples/pom.xml test -
打开第 2 章 companion 文件:
不要只读本章片段。最好把本书和 companion 项目并排打开,边看边运行,这样例子始终和可执行的东西绑在一起。
项目布局
每一章的 .bloge 文件都放在 src/main/resources/bloge/chNN/ 下。src/test/java/ 里的测试类会编译或执行这些文件。随着你继续阅读本书,记得运行对应章节的 companion 示例,让理解始终落到真实产物上。
心智模型
- 你先创建一个 GraphContext,把初始值放进去。
- 引擎解析 input bindings —— 比如
ctx.message—— 从 context(或上游输出)取值。 - 每个 node 的 operator 收到组装好的输入,执行逻辑并返回输出。
- 执行结束后,你从 GraphResult 里读取结果。
第一个可运行示例
DSL 写法
这就是
hello-world.bloge
的内容:
graph helloWorld {
node echo : EchoOperator {
input {
message = ctx.message
}
timeout = 1s
}
}
这已经是一个完整且合法的 BLOGE graph。逐项拆开看:
| 语法 | 含义 |
|---|---|
graph helloWorld | 声明一个名为 helloWorld 的 graph |
node echo | 声明一个名为 echo 的节点 |
: EchoOperator | 指定这个节点执行的 operator |
input { message = ctx.message } | 把 context 里的 message 绑定到节点输入字段 message |
timeout = 1s | 节点必须在 1 秒内完成 |
从 Java 里加载并运行它:
// 1. 注册你的 operator
OperatorRegistry registry = new DefaultOperatorRegistry();
registry.register("EchoOperator", (input, opCtx) -> input);
// 2. 加载 DSL 文件
GraphLoader loader = new GraphLoader(registry);
Graph graph = loader.load(Path.of("src/main/resources/bloge/hello-world.bloge"));
// 3. 构造上下文
GraphContext ctx = new GraphContext();
ctx.put("message", "hello");
// 4. 执行
GraphEngine engine = GraphEngine.builder().registry(registry).build();
GraphResult result = engine.execute(graph, ctx);
Java API 写法
同一个 graph,也可以用 Fluent Java API 构造:
Graph graph = Graph.builder("helloWorld")
.node("echo", (Map<String, Object> input, OperatorContext opCtx) ->
Map.of("message", opCtx.graphContext().get("message", String.class)))
.timeout(Duration.ofSeconds(1))
.build();
GraphContext ctx = new GraphContext();
ctx.put("message", "hello");
GraphEngine engine = GraphEngine.builder().build();
GraphResult result = engine.execute(graph, ctx);
两种写法最终生成的是同一个运行时 graph。DSL 更声明式、更适合外部编排;Java API 更适合在代码里做类型安全组合。很多团队会先用 DSL 做原型,再把稳定下来的 operator 逻辑收进 Java 类。
先读懂三种结果,再增加配置
第一个 graph 不能以“能够编译”作为终点。你还应该能从一份很小的结果里 看出:执行是否成功,以及两种常见失败停在哪个阶段。这样,后面的 Builder 选项才是在解决问题,而不是一份需要背诵的 API 清单。
结果 1 —— 确实拿到了值
RC1 基础探针以 message=hello 运行 hello-world,并检查结果,而不是把
“没有抛异常”当成成功:
graph: hello-world status: SUCCESS
node: echo status: EXECUTED
output.message: hello
这里有三个独立事实:graph 已结束、echo 确实执行、输出值符合预期。graph
变大以后,这三类断言仍应保留。
结果 2 与 3 —— 执行根本没有开始
| 阶段 | 可观察结果 | 修复最小 owner |
|---|---|---|
| 编译 | node 没有 Operator 绑定,诊断指向声明位置 | 修正 .bloge node 声明 |
| 加载 | registry 中不存在 MissingOperator | 注册能力或修正名称 |
| 执行 | 已绑定的 Operator 返回或抛错 | 检查 node 状态、输出与失败证据 |
编译失败和加载失败都应满足 operator calls = 0。重试 graph 无法修复这两类
问题,因为业务代码从未被派发。
到这里,Builder 选项才有了落点:listener 观察阶段,interceptor 包裹真实 调用,并发参数改变调度,checkpoint store 改变执行状态的保留方式。先看到 问题,再选择拥有这个问题的配置项。
拆开来看
加一个第二节点
看一下
two-node-chain.bloge:
graph twoNodeChain {
node normalizeName : NormalizeNameOperator {
input { name = ctx.name }
timeout = 1s
}
node buildGreeting : BuildGreetingOperator {
depends_on = [normalizeName]
input { value = normalizeName.output.value }
timeout = 1s
}
}
新增了两个概念:
| 概念 | 含义 |
|---|---|
depends_on = [normalizeName] | 显式依赖 —— buildGreeting 必须等待 normalizeName |
normalizeName.output.value | 读取上游节点输出里的 value 字段 |
注意:即使不写 depends_on,由于 buildGreeting 的输入里引用了 normalizeName.output.value,编 译器也会自动推断出这个依赖。显式声明只是让意图更清楚。
Conformance 基线
Conformance 测试套件里有一个最小输入绑定示例
(input-bindings.bloge):
graph g {
node a : Op {
input {
x = ctx.foo
y = "bar"
}
}
}
它说明 input 右侧既可以是 context 引用(ctx.foo),也可以是字面量("bar")。
只在观察到问题后增加配置
不要把所有 Builder 选项复制进第一个 graph。先保留下面的路由表,等故事走到 对应问题时再回来:
| 观察到的需要 | 配置 owner 与后续章节 |
|---|---|
| 拒绝不兼容的输入输出形状 | Graph.builder().schemaValidation(...) 与 .contracts(...) —— 第 8 章 |
| 观察或包裹每次调用 | GraphEngine.builder().listeners(...) 与 .interceptors(...) —— 第 19 章 |
| 限制同时运行的工作量 | .maxGlobalConcurrency(...) —— 第 21 章 |
| 保留或迁移执行状态 | .executionCheckpointStore(...)、.checkpointCodec(...)、.versionMismatchPolicy(...) —— 第 13 章 |
| 解析租户范围与准入 | .tenantContextResolver(...)、.tenantResourcePolicy(...) —— 第 21 章 |
| 替换调度时间实现 | .schedulerTimerSupport(...) —— 第 11—12 章 |
Graph.builder() 描述一个 graph;GraphEngine.builder() 配置可以运行多个 graph
的 runtime。说不清要解决哪个已观察问题时,就保留默认值。
常见陷阱
❌ 忘了写 operator 绑定
// 错误 —— 编译器需要知道节点要运行哪个 operator
node echo {
input { message = ctx.message }
}
每个 node 都必须通过 : OperatorName 的形式声明 operator。否则编译器根本不知道该调哪段逻辑。
// 正确
node echo : EchoOperator {
input { message = ctx.message }
}
常见故障
缺少 operator 绑定 —— 编译器错误
如果你忘了写 operator 类型:
node echo {
input { message = ctx.message }
}
解析器会立刻停下来,报出的核心信息是:
GraphDefinitionException: Expected ':' after node id
如果你保留了冒号,但后面仍然没有 operator 名称,接下来还会看到
Expected operator reference。
修复: 每个 node 都必须写成 : OperatorName。
operator 未注册 —— 加载阶段错误
如果 DSL 里引用了注册表中不存在的 operator:
registry.register("EchoOperator", (input, opCtx) -> input);
Graph graph = loader.load(Path.of("hello-world.bloge"));
// 但 DSL 里写的是 "ShoutOperator"
那么在加载 graph 时就会失败,还没进入执行阶段:
GraphDefinitionException: Node 'shout' references unregistered operator 'ShoutOperator'
修复: 在 load() 之前,把 .bloge 文件里出现的每个 operator 名称都注册好。
引导式重写
从 hello-world 这个 graph 出发,继续往前走一步:
- 新增一个名为
shout的节点,把echo的输出转成大写。 - 给它设置
timeout = 2s。 - 预测执行顺序。 哪个节点先运行?为什么?
你的 DSL 大概会长这样:
graph helloWorld {
node echo : EchoOperator {
input { message = ctx.message }
timeout = 1s
}
node shout : ShoutOperator {
input { message = echo.output.message }
timeout = 2s
}
}
因为 shout 引用了 echo.output,引擎会自动推断依赖,所以必然是 echo 先执行,再轮到 shout。
脑力检查
- 要执行一个 graph,最少需要哪四样东西?(Graph、OperatorRegistry、GraphContext、GraphEngine。)
- 如果某个节点的输入绑定引用了
ctx.userId,这个值来自哪里?(来自你在执行前构造的 GraphContext。) - 同一个
inputblock 里能不能同时写字面量和上游引用?(可以——Conformance fixture 里x = ctx.foo与y = "bar"就是同一个 block。) - 你在 workshop 里勾画一个 workflow,想要最快的“改了就能跑”反馈循环。你会先用 DSL 文件还是 Fluent Java API?什么时候会切换到另一种?(多数团队先用 DSL,因为 graph 的形状更直观、改起来更快。当你需要更强的代码复用、更紧的类型约束,或更深的应用级组合时,再切换到 Java API。)
- 设计题: 如果一个 graph 有十个节点,但只有两个节点真的需要从 context 取数据,是不是每个节点都必须写
inputblock?(不是。只有消费外部数据或上游输出的节点才需要input。没有inputblock 的节点会收到一个空输入 map。)
练习
- 新建一个
my-first-graph.bloge文件,定义一个graph greet,包含:- 一个
fetchName节点,读取ctx.name。 - 一个
buildGreeting节点,读取fetchName.output.name并生成问候语。
- 一个
- 在 Java 里注册这两个 operator,加载这个文件,用
ctx.put("name", "Alice")执行 graph,并打印结果。 - 通过
GraphResult验证buildGreeting是否确实在fetchName之后执行。
项目环境与运行细节可参考入门指南。
实验验收卡
- 预期与观察: 最小 graph 返回 SUCCESS,节点为 EXECUTED,最终 output 可读取。
- 失败与恢复: 拼错 operator 名得到 compile/load error;恢复名称后重跑。
- 证明边界: 只证明最小装配和结果读取,不证明重试、持久化或业务正确性。
- 练习合同: hello graph;只改一个 operator 标识;交付成功和失败输出;两者可重复即停止。
回顾
- 一个最小 BLOGE graph 需要有
graph块、至少一个带 operator 绑定的node,以及对应的input。 - GraphContext 负责携带初始数据;input bindings 负责把数据送进节点。
- Fluent Java API 和
.blogeDSL 最终构造的是同一种运行时 graph。 - 依赖关系既可以是显式的(
depends_on),也可以通过.output自动推断出来。
下一步
第 3 章 —— 用依赖关系思考进一步展开依赖图调度,以及“按依赖思考,而不是按顺序思考”为什么是 graph 设计的关键 。
参考链接
- 入门指南 —— 完整环境说明
- DSL 规范 —— 完整语法
hello-world.bloge—— 源码two-node-chain.bloge—— 源码input-bindings.bloge—— conformance fixture
Coding Agent: Open the versioned task guide.