Spring端点长轮询场景下如何检测客户端断开连接?
检测Spring长轮询中客户端断开连接的方案
1. 同步端点无法感知断开的原因
你用的同步@RestController端点(带长时间循环),客户端断开时Servlet容器不会主动中断正在执行的线程——只有当线程尝试向响应流写入数据时,才会抛出容器特定的断开异常(比如Tomcat的ClientAbortException)。如果你的循环只是纯计算、没有写响应操作,就不会触发这个异常,自然感知不到客户端断开。
2. 异步组件的正确使用姿势(DeferredResult)
你之前尝试的DeferredResult没生效,大概率是没注册断开回调或在异步任务中做状态检查。示例代码如下:
import org.apache.catalina.connector.ClientAbortException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import java.util.concurrent.CompletableFuture; @RestController public class LongPollController { private static final Logger log = LoggerFactory.getLogger(LongPollController.class); @GetMapping("/long-poll") public DeferredResult<ResponseEntity<String>> longPoll() { // 设置永不超时,由任务自行控制 DeferredResult<ResponseEntity<String>> deferredResult = new DeferredResult<>(Long.MAX_VALUE); // 注册客户端断开/错误回调 deferredResult.onError(throwable -> { if (throwable instanceof ClientAbortException) { log.warn("客户端已断开连接,终止长轮询任务", throwable); } else { log.error("长轮询任务执行出错", throwable); } }); // 异步执行长时间任务 CompletableFuture.runAsync(() -> { try { for (int i = 0; i < 100; i++) { // 定期检查任务是否已被标记为完成(客户端断开会触发此状态) if (deferredResult.isSetOrExpired()) { log.info("检测到客户端断开,提前终止循环"); return; } Thread.sleep(1000); // 模拟耗时操作 log.info("执行长轮询步骤 {}", i); } // 任务正常完成,返回结果 deferredResult.setResult(ResponseEntity.ok("长轮询任务完成")); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("长轮询任务被中断", e); deferredResult.setErrorResult(ResponseEntity.status(500).body("任务中断")); } }); return deferredResult; } }
核心要点:
- 异步任务中定期调用
deferredResult.isSetOrExpired(),判断客户端是否已断开,及时终止任务 - 通过
onError回调捕获容器抛出的客户端断开异常,统一日志
3. ResponseBodyEmitter/SseEmitter的实现方案
如果是流式响应场景,send方法在客户端断开时会直接抛出IO异常,捕获即可终止任务:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; import java.io.IOException; import java.util.concurrent.CompletableFuture; @RestController public class StreamController { private static final Logger log = LoggerFactory.getLogger(StreamController.class); @GetMapping("/stream-task") public ResponseBodyEmitter streamTask() { ResponseBodyEmitter emitter = new ResponseBodyEmitter(Long.MAX_VALUE); CompletableFuture.runAsync(() -> { try { for (int i = 0; i < 100; i++) { // 尝试发送数据,客户端断开时会抛出IOException emitter.send("流式任务执行步骤:" + i); Thread.sleep(1000); } emitter.complete(); } catch (IOException e) { log.warn("客户端断开连接,终止流式任务", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("流式任务被中断", e); emitter.completeWithError(e); } }); return emitter; } }
4. 同步端点的补救方案(不推荐)
如果必须使用同步端点,只能在循环中尝试刷新响应流,触发断开异常:
import org.apache.catalina.connector.ClientAbortException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @RestController public class SyncLongTaskController { private static final Logger log = LoggerFactory.getLogger(SyncLongTaskController.class); @GetMapping("/sync-long-task") public ResponseEntity<String> syncLongTask(HttpServletResponse response) { try { for (int i = 0; i < 100; i++) { // 刷新响应流,客户端断开时会抛出异常 response.flushBuffer(); Thread.sleep(1000); log.info("执行同步任务步骤 {}", i); } return ResponseEntity.ok("同步任务完成"); } catch (ClientAbortException e) { log.warn("客户端断开连接,终止同步任务", e); return ResponseEntity.status(502).build(); } catch (IOException | InterruptedException e) { log.error("同步任务执行出错", e); return ResponseEntity.status(500).body("任务失败"); } } }
注意:该方案依赖Servlet容器实现,频繁刷新会影响性能,仅作为临时补救方案。
5. 全局异常统一处理
可以用@ControllerAdvice全局捕获客户端断开异常,避免每个端点重复处理:
import org.apache.catalina.connector.ClientAbortException; import org.eclipse.jetty.io.EofException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice public class GlobalDisconnectHandler { private static final Logger log = LoggerFactory.getLogger(GlobalDisconnectHandler.class); // 兼容Tomcat、Jetty等不同容器的断开异常 @ExceptionHandler({ClientAbortException.class, EofException.class}) public void handleClientDisconnect(Exception e) { log.warn("客户端在请求处理过程中断开连接", e); // 无需返回响应,客户端已断开连接 } }
内容的提问来源于stack exchange,提问作者honeysuckle1234
相关产品推荐
相关产品推荐

