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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 16:25:55