Spring Boot异步请求响应体为空问题排查求助
我们有一个返回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

