CommandExecutor
CommandExecutor 是 Command Pipeline 的服务端入口。REST 端点 POST /api/meta/commands/execute/{commandCode} 在 HTTP 上做的事情,CommandExecutor.execute(commandCode, request) 可以在任何 Java 代码路径中以 inline 方式完成。插件用它从 listener、定时任务、agent 工具、迁移脚本中组合 command —— 而无需再经 HTTP。
概念
该 executor 编排一条多阶段 pipeline:idempotency → authorize(CommandAuthorizationPhase)→ load → validate(SchemaValidatePhase + ValidatorExtension)→ pre-actions(PreActionsPhase)→ handler(HandlerPhase,执行 CommandHandlerExtension.execute)→ state-check → post-execution(PostExecutionPhase)→ completion。整个 pipeline 在 executor 持有的同一个 @Transactional 边界内运行 —— handler 加入此事务,listener 不会。
调用模式:execute(commandCode, request) 在 commit 之后同步返回 CommandExecuteResult。
对于 chat 工具,不要 在 agent 工具实现中直接调用 CommandExecutor —— 工具框架会用 approval gate 与审计 step 进行包装。参见 ConversationTurnService。
接口签名
package com.auraboot.framework.meta.service;
import com.auraboot.framework.meta.dto.CommandExecuteRequest;
import com.auraboot.framework.meta.dto.CommandExecuteResult;
public interface CommandExecutor {
/** 在当前(或新)事务内同步执行命令。 */
CommandExecuteResult execute(String commandCode, CommandExecuteRequest request);
}CommandExecuteResult 暴露 getCommandCode()、getPhaseReached()、getData()(例如已插入记录)、getExecutionTimeMs() 与 isIdempotentReplay()(命中幂等去重时为 true)。CommandExecuteRequest 提供:
payload—— 命令的输入数据(Map<String, Object>)。clientRequestId—— executor 据此做幂等去重(键表达式commandCode + ':' + clientRequestId,TTL 24 小时)。operationType—— 可选的操作类型提示(CREATE/UPDATE/DELETE)。targetRecordId—— 可选目标记录 id(用于UPDATE/DELETE)。auditContext—— 可选的审计来源元数据(不会合并进 handler 看到的业务 payload)。expectedVersion—— 可选乐观锁版本号。dryRun—— 为true时跑完整 pipeline 但强制回滚事务,产生无副作用的写入模拟。
实现示例
一个迁移脚本,按外部 id 去重导入旧版 CRM lead:
package com.acme.migration;
import com.auraboot.framework.meta.dto.CommandExecuteRequest;
import com.auraboot.framework.meta.dto.CommandExecuteResult;
import com.auraboot.framework.meta.service.CommandExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class LegacyLeadImporter {
@Autowired private CommandExecutor commandExecutor;
public void importBatch(Iterable<LegacyLead> batch) {
for (LegacyLead row : batch) {
CommandExecuteRequest req = new CommandExecuteRequest();
req.setPayload(Map.of(
"lead_name", row.name(),
"email", row.email(),
"source", "legacy_import",
"external_id", row.externalId()
));
req.setClientRequestId("legacy:" + row.externalId());
CommandExecuteResult result =
commandExecutor.execute("crm:create_lead", req);
if (result.isIdempotentReplay()) {
// already imported in a prior run — skip
continue;
}
}
}
}在 listener 内的写法相同,唯一需注意:listener 在原始 commit 之 后 运行 —— 每次 execute 调用都会开启新事务:
@Override
public void onEvent(EventContext e) {
CommandExecuteRequest req = new CommandExecuteRequest();
req.setTargetRecordId(e.recordId());
req.setPayload(Map.of("assigned_to", router.next(e.tenantId())));
commandExecutor.execute("crm:update_lead", req);
}注册
宿主托管的单例。任意位置 @Autowired。无 SPI —— 扩展通过 pipeline 上的 CommandHandlerExtension / ValidatorExtension 完成,而不是替换 executor。
事务边界速查
| 调用者 | 事务 | 备注 |
|---|---|---|
CommandHandlerExtension.execute(在 HandlerPhase 运行) | 加入 | throw 会回滚整个 turn。 |
EventListenerExtension.onEvent | 全新 | listener 在 commit 后运行。 |
| 定时任务 | 每次调用全新 | 每次 execute 都是独立事务。 |
| Agent 工具(经 ConversationTurnService) | 被包装 | 工具框架持有边界。 |
常见陷阱
- 在 handler 内重入
execute。 重新进入同一条 pipeline —— 轻则做无用功,重则死锁。请把逻辑重构进同一个 command,或经事件/listener 解耦。 - 在易重试路径中省略
clientRequestId。 listener 是 at-least-once;没有它会产生重复插入。 - catch 住
execute抛出的异常。 这里的catch (Exception e)与红线 §8 反模式一致 —— 你会掩盖rollback-only并产生鬼影写入。 - 在面向用户的流程中误用
dryRun = true。 dryRun 会强制回滚事务、产生无副作用模拟;不要在真正需要落库的用户流程里打开。
相关
- Commands pipeline
- CommandHandlerExtension
- ConversationTurnService —— chat-tool 对它的封装
- REST API —— HTTP 等价入口