背景
在仓储物流、产线自动化等场景中,我们经常需要通过 Java 后端协调 AGV 小车完成一系列连续动作:先通过 HTTP 接口下发移动任务,等待小车到达指定位置后,再通过 OPC UA 协议控制机械臂完成夹取,最后再下发移动任务到目的地。
这类场景的核心难点在于:HTTP 任务下发是异步的,OPC UA 指令执行也是异步的,而多个步骤之间存在严格的先后依赖关系。本文将介绍三种主流的实现方案,从轻量级到重量级依次展开。
问题拆解
一个典型的 AGV 搬运流程如下:
- 通过 HTTP 接口向 RCS(机器人调度系统)下发移动任务,目标为取货点
- 监听任务状态,等待小车到达取货点
- 到达后,通过 OPC UA 协议向机械臂下发夹取指令
- 监听夹取指令执行结果,等待夹取完成
- 夹取完成后,再次通过 HTTP 接口下发移动任务,目标为目的地
- 监听任务状态,等待小车到达目的地,流程结束
可以看到,整个流程是一个典型的多步骤异步流程编排问题,每一步都需要等待上一步完成后再触发。
方案一: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 实现,可以直接跑起来看效果。