如何无需额外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
相关产品推荐
相关产品推荐

