Skip to main content

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

附录 I —— Spring 生产参考

请在第 20 章已经跑通后使用本附录。这里是生产装配的查询面,不是第二条教程主线。

如果组件所有权仍不清楚,请回到第 20 章

拆开来看

starter 自动配置了什么

自动配置通过 Spring Boot 3.x 的 AutoConfiguration.imports 文件注册,其中列出了十个顶层自动配置入口。下表还展开了 BlogeAutoConfiguration 内部导入的 BlogeCoreAutoConfiguration

配置类提供的内容激活条件
BlogeAutoConfiguration入口点 — 导入 BlogeCoreAutoConfiguration始终激活
BlogeCoreAutoConfigurationOperatorRegistryGraphLoaderList<Graph>、租户解析器、租户过滤器始终激活
BlogeDurableMigrationConfiguration显式的加密 checkpoint 迁移工具bloge.decrypt-checkpoints=true 且 classpath 上存在迁移类
BlogeGraphEngineAutoConfiguration通过 builder 创建 GraphEngine,包含所有 ObjectProvider 协作者未启用 checkpoint 解密迁移模式时激活
BlogeDurableAutoConfiguration所有持久化存储、RecoveryConfigRoutingStore、Flyway 迁移classpath 上有 bloge-durable 且存在 DataSource bean
BlogeAuditAutoConfigurationAuditJournalListenerspring.bloge.audit.enabled=true 且存在 AuditJournalStore bean
SessionExecutorAutoConfiguration纯内存 SessionExecutor;可选的 durable session adapter 与 managerclasspath 上有 bloge-session-ext;durable bean 还要求对应类、store 与 definition lookup
BlogeStateMachineAutoConfigurationdurable state-machine manager 与恢复接线classpath 上有 durable state-machine 类且 spring.bloge.state-machine.enabled=true
BlogeEndpointAutoConfiguration/actuator/bloge/actuator/blogeStatsclasspath 上有 spring-boot-actuator
BlogeObservabilityAutoConfiguration指标、链路追踪、MDC、日志、健康指示器classpath 上有 bloge-metrics-otel
BlogeEventJournalAutoConfiguration事件日志保留和生命周期classpath 上有 bloge-event-journal

如果你要接入多轮 session 流程,可以配合阅读第 14 章 —— 多轮 Session。 那一章会把 SessionExecutorAutoConfiguration 背后真正接起来的运行时模型讲清楚: session graph、phase / round 执行、空闲超时,以及可选的持久恢复。

@BlogeOperator 注解

定义在 BlogeOperator.java 中:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface BlogeOperator {
@AliasFor(annotation = Component.class, attribute = "value")
String value() default "";
String description() default "";
String owner() default "";
String[] tags() default {};
String promptHint() default "";
String usageExample() default "";
String constraintsDescription() default "";
}

它底层就是 @Component,所以 Spring 能发现它。BlogeAutoConfiguration.operatorRegistry() bean 方法会扫描所有标注了 @BlogeOperator 的 bean,并以注解的 value() 为键将每一个注册到 DefaultOperatorRegistry 中。

面向 agent/LLM 集成时,三个额外属性向 AI agent 描述该 operator: promptHint 告诉 agent 何时选择此 operator;usageExample 给出展示该 operator 用法的 DSL 片段;constraintsDescription 记录了从 schema 中不易察觉 的约束。这三个属性均为可选。

GraphEngine 是如何组装的

BlogeGraphEngineAutoConfiguration 负责创建引擎 bean,接受 tenantContextResolvertenantResourcePolicy 用于多租户部署, 并提供 GraphEngineCustomizer 回调钩子,让你无需替换整个引擎 bean 即可调整它。

BlogeGraphEngineAutoConfiguration 中的 graphEngine() bean 方法对每个可选协作者都使用了 ObjectProvider

