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 会强制回滚事务、产生无副作用模拟;不要在真正需要落库的用户流程里打开。

相关