Java 实现多步骤异步任务编排

背景

在仓储物流、产线自动化等场景中,我们经常需要通过 Java 后端协调 AGV 小车完成一系列连续动作:先通过 HTTP 接口下发移动任务,等待小车到达指定位置后,再通过 OPC UA 协议控制机械臂完成夹取,最后再下发移动任务到目的地。

这类场景的核心难点在于:HTTP 任务下发是异步的,OPC UA 指令执行也是异步的,而多个步骤之间存在严格的先后依赖关系。本文将介绍三种主流的实现方案,从轻量级到重量级依次展开。


问题拆解

一个典型的 AGV 搬运流程如下:

  1. 通过 HTTP 接口向 RCS(机器人调度系统)下发移动任务,目标为取货点
  2. 监听任务状态,等待小车到达取货点
  3. 到达后,通过 OPC UA 协议向机械臂下发夹取指令
  4. 监听夹取指令执行结果,等待夹取完成
  5. 夹取完成后,再次通过 HTTP 接口下发移动任务,目标为目的地
  6. 监听任务状态,等待小车到达目的地,流程结束

可以看到,整个流程是一个典型的多步骤异步流程编排问题,每一步都需要等待上一步完成后再触发。


方案一:CompletableFuture 链式编排(推荐,轻量级)

Java 8 引入的 CompletableFuture 天然适合这种"步骤 A 完成 → 触发步骤 B → 触发步骤 C"的场景。通过 thenCompose 方法可以优雅地串联有依赖关系的异步操作,避免嵌套回调(即"回调地狱")。

typescript 复制代码
@Service
public class AgvTaskOrchestrator {

    private static final Logger log = LoggerFactory.getLogger(AgvTaskOrchestrator.class);

    @Autowired
    private AgvHttpService agvHttpService;
    @Autowired
    private OpcUaService opcUaService;
    @Autowired
    private TaskStatusListener statusListener;

    public CompletableFuture<Void> executePickAndMove(String agvId, String targetStation) {

        // 步骤1:下发HTTP任务,前往取货点,等待到达
        CompletableFuture<String> goToPoint1 = agvHttpService.sendMoveTask(agvId, "PICK_POINT")
                .thenCompose(taskId -> statusListener.waitForComplete(taskId));

        // 步骤2:到达后,通过OPC UA下发夹取指令,等待夹取完成
        CompletableFuture<Void> grabAction = goToPoint1
                .thenCompose(arrivedTaskId -> opcUaService.sendGripperCommand(agvId, "GRAB"))
                .thenCompose(cmdId -> opcUaService.waitForCommandComplete(cmdId));

        // 步骤3:夹取完成后,下发移动任务到目的地,等待到达
        CompletableFuture<String> goToTarget = grabAction
                .thenCompose(v -> agvHttpService.sendMoveTask(agvId, targetStation))
                .thenCompose(taskId -> statusListener.waitForComplete(taskId));

        return goToTarget.thenAccept(v -> log.info("AGV[{}] 全流程执行完成", agvId));
    }
}

关键点thenCompose 用于串联有依赖关系的异步操作,它会将前一步的返回结果传递给下一步,同时避免 CompletableFuture<CompletableFuture<...>> 的嵌套问题。

适用场景:步骤较少(3~5 步)、逻辑简单、没有复杂分支的场景。


方案二:状态机模式(适合复杂流程)

当流程步骤增多、出现分支逻辑(如夹取失败要重试、异常要回退)时,CompletableFuture 链式编排会变得难以维护。此时推荐使用状态机模式来管理流程状态。

typescript 复制代码
public enum AgvFlowState {
    IDLE,                // 空闲
    MOVING_TO_PICK,      // 前往取货点
    GRABBING,            // 夹取中
    MOVING_TO_TARGET,    // 前往目的地
    COMPLETED,           // 完成
    FAILED               // 失败
}

@Service
public class AgvStateMachine {