@Bean
@ConditionalOnMissingBean
public GraphEngine graphEngine(
OperatorRegistry registry,
ObjectProvider<OperatorInterceptor> interceptors,
ObjectProvider<ExecutionListener> listeners,
ObjectProvider<CallerContextCarrier> contextCarriers,
ObjectProvider<CheckpointCodec> checkpointCodec,
ObjectProvider<VersionRoutingPolicy> versionRoutingPolicy,
ObjectProvider<TenantContextResolver> tenantContextResolver,
ObjectProvider<TenantResourcePolicy> tenantResourcePolicy,
ObjectProvider<GraphEngineCustomizer> customizers,
/* ... 更多 ObjectProvider ... */) {
GraphEngine.Builder builder = GraphEngine.builder()
.registry(registry)
.interceptors(interceptors.orderedStream().toList())
.listeners(listeners.orderedStream().toList())
.contextCarriers(contextCarriers.orderedStream().toList())
.checkpointCodec(checkpointCodec.getIfAvailable())
.versionRoutingPolicy(versionRoutingPolicy.getIfAvailable())
.tenantContextResolver(tenantContextResolver.getIfAvailable())
.tenantResourcePolicy(tenantResourcePolicy.getIfAvailable());

if (properties.getEngineMode() == BlogeProperties.EngineMode.REQUEST_RESPONSE) {
builder.requestResponseDefaults();
}

customizers.orderedStream().forEach(c -> c.customize(builder));
return builder.build();
}

这意味着你声明为 Spring bean 的 任何 OperatorInterceptorExecutionListenerCallerContextCarrierTenantContextResolverTenantResourcePolicy 都会被引擎自动拾取。你永远不需要手动操作引擎的 builder。

GraphEngineCustomizer 是一个回调接口,让你在所有自动配置默认值和 engine-mode 预设应用完毕后微调 GraphEngine.Builder —— 无需替换整个引擎 bean:

@Bean
public GraphEngineCustomizer myCustomizer() {
return builder -> builder
.schedulerTimerSupport(myTimerSupport);
}

核心配置属性

声明在 BlogeProperties.java 中,前缀为 spring.bloge

spring:
bloge:
dsl-locations: classpath:bloge/ # .bloge 文件的扫描位置
hot-reload: false # 文件监听并在变更时重新编译
default-timeout: 30s # 默认节点超时
accessor-mode: method-handle # 表达式编译模式
engine-mode: auto # 引擎预设(auto | request-response)
streaming-default-buffer-size: 16 # 流式执行缓冲区大小

engine-modestreaming-default-buffer-size engine-mode 为引擎 builder 选择高层预设。AUTO(默认)使用标准设置; REQUEST_RESPONSE 为短生命周期、请求范围内的 graph 执行应用优化默认值。 streaming-default-buffer-size 控制流式 graph 执行的默认背压缓冲区。

租户属性

spring.bloge.tenant.enabled=true 时,starter 会创建 SpringTenantContextResolverSpringTenantResourcePolicy bean, 注入引擎的多租户准入控制:

spring:
bloge:
tenant:
enabled: false # 主开关
mode: header # header | security-context | static
header-name: X-Tenant-Id # 租户解析用的 HTTP 头
namespace-header-name: X-Namespace # 命名空间解析用的 HTTP 头
default-tenant: default # 回退租户 ID
default-namespace: default # 回退命名空间
max-concurrent-executions: 100 # 每租户并发上限
max-starts-per-minute: 0 # 每租户速率限制(0 = 无限制)

mode 属性决定租户的解析方式:

模式如何解析租户
HEADER从 HTTP 请求头中读取 X-Tenant-IdX-Namespace
SECURITY_CONTEXT从 Spring Security 的 Authentication.getDetails() 中提取 tenantId / namespace
STATIC始终返回 default-tenant / default-namespace

启用租户支持后,starter 还会注册一个 TenantContextFilter —— 一个 servlet 过滤器,通过 TenantContextHolder.callWith() 将解析后的 TenantContext 绑定到当前请求线程,使整个 HTTP 请求期间的存储查询和默认 GraphContext 构造 都能看到同一个租户。

版本路由属性

spring:
bloge:
version-routing:
policy: latest # latest | pinned | canary
pinned-version: null # policy=pinned 时必填
canary-primary: null # canary 的主版本
canary-candidate: null # canary 的候选版本
canary-percentage: 10 # 路由到候选版本的流量百分比

持久化路由属性

spring:
bloge:
durable:
routing:
mode: single # single | hash | tenant
tenant-strategy: null # shared-table | separate-schema | separate-database
default-shard-id: default
fallback-shard-id: null
shard-ids: []
tenant-shards: {} # tenantId → shardId 的映射

持久化存储接线

当 classpath 上有 bloge-durable 且存在 DataSource bean 时,starter 会创建一个 durableSqlSessionFactory bean,运行 Flyway 迁移(如果 spring.bloge.durable.flyway.enabled=true,这是默认值),并注册十多个运行时存储 bean —— 每个都带有 @ConditionalOnMissingBean,所以你可以覆盖其中任何一个。完整列表包括 ExecutionStoreExecutionCheckpointStoreWaitStoreWorkItemStoreGraphRegistryStoreRoutingStoreTaskInboxStoreEventMatcherStoreEventDeduplicationStoreArchiveServiceDurableControlPlaneService

