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

Spring中返回Json流的REST端点为何会绕过自定义Filter?

问题描述

我有一个返回ResponseBodyEmitter的REST端点:

@GetMapping("/foo/stream")
public ResponseEntity<ResponseBodyEmitter> getFoos() {
    ResponseBodyEmitter rbe = new ResponseBodyEmitter();
    executor.execute(() -> {
        try {
                rbe.send(foo1);
                Thread.sleep(2000);
                rbe.send(foo2);
                Thread.sleep(2000);
                rbe.send(foo3);
                Thread.sleep(2000);
                rbe.complete();
            } catch (Exception ex) {
            rbe.completeWithError(ex);
        }
    });
    return ResponseEntity.ok(rbe);
}

我创建了一个自定义Filter(FooValidationFilter),用于在调用该端点时检查foo对象:

@Slf4j
@AllArgsConstructor
public class FooValidationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        ContentCachingRequestWrapper requestToCache = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseToUse = new ContentCachingResponseWrapper(response);

        filterChain.doFilter(request, responseToUse);

        if (!responseToUse.isCommitted() &&
                responseToUse.getStatus() >= 200 && responseToUse.getStatus() < 300 &&
                HttpMethod.GET.matches(request.getMethod())) {

            Scanner scanner = new Scanner(responseToUse.getContentInputStream());
            String fooField;
            do {
                fooField = scanner.findWithinHorizon(REGEX, 0); 
                // 执行一些检查操作
            } while (fooField != null);
        }

        if (requestToCache.isAsyncStarted()) {
            requestToCache.getAsyncContext().addListener(new AsyncListener() {
                public void onComplete(AsyncEvent asyncEvent) throws IOException {
                    responseToUse.copyBodyToResponse();
                }

                public void onTimeout(AsyncEvent asyncEvent) throws IOException {
                }

                public void onError(AsyncEvent asyncEvent) throws IOException {
                }

                public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
                }
            });
        } else {
            responseToUse.copyBodyToResponse();
        }
    }

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }
}

注意事项:

  • 调试模式下,每次调用ResponseBodyEmitter.send()后都会触发FooValidationFilter。
  • 正常模式下,FooValidationFilter仅被调用一次,导致部分Foo对象的检查被绕过。

请问是什么原因导致了这个问题?


原因分析

核心原因在于调试模式与正常模式下Spring MVC对异步响应分块发送的处理逻辑差异,再结合Filter的缓存机制局限性共同导致:

  1. 调试模式的特殊行为
    调试模式下,Spring MVC会为ResponseBodyEmitter的每一次send()操作触发异步调度(async dispatch),这是调试环境为了便于跟踪每一块响应输出而保留的逻辑。由于你的shouldNotFilterAsyncDispatch()返回false,每一次异步调度都会走完整的Filter链,所以每次send()都会触发检查。

  2. 正常模式的性能优化
    生产正常模式下,Spring MVC会对异步响应做性能优化:只有第一次请求进入时会执行完整的Filter链,后续的send()操作直接通过异步上下文输出响应内容,不会再触发Filter的重新执行。这种设计是为了避免重复执行Filter逻辑,提升接口性能。

  3. 响应缓存Wrapper的局限性
    你使用的ContentCachingResponseWrapper仅会缓存第一次请求的响应内容,后续分块发送的foo2、foo3不会被缓存到getContentInputStream()中。即便正常模式下Filter能多次触发,也无法获取到后续分块的内容,更不用说实际只执行一次Filter,自然会漏掉后续对象的检查。

简言之,调试模式为了调试便利性保留了每一次异步调度的Filter触发,而生产模式为性能优化跳过了后续调度的Filter执行,同时缓存Wrapper无法捕获分块发送的全部内容,最终导致部分检查被绕过。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 17:36:30