You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何无需额外Controller将Spring Boot异步任务数据推送给React前端

问题解答

这个需求完全可以实现。首先要明确:HTTP协议是请求-响应模型,同一个请求只能返回一次响应,你的原Controller返回ResponseEntity之后,本次HTTP请求的生命周期就已经结束,没办法复用原请求链路回传进度数据,所以我们用服务端主动推送的方案实现,全程不需要新增Controller类,符合你的要求。

推荐方案:SSE(Server-Sent Events,服务端发送事件)

SSE是HTTP原生支持的单向推送技术,实现轻量,刚好适配进度推送的场景,只需要在原有Controller类中新增一个接口方法即可,不算新增Controller类。
实现步骤如下:

  • 第一步:定义进度管理器,用来存储任务ID和SSE连接的映射关系,可以用ConcurrentHashMap实现简单版:
@Component
public class ProgressManager {
    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();

    public void addEmitter(String taskId, SseEmitter emitter) {
        emitterMap.put(taskId, emitter);
    }

    public SseEmitter getEmitter(String taskId) {
        return emitterMap.get(taskId);
    }

    public void removeEmitter(String taskId) {
        emitterMap.remove(taskId);
    }
}
  • 第二步:在你现有的Controller类中新增SSE接口:
@RestController
@RequestMapping("/your-existing-controller-path")
public class YourExistingController {
    @Autowired
    private ProgressManager progressManager;

    // 你原有的触发异步任务的接口,改造成返回taskId
    @PostMapping("/trigger-task")
    public ResponseEntity<String> triggerTask() {
        // 生成唯一任务ID
        String taskId = UUID.randomUUID().toString();
        // 把taskId传给异步任务
        CompletableFuture.runAsync(yourAsyncTask(taskId));
        // 返回taskId给前端
        return ResponseEntity.ok(taskId);
    }

    // 新增的SSE接口,在原有Controller类内,不算新增Controller
    @GetMapping(value = "/progress/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter getProgress(@PathVariable String taskId) {
        // 设置超时时间,根据你的任务最长耗时调整
        SseEmitter emitter = new SseEmitter(5 * 60 * 1000L);
        progressManager.addEmitter(taskId, emitter);
        // 连接结束/超时后清理资源
        emitter.onCompletion(() -> progressManager.removeEmitter(taskId));
        emitter.onTimeout(() -> progressManager.removeEmitter(taskId));
        emitter.onError(e -> progressManager.removeEmitter(taskId));
        return emitter;
    }

    // 你的异步任务逻辑,改造为接收taskId参数
    private Runnable yourAsyncTask(String taskId) {
        return () -> {
            int count = 0;
            // 你的业务逻辑循环
            while (/* 任务未完成 */) {
                if(test.getNumberComplete() > count){
                    count = test.getNumberComplete();
                    String progress = String.format("%d/%d complete so far", count, test.getNumberComplete());
                    logger.info(progress);
                    // 推送进度
                    SseEmitter emitter = progressManager.getEmitter(taskId);
                    if (emitter != null) {
                        try {
                            emitter.send(SseEmitter.event().name("progress").data(progress));
                            // 任务完成关闭连接
                            if (count == test.getNumberComplete()) {
                                emitter.complete();
                            }
                        } catch (IOException e) {
                            emitter.completeWithError(e);
                        }
                    }
                }
            }
        };
    }
}
  • 第三步:React前端对接逻辑:
// 调用触发任务接口拿到taskId后,建立SSE连接
const listenProgress = (taskId) => {
  const eventSource = new EventSource(`/your-existing-controller-path/progress/${taskId}`);
  // 监听进度事件
  eventSource.addEventListener('progress', (event) => {
    // 把event.data的值渲染到页面顶部角落即可,比如存到state里
    console.log('进度更新:', event.data); // 输出类似 "1/3 complete so far"
  });
  // 连接错误或者任务完成后关闭连接
  eventSource.onerror = () => {
    eventSource.close();
  };
}

注意事项

如果完全不想在现有Controller里加任何方法,可以选择WebSocket方案,Spring Boot集成WebSocket后可以直接通过消息模板推送进度,不需要新增Controller类,不过配置成本比SSE高,没有特殊需求优先用SSE即可。
分布式部署场景下需要把SSE连接的映射关系存在 Redis 等公共缓存中,避免多实例部署时推送找不到对应连接。

内容的提问来源于stack exchange,提问作者mcool

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 08:54:00