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

Spring Boot异步请求响应体为空问题排查求助

问题:Spring Boot异步接口响应体为空(Content-Length=0)

我们有一个返回WebAsyncTask的Rest Controller,代码如下:

@RequestMapping(value="/test", produces="text/plain", method=RequestMethod.POST)
@ResponseBody
public WebAsyncTask<ResponseEntity<String>> test(@RequestParam(value="foo", required=false) String data) throws IOException {
  Callable<ResponseEntity<String>> callable=() -> {
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity("testping",responseHeaders,HttpStatus.OK);
  };

  WebAsyncTask<ResponseEntity<String>> webAsyncTask = new WebAsyncTask<>(getTimeOut(), getAsyncTaskExecutor(), callable);
  return webAsyncTask;
}

AsyncTaskExecutor的XML配置:

<bean id="asyncTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  <property name="corePoolSize" value="50"/>
  <property name="maxPoolSize" value="50"/>
  <property name="queueCapacity" value="5000"/>
</bean>

Boot应用主类代码:

@SpringBootApplication(exclude={JacksonAutoConfiguration.class,
CodecsAutoConfiguration.class,HttpMessageConvertersAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,WebMvcAutoConfiguration.class})
@ImportResource(locations={"classpath:META-INF/spring/test-boot-applicationContext.xml"})
@EnableWebSecurity
@EnableAsync
public class TestBootApplication extends SpringBootServletInitializer {
  public static void main(String[] args) {
    SpringApplication.run(TestBootApplication.class, args);
  }

  @Configuration
  public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
      .authorizeRequests().antMatchers("/**").permitAll()
      .and().csrf().disable();
    }
  }

  @Bean
  public ServletRegistrationBean dispatcherController() {
    DispatcherServlet dispatcherServlet = new DispatcherServlet();
    dispatcherServlet.setNamespace("tempspace");
    XmlWebApplicationContext applicationContext = new XmlWebApplicationContext();
    applicationContext.setConfigLocations("classpath:META-INF/spring/test-dispatcher-servlet.xml");
    dispatcherServlet.setApplicationContext(applicationContext);
    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet,"/temp/*");
    servletRegistrationBean.setName("testdispatcher");
    servletRegistrationBean.setLoadOnStartup(20);
    return servletRegistrationBean;
  }

  @Bean
  public ObjectMapper defaultObjectMapper() {
    return new ObjectMapper();
  }
}

使用Postman发送POST请求时,最初得到200 OK但只有确认响应,无实际内容。排查后知道需要给Filter添加DispatcherType.ASYNC,Filter代码如下:

@Component
public class AsyncFilter implements Filter {
  private static final Logger LOG = LoggerFactory.getLogger(AsyncFilter.class);

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
    try {
      filterChain.doFilter(requestWrapper,responseWrapper);
    } finally {
      LOG.trace("Before - ResponseWrapper: {}", 
      getResponsePayload(responseWrapper));
      LOG.trace("Before - Response: {}", 
      getResponsePayload((HttpServletResponse) response));

      byte[] responseArray = responseWrapper.getContentAsByteArray();
      String responseString = new String(responseArray, 
      responseWrapper.getCharacterEncoding());
      LOG.trace("####### responseString = {}", responseString);

      responseWrapper.copyBodyToResponse();

      LOG.trace("After - ResponseWrapper: {}", 
      getResponsePayload(responseWrapper));
      LOG.trace("After - Response: {}", 
      getResponsePayload((HttpServletResponse) response));
    }
  }
}

在TestBootApplication中注册Filter的方式:

@Bean
public FilterRegistrationBean<AsyncFilter> asyncFilter() {
 FilterRegistrationBean<AsyncFilter> registrationBean = new FilterRegistrationBean<>();
 registrationBean.setFilter(new AsyncFilter());
 registrationBean.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
 registrationBean.addUrlPatterns("/*");
 return registrationBean;
}

添加后能获取到响应的Content-Type,但响应体为空,Content-Length为0:

KEY. VALUE
X-Frame-Options DENY
Content-Security-Policy frame-ancestors 'none'
X-Content-Type-Options nosniff
X-XSS-Protection 1; mode=block
Content-Type text/plain
Content-Length 0
Date Thu, 29 Dec 2022 09:33:38 GMT
Keep-Alive timeout=60
Connection keep-alive

日志片段显示:

16:57:11.099 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - Before - ResponseWrapper: testping
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - Before - Response: [unknown]
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - ####### responseString = testping
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - After - ResponseWrapper: [unknown]
16:57:11.100 [http-nio-8081-exec-5] TRACE c.b.a.filters.AsyncFilter - After - Response: [unknown]

怀疑响应在控制器处理完成前就被分发,使用Spring Boot 2.6.12,请问遗漏了什么?


解决方案

问题核心在于异步请求生命周期中ContentCachingResponseWrapper的使用逻辑错误,以及DispatcherServlet的异步支持配置缺失:

1. 异步请求中Filter的执行逻辑问题

异步请求会触发Filter两次执行:

  • 初始请求阶段:调用filterChain.doFilter()后立即进入finally块,此时异步任务还在后台执行,响应内容未生成;
  • 异步任务完成阶段:再次执行Filter,此时响应内容已写入缓存,但你的代码在初始阶段就调用了copyBodyToResponse(),清空了缓存,导致第二次执行时无内容可写入实际响应。

修正Filter逻辑,仅在异步任务完成后或非异步请求时,才将缓存内容写入响应:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

    try {
        filterChain.doFilter(requestWrapper, responseWrapper);
    } finally {
        // 仅在异步请求完成,或非异步请求时处理响应缓存
        if (!httpRequest.isAsyncStarted() || httpRequest.isAsyncCompleted()) {
            LOG.trace("Before - ResponseWrapper: {}", getResponsePayload(responseWrapper));
            LOG.trace("Before - Response: {}", getResponsePayload((HttpServletResponse) response));

            byte[] responseArray = responseWrapper.getContentAsByteArray();
            String responseString = new String(responseArray, responseWrapper.getCharacterEncoding());
            LOG.trace("####### responseString = {}", responseString);

            responseWrapper.copyBodyToResponse();

            LOG.trace("After - ResponseWrapper: {}", getResponsePayload(responseWrapper));
            LOG.trace("After - Response: {}", getResponsePayload((HttpServletResponse) response));
        }
    }
}

2. 启用DispatcherServlet的异步支持

由于你手动注册了DispatcherServlet,需要显式开启异步支持,否则异步请求的响应无法正确传递:

// 在dispatcherController Bean中添加
dispatcherServlet.setAsyncSupported(true);

或者在对应的XML配置中添加:

<servlet>
    <servlet-name>testdispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <async-supported>true</async-supported>
</servlet>

3. 额外检查点

  • 确认WebAsyncTask的超时时间设置合理,避免异步任务未完成就触发超时;
  • 验证ThreadPoolTaskExecutor是否正确注入到Controller中,确保异步任务能被执行;
  • 检查getResponsePayload方法实现,确保它能正确读取ContentCachingResponseWrapper的缓存内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 20:35:28