BLOGE
0.9.8-RC1在线版 · 事实校验 2026-09-15 · English
第 20 章 —— Spring 与生产环境接线
承诺: 读完本章后,你能够通过 Spring Boot 启动一张 BLOGE graph, 不手写 engine bean;能够读懂 health 结果;也能只用一次
GraphEngineCustomizer覆盖完成必要调整,而不接管整棵自动配置图。
学习目标
- 添加
bloge-spring依赖,让BlogeAutoConfiguration自动接线GraphEngine、OperatorRegistry、GraphLoader和编译后的List<Graph>,无需手动定义任何 bean。 - 使用
@BlogeOperator标注实现Operator、SuspendableOperator或StreamingOperator的运行时对象,让 Spring 发现并注册它们。 - 将
GraphEngine和编译后的 graph 列表注入到一个 service 类中,执行 graph 并通过 HTTP 端点返回结果。 - 把
UP + graphCount=1读成接线观察,而不是业务证明。 - 使用
GraphEngineCustomizer调整轻量引擎,并判断何时替换整个 bean 会制造不必要的责任。
前置条件
- 第 7 章 —— 设计良好的 Operator —— 在将 operator 接入 Spring 之前,你需要掌握 operator 的设计模型。
- 第 13 章 —— 持久执行 —— 运行时存储、检查点和挂起/恢复,因为 starter 会有条件地接线所有这些组件。
- 熟悉 Spring Boot 自动配置、
@ConfigurationProperties和 Spring Boot Actuator 基础。
源示例
| 文件 | 展示内容 |
|---|---|
BlogeAutoConfiguration.java | 导入 core slice 的入口自动配置 |
BlogeProperties.java | 所有 spring.bloge.* 配置属性及嵌套分组 |
BlogeOperator.java | 由 @Component 组合而成的 operator 扫描注解 |
BlogeObservabilityAutoConfiguration.java | 指标、链路追踪、MDC、健康指示器接线 |
BlogeEndpointAutoConfiguration.java | /actuator/bloge 和 /actuator/blogeStats 端点 |
BlogeAuditAutoConfiguration.java | 审计日志监听器激活 |
SessionExecutorAutoConfiguration.java | SessionExecutor 接线,包含访问守卫和监听器 |
spring-ticket-triage.bloge | starter 示例加载的最小 DSL |
SpringBootTicketTriageApplication.java | 端到端的 Spring Boot 应用,使用 starter |
SpringTicketTriageService.java | 注入 GraphEngine 和 List<Graph> 的 Service 层 |
为什么这很重要
在前面的章节中,你通过手动方式构建引擎:
var engine = GraphEngine.builder()
.registry(registry)
.executionCheckpointStore(checkpointStore)
.listeners(listeners)
.build();
这在测试和学习时没问题。但在生产环境中,需要接线的列表会迅速膨胀:拦截器、监听器、上下文载体、持久化存储、检查点编解码器、恢复配置、分片解析器、定时器服务、事件去重 —— 而且每个 bean 都有自己的条件和回退逻辑。
bloge-spring 模块处理这部分组装工作。它是一个 Spring Boot Starter,会自动配置 classpath 与属性条件已经满足的组件。把 operator 声明为 Spring bean,并将 .bloge 文件放到 classpath 后,starter 可以为这个应用切片组装一套可运行引擎。加入 bloge-durable 或 bloge-metrics-otel 后,starter 会检测并接线其中满足条件的 bean,不需要重复声明 @Bean。这只证明应用接线已经建立,不证明持久化 store、外部 effect 或生产容量已经就绪。
核心洞察: starter 并没有隐藏引擎 —— 它只是组装引擎。多数可替换的 singleton 默认值使用
@ConditionalOnMissingBean;扩展集合和 endpoint slice 有自己的条件,因此覆盖前仍要确认目标 bean 的具体合同。
最短接线路径
不要从所有可选生产能力开始。先打通一个纵向切片:一个依赖、一个带注解的 operator、一份 classpath DSL、一个注入的 executor,以及一次 health 观察。
| 你负责 | Starter 负责 | 可观察检查 |
|---|---|---|
实现运行时 operator 合同的 @BlogeOperator bean | OperatorRegistry | DSL 加载时能解析 operator 名 |
classpath:/bloge/*.bloge | GraphLoader 与编译后的 List<Graph> | 目标 graph 存在 |
| service 输入与输出 | 轻量 GraphEngine / GraphExecutor | 一次执行返回预期节点事实 |
| 部署依赖 | Actuator 存在时的 BlogeHealthIndicator | UP、graphCount=1 |
这里的 UP 只表示应用至少加载了一张 graph。它不表示订单 Scenario 通过、
外部支付系统健康,也不表示该版本具备发布资格。
心智模型
把 bloge-spring 想象成一条 分层装配线。每一层只有在其 classpath 和属性前置条件都满足时才会激活:
第 2–6 层是叠加且独立的。你不需要一次全部启用,移除某个 classpath 依赖就能干净地移除对应层。
第一个可运行示例
这是来自
bloge-examples/src/main/java/com/leanowtech/bloge/examples/integration/spring/ 的工单分流示例。
步骤 1 — 添加依赖
<dependency>
<groupId>com.leanowtech.bloge</groupId>
<artifactId>bloge-spring</artifactId>
<version>${bloge.version}</version>
</dependency>
步骤 2 — 把 operator 编写为 Spring bean
下面的示例对 SpringTicketClassifierOperator.java 做了简化:
@BlogeOperator(
value = "SpringTicketClassifierOperator",
description = "Classifies a support ticket",
owner = "examples",
tags = {"spring", "starter", "triage"}
)
public class SpringTicketClassifierOperator implements
Operator<Map<String, Object>, ClassifiedTicket> {
public record ClassifiedTicket(String queue, int priorityScore, boolean vip) {}
@Override
public ClassifiedTicket execute(Map<String, Object> input, OperatorContext ctx) {
boolean vip = "vip".equalsIgnoreCase(String.valueOf(input.get("customerTier")));
String queue = vip ? "vip-escalation" : "general-support";
int priorityScore = vip ? 70 : 30;
return new ClassifiedTicket(queue, priorityScore, vip);
}
}
@BlogeOperator 由 @Component 组合而成,因此 Spring 会发现这个 bean;
registry 只会接收其中实现 Operator、SuspendableOperator 或
StreamingOperator 的对象。value 设置 DSL 中的 operator 名。仓库里的完整
示例刻意把 domain bean 保持为普通对象,再通过显式 OperatorRegistry 适配;
上面的精简版选择的是零手写 registry 路径。
步骤 3 — 把 DSL 放到 classpath 上
来自 spring-ticket-triage.bloge:
graph springTicketTriage {
node classifyTicket : SpringTicketClassifierOperator {
input {
ticketId = ctx.ticketId
message = ctx.message
customerTier = ctx.customerTier
}
timeout = 2s
}
node draftReply : SpringReplyDraftOperator {
depends_on = [classifyTicket]
input {
ticketId = ctx.ticketId
queue = classifyTicket.output.queue
priorityScore = classifyTicket.output.priorityScore
vip = classifyTicket.output.vip
}
timeout = 2s
}
}
starter 的默认 DSL 位置是 classpath:bloge/。将此文件放在 src/main/resources/bloge/spring-ticket-triage.bloge,它就会被自动加载。
步骤 4 — 注入并执行
来自 SpringTicketTriageService.java:
@Service
public class SpringTicketTriageService {
private final GraphEngine engine;
private final Map<String, Graph> graphsByName;
public SpringTicketTriageService(GraphEngine engine, List<Graph> graphs) {
this.engine = engine;
this.graphsByName = graphs.stream()
.collect(Collectors.toUnmodifiableMap(Graph::name, Function.identity()));
}
public TicketTriageResponse triage(String ticketId, String message, String customerTier) {
Graph graph = graphsByName.get("springTicketTriage");
var result = engine.execute(graph, new GraphContext(Map.of(
"ticketId", ticketId,
"message", message,
"customerTier", customerTier
)));
return /* 将 result 映射为 response */;
}
}
没有 GraphEngine.builder()。没有 DefaultOperatorRegistry。没有 GraphLoader 调用。starter 已经把这一切全部组装好了。
步骤 5 — 通过 HTTP 暴露
来自 SpringTicketTriageController.java:
@RestController
@RequestMapping("/api/bloge/tickets")
public class SpringTicketTriageController {
private final SpringTicketTriageService triageService;
public SpringTicketTriageController(SpringTicketTriageService triageService) {
this.triageService = triageService;
}
@GetMapping("/triage")
public TicketTriageResponse triage(
@RequestParam("ticketId") String ticketId,
@RequestParam("message") String message,
@RequestParam(name = "customerTier", defaultValue = "standard") String customerTier) {
return triageService.triage(ticketId, message, customerTier);
}
}
一次 override,只增加一个明确 owner
假设默认轻量引擎都合适,只有 scheduler 集成需要调整。保留 starter 管理的 其它 bean,只使用 builder 接缝:
@Bean
GraphEngineCustomizer schedulerOverride(MyTimerSupport timerSupport) {
return builder -> builder.schedulerTimerSupport(timerSupport);
}
Customizer 在轻量 engine-mode preset 之后、核心 GraphEngine 构建之前运行;
它不配置 durable runtime 路径。如果替换整个 GraphEngine bean,你也会
同时接管 listener、interceptor、context carrier 和未来 starter 默认值。
本章余下部分作为生产参考。首次阅读只需完成最短切片、health 边界和这一次 override,就已经形成完整闭环。
生产参考
上面的最短路径已经足以理解所有权。需要查询完整 bean 清单、属性分组、持久化 store 接线、健康检查或 HTTP 错误契约时,请转到附录 I —— Spring 生产参考。
常见陷阱
❌ 在 @BlogeOperator 已存在时仍手动注册 operator
// 如果你同时也用 @BlogeOperator 标注了这个类,就不要这么做:
@Bean
public OperatorRegistry operatorRegistry() {
var registry = new DefaultOperatorRegistry();
registry.register("FetchUserOperator", new FetchUserOperator(userService));
return registry;
}
因为 BlogeAutoConfiguration 提供了一个会扫描 @BlogeOperator 标注 bean 的 OperatorRegistry bean,而且 它的 bean 带有 @ConditionalOnMissingBean,声明你自己的 OperatorRegistry bean 会完全替换掉自动配置的那个。你的自定义 registry 不会扫描 @BlogeOperator bean —— 因此所有标注的 operator 会悄然消失。
修复: 要么完全依赖 @BlogeOperator 扫描(标准路径),要么提供一个包含所有需要内容的完整 OperatorRegistry bean。不要混用两种方式。
常见故障
Operator 悄然消失
如果一个类已经标注 @BlogeOperator("MyOperator"),同时又声明了空的自定义
OperatorRegistry bean,@ConditionalOnMissingBean 会让自动扫描 registry
退出。DSL 随后会以 unregistered operator 'MyOperator' 停止加载。
修复边界只有两个:删除自定义 registry,完全交给自动扫描;或者让自定义 registry 显式注册应用需要的全部 operator。不能把两种 owner 混在一起。
每次只增加一个生产扩展
只有明确的运维需求出现时,才增加可观测性、审计、恢复或 interceptor。附录 I 提供配置地图与部署检查;本章只保留一条设计规则:每个 override 都必须只有一个理由和一个 owner。
脑力检查
-
自动注册一个 operator 需要同时满足什么? (答案:bean 带有
@BlogeOperator,并实现Operator、SuspendableOperator或StreamingOperator;单有注解只保证 Spring 发现。) -
starter 默认在哪里查找
.bloge文件?哪个属性可以覆盖它? (答案:classpath:bloge/,可通过spring.bloge.dsl-locations配置。) -
如果你声明了自己的
OperatorRegistrybean,自动配置的那个会怎样? (答案:它会被跳过 ——@ConditionalOnMissingBean意味着你的 bean 优先,而自动@BlogeOperator扫描将不再执行。) -
列举两个仅通过添加 classpath 依赖就能激活、无需修改任何属性的生产特性。 (答案:当
spring-boot-actuator存在时,Actuator 端点自动激活;当bloge-metrics-otel存在时,可观测性指标和链路追踪自动激活。) -
当没有找到任何
.bloge文件时,BlogeHealthIndicator报告什么? (答案:DOWN,附带详情"reason": "No graphs loaded"。)
实验
从一个全新的 Spring Boot 应用开始:
-
添加
bloge-spring到你的 POM,创建一个用@BlogeOperator标注、并实现Operator的对象,返回一个问候字符串。 -
编写一个单节点
.blogegraph,放在src/main/resources/bloge/下,引用你的 operator。 -
创建一个
@Service,注入GraphEngine和List<Graph>,按名称查找你的 graph 并执行它。 -
验证健康指示器:启动应用并访问
/actuator/health。确认bloge组件显示UP以及你的 graph 名称。 -
故意破坏它:重命名
.bloge文件使其无法加载任何 graph。重启并验证健康指示器报告DOWN。 -
添加一个自定义
ExecutionListenerbean,记录 graph 级别的完成耗时。再次执行你的 graph,确认该监听器被触发,且无需对引擎 bean 做任何修改。
实验验收卡
- 预期与观察: starter 装配 engine、loader 和 health,一个 customizer 只有一个 owner。
- 失败与恢复: 移除 bean 或重复 override;恢复最短自动配置。
- 证明边界: 证明 Spring 接线和 health,不证明生产容量或业务正确性。
- 练习合同: 最小应用;只改一个 bean/customizer;交付 context 和 health;启动成功且 owner 唯一即停止。
回顾
bloge-spring是 Spring Boot Starter;只有 classpath、property 与 bean 条件满足时,它才会自动配置GraphEngine、OperatorRegistry、GraphLoader和编译后的 graph。@BlogeOperator是由@Component组合而成的注解。标注的 bean 只有实现 BLOGE 运行时 operator 合同后才会自动注册;普通 domain bean 需要 adapter/customizer 路径。其promptHint、usageExample、constraintsDescription属性支持 agent/LLM 集成。- starter 使用 分层的条件化自动配置:核心 bean 只在 starter 条件生效的 context 中存在;持久化存储在
bloge-durable+DataSource存在时激活;可观测性在bloge-metrics-otel存在时激活;审计和恢复在各自的属性设置为true时激活。 - 自动配置被拆分为聚焦的切片(
BlogeCoreAutoConfiguration、BlogeGraphEngineAutoConfiguration、BlogeDurableAutoConfiguration等),实现了更清晰的关注点分离。 - 新增
spring.bloge.engine-mode属性用于选择引擎预设(AUTO或REQUEST_RESPONSE)。 - 新增
spring.bloge.tenant.*属性启用自动配置的多租户支持 —— 租户解析、命名空间解析、并发上限和速率限制 —— 无需 编写自定义TenantContextResolverbean。 - 新增
spring.bloge.version-routing.*属性在 starter 层面支持latest、pinned和canarygraph 版本路由。 GraphEngineCustomizer让你无需替换整个 bean 即可微调引擎 builder。- 多数可替换的 singleton 默认值带有
@ConditionalOnMissingBean;覆盖前应检查具体 slice。 - 自定义的
OperatorInterceptor、ExecutionListener和CallerContextCarrierbean 通过ObjectProvider自动收集并注入引擎。 - Actuator 端点(
/actuator/bloge、/actuator/blogeStats)提供已加载 graph 和执行指标的运行时可见性。 BlogeHealthIndicator开箱即用地提供/actuator/health集成。
下一步
在第 21 章——单 JVM 里的调度与复杂度中,你会先隔离本地调度机制:ready set、完成事件与 graph 复杂度护栏。分布式所有权和容量留到第 22 章。
参考链接
bloge-spring模块 README —— 完整属性表和快速入门指南BlogeAutoConfiguration.java—— 核心自动配置源码BlogeObservabilityAutoConfiguration.java—— 指标、链路追踪、健康指示器BlogeEndpointAutoConfiguration.java—— Actuator 端点BlogeAuditAutoConfiguration.java—— 审计日志SpringBootTicketTriageApplication.java—— 完整的 Spring Boot 示例AutoConfiguration.imports—— starter 注册文件- 入门指南 —— 环境搭建指南
- 核心架构 —— 引擎内部机制参考
Coding Agent: Open the versioned task guide.