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的缓存机制局限性共同导致:
调试模式的特殊行为
调试模式下,Spring MVC会为ResponseBodyEmitter的每一次send()操作触发异步调度(async dispatch),这是调试环境为了便于跟踪每一块响应输出而保留的逻辑。由于你的shouldNotFilterAsyncDispatch()返回false,每一次异步调度都会走完整的Filter链,所以每次send()都会触发检查。正常模式的性能优化
生产正常模式下,Spring MVC会对异步响应做性能优化:只有第一次请求进入时会执行完整的Filter链,后续的send()操作直接通过异步上下文输出响应内容,不会再触发Filter的重新执行。这种设计是为了避免重复执行Filter逻辑,提升接口性能。响应缓存Wrapper的局限性
你使用的ContentCachingResponseWrapper仅会缓存第一次请求的响应内容,后续分块发送的foo2、foo3不会被缓存到getContentInputStream()中。即便正常模式下Filter能多次触发,也无法获取到后续分块的内容,更不用说实际只执行一次Filter,自然会漏掉后续对象的检查。
简言之,调试模式为了调试便利性保留了每一次异步调度的Filter触发,而生产模式为性能优化跳过了后续调度的Filter执行,同时缓存Wrapper无法捕获分块发送的全部内容,最终导致部分检查被绕过。
内容的提问来源于stack exchange,提问作者Katy