    private static final Logger log = LoggerFactory.getLogger(AgvStateMachine.class);

    private final Map<String, AgvFlowState> taskStates = new ConcurrentHashMap<>();

    @Autowired
    private AgvHttpService agvHttpService;
    @Autowired
    private OpcUaService opcUaService;

    /**
     * 收到HTTP任务完成回调时调用
     */
    public void onTaskArrived(String taskId) {
        AgvFlowState currentState = taskStates.get(taskId);
        if (currentState == null) return;

        switch (currentState) {
            case MOVING_TO_PICK:
                log.info("AGV已到达取货点,下发夹取指令, taskId={}", taskId);
                taskStates.put(taskId, AgvFlowState.GRABBING);
                opcUaService.sendGripperCommand(taskId, "GRAB");
                break;
            case MOVING_TO_TARGET:
                log.info("AGV已到达目的地,流程结束, taskId={}", taskId);
                taskStates.put(taskId, AgvFlowState.COMPLETED);
                break;
            default:
                log.warn("收到意外的任务完成回调, taskId={}, state={}", taskId, currentState);
        }
    }

    /**
     * 收到OPC UA指令完成回调时调用
     */
    public void onGripperComplete(String taskId) {
        AgvFlowState currentState = taskStates.get(taskId);
        if (currentState == AgvFlowState.GRABBING) {
            log.info("夹取完成,下发移动任务到目的地, taskId={}", taskId);
            taskStates.put(taskId, AgvFlowState.MOVING_TO_TARGET);
            agvHttpService.sendMoveTask(taskId, "TARGET_STATION");
        }
    }

    /**
     * 启动一个新的搬运流程
     */
    public void startFlow(String taskId) {
        taskStates.put(taskId, AgvFlowState.MOVING_TO_PICK);
        agvHttpService.sendMoveTask(taskId, "PICK_POINT");
    }
}

优势:每个步骤的状态转换清晰可控,方便加日志、异常处理和断点恢复。当流程变复杂时,还可以引入 Spring StateMachine 框架来进一步简化状态定义和转换规则。

适用场景:步骤较多、有分支/重试/回退逻辑的复杂场景。


方案三:回调 + 事件驱动(Webhook 模式)

如果你的 RCS 系统支持 Webhook 回调(如海康 RCS-2000 等主流系统),可以用事件驱动的方式来实现,让 RCS 在任务状态变更时主动推送通知。

less 复制代码
@RestController
@RequestMapping("/agv")
public class AgvCallbackController {

    private static final Logger log = LoggerFactory.getLogger(AgvCallbackController.class);

    @Autowired
    private AgvTaskOrchestrator orchestrator;

    /**
     * RCS回调接口:任务状态变更时主动推送
     */
    @PostMapping("/callback")
    public ResponseEntity<?> onAgvCallback(@RequestBody AgvCallbackDTO callback) {
        log.info("收到AGV回调, taskId={}, status={}", callback.getTaskId(), callback.getStatus());

        switch (callback.getStatus()) {
            case "ARRIVED":
                orchestrator.onArrived(callback.getTaskId());
                break;
            case "COMPLETED":
                orchestrator.onMoveComplete(callback.getTaskId());
                break;
            case "FAILED":
                orchestrator.onFailed(callback.getTaskId(), callback.getErrorMessage());
                break;
            default:
                log.warn("未知的回调状态: {}", callback.getStatus());
        }
        return ResponseEntity.ok().build();
    }
}

适用场景:RCS 系统支持回调/Webhook 推送的场景,实时性最好,无需轮询。


关于"监听任务完成"的两种实现方式

在上述方案中,"监听任务完成"是一个关键环节,通常有两种实现方式:

方式 实现思路 适用场景
轮询 定时调用查询接口检查任务状态 RCS 不支持回调时
回调/Webhook 暴露 HTTP 接口,RCS 主动推送状态变更 RCS 支持回调时(推荐)