健康指示器

BlogeHealthIndicator 在至少一个 graph 被加载时报告 UP,在列表为空时报告 DOWN 并附带 "reason": "No graphs loaded"。它出现在 /actuator/healthbloge 组件下。


错误响应 —— RFC 9457 ProblemDetail

bloge-spring-web 在 classpath 上时,所有 BLOGE 异常都会被映射到 RFC 9457 ProblemDetail,这样客户端看到的是统一、机器可读的错误格式, 而不是堆栈。

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
"type": "https://bloge.dev/problems/graph-version-mismatch",
"title": "Graph version mismatch",
"status": 422,
"detail": "Execution started against orderProcess@v3 but live registry has @v4.",
"instance": "/executions/exec-12af9",
"executionId": "exec-12af9",
"graphId": "orderProcess",
"expectedHash": "sha256:1f3a…",
"actualHash": "sha256:9c0e…"
}

BlogeProblemDetailAdvice 内置的映射:

异常HTTP 状态type URI 后缀
GraphDefinitionException400/problems/graph-definition
GraphValidationException422/problems/graph-validation
GraphVersionMismatchException422/problems/graph-version-mismatch
SessionAccessDeniedException403/problems/session-access-denied
StaleLeaseException409/problems/stale-lease
OperatorTimeoutException504/problems/operator-timeout
其他500/problems/internal(不泄漏异常细节)

要禁用或覆盖,自己声明一个 @RestControllerAdvice bean 即可 —— 自动配置 通过 @ConditionalOnMissingBean(BlogeProblemDetailAdvice.class) 让位。


引导式重写

从工单分流示例出发,逐层添加生产特性。

1. 添加可观测性

在 POM 中添加 bloge-metrics-otel。无需修改任何配置 —— starter 会检测到 classpath 上的 MetricsExecutionListener,并将其注册到你现有的 MeterRegistry 中。

验证:

GET /actuator/bloge

你应该看到:

{
"graphCount": 1,
"graphs": [
{
"name": "springTicketTriage",
"nodeCount": 2,
"edgeCount": 1,
"sourceNodes": ["classifyTicket"],
"terminalNodes": ["draftReply"]
}
]
}

2. 启用审计日志

application.yml 中添加:

spring:
bloge:
audit:
enabled: true
capture-input: true
capture-output: false
async-flush: true
flush-interval: 100ms
batch-size: 64

这需要一个持久化 DataSource,以便 AuditJournalStore 可用。BlogeAuditAutoConfiguration 会创建一个 AuditJournalListener,引擎会自动拾取它。

3. 启用崩溃恢复

spring:
bloge:
recovery:
enabled: true
scan-interval: 30s
batch-size: 10
claim-timeout: 5m
max-recovery-attempts: 3

starter 会创建一个 RecoveryConfig bean 并将其接入 GraphEngine。引擎在后台管理执行租约,并扫描过期的 RUNNING 执行以进行恢复。

恢复需要 classpath 上有 bloge-durable,以及版本 V11 或更高的 Flyway 迁移。

4. 添加自定义拦截器

@Bean
public OperatorInterceptor loggingInterceptor() {
return invocation -> {
log.info("Executing node: {}", invocation.nodeId());
return invocation.proceed();
};
}

graphEngine() bean 通过 ObjectProvider<OperatorInterceptor> 收集所有 OperatorInterceptor bean,并按 Spring 排序注入。无需编写注册代码。

5. 生产部署模式

生产部署通常再增加 graph 版本路由、按 Spring profile 切换 operator 实现, 以及共享 durable store 的滚动升级。它们都属于部署参考,不改变本章的最短 接线主线;先固定单一变化轴,再分别验证版本、环境绑定或恢复行为。


生产就绪快速检查

边界发布前必须回答的问题
组件所有权哪些 bean 来自自动配置,哪些被 override,谁拥有 override?
Operator 发现应用能否证明每个必需 operator 恰好注册一次?
持久化声称重启恢复时,execution、wait 与 work-item store 是否真正持久?
路由Tenant、version 与 durable routing 策略是否显式且经过测试?
错误契约客户端是否得到稳定 ProblemDetail type,并且看不到内部异常?
可观测性一个 execution identity 能否连接日志、指标、trace 与业务事件?
恢复是否针对当前 store 与版本完成过冷重启测试?

请记录答案与证据位置。没有 owner、测试和运维后果的属性值,只是配置知识,不是生产就绪证据。