轮询方式的实现示例:

php 复制代码
public CompletableFuture<String> waitForComplete(String taskId) {
    return CompletableFuture.supplyAsync(() -> {
        int maxRetries = 300; // 最多等待5分钟
        int retryCount = 0;

        while (retryCount < maxRetries) {
            TaskStatus status = agvHttpService.queryTaskStatus(taskId);
            if (status == TaskStatus.COMPLETED) {
                return taskId;
            }
            if (status == TaskStatus.FAILED) {
                throw new RuntimeException("任务执行失败, taskId=" + taskId);
            }
            try {
                Thread.sleep(1000); // 每秒轮询一次
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("等待任务完成被中断", e);
            }
            retryCount++;
        }
        throw new RuntimeException("任务超时, taskId=" + taskId);
    });
}

注意:轮询方式一定要设置超时机制,避免无限等待导致线程泄漏。


方案对比与选型建议

维度 CompletableFuture 状态机 事件驱动
复杂度
可维护性 步骤多时较差
实时性 取决于监听方式 取决于监听方式 最好
异常处理 需手动处理 天然支持 需手动处理
适用场景 3~5步简单流程 多步骤复杂流程 实时性要求高

实际选型建议

  • 步骤少(3~5 步)、逻辑简单 :用 CompletableFuture 链式编排即可,代码简洁直观
  • 步骤多、有分支/重试/回退逻辑:用状态机模式,或引入 Spring StateMachine 框架
  • 对实时性要求高:优先使用回调/Webhook 模式,避免轮询带来的延迟
  • 对可靠性要求高:无论哪种方案,都要加上超时机制、失败重试、任务持久化(防止服务重启丢失流程状态)

工业现场的注意事项

在实际工业项目中,正常流程往往不是最难的,异常处理才是重中之重:

  • 超时处理:小车长时间未到达指定位置,要能自动告警并取消任务
  • 失败重试:夹取失败时要能自动重试(设置最大重试次数)
  • 断点恢复:服务重启后能恢复未完成的流程,而不是从头开始
  • 通信容错:网络断开后恢复时,要能继续未完成的流程
  • 日志追踪:每个步骤都要有完整的日志记录,方便排查问题

总结

Java 实现 AGV 多步骤异步任务编排,核心思路就是异步等待 + 状态驱动CompletableFuture 提供了轻量级的链式编排能力,状态机模式提供了复杂流程的可控性,而事件驱动则提供了最佳的实时性。根据实际业务复杂度选择合适的方案,同时务必做好异常处理和可靠性保障,这才是工业级应用的关键所在。


要不要我补充一个完整的项目结构示例?包括 pom.xml 依赖、配置文件和几个核心接口的 Mock 实现,可以直接跑起来看效果。

相关推荐
小强19881 小时前
告别 "if-else 地狱":PHP 中的策略模式、管道模式与责任链模式实战
后端
面试鸭1 小时前
Codex 跑进法务部,周活居然暴涨了 108 倍???
后端·面试·求职
Larcher2 小时前
从 SDD 到可交付:我如何用规范驱动 AI 做出一个 Chrome 翻译插件
前端·后端·架构
choumou_M2 小时前
SpringBoot_5:商品秒杀场景的并发实践(初步)
java·spring boot·后端
陈明勇3 小时前
Go 1.27 重磅发布:泛型方法落地、分配性能跃升,JSON v2 正式可用
后端
Zane19943 小时前
create_task 和直接 await 到底有什么不一样?asyncio 的 Task、gather 与并发数量控制
后端·python
喜欢吃豆3 小时前
5 分钟,让网页里站着一个会说话的 3D 数字人
后端
Python私教3 小时前
创业团队做管理系统,别先堆页面:我把 14 个问题做成了需求门禁
后端·python·架构
攻城有术3 小时前
专项攻克-SpringBoot启动后 Bean的实例数量问题
java·spring boot·后端