Spring MVC 集成Servlet 3.0 AsyncContext异步请求异常分析
在调试Spring MVC 集成 Servlet 3.0的 AsyncContext 做异步处理时,碰到一个平时未注意到和较少会触发的怪异问题,结合调试结果和自己的猜想,对比研究了下源码来解释此问题的根本原因。
前置
首先是 Controller 方法没入参 HttpServletResponse,只有 HttpServletRequest。
通过 HttpServletRequest 开启异步模式拿到 AsyncContext 后,在异步线程内通过 AsyncContext 取出 ServletResponse 用于写出响应数据。代码如下:
1 |
|
问题
服务启动后第一次请求总是正常。结果如下:
1
2
3
4接收请求线程开始:http-nio-80-exec-3
接收请求线程结束
异步线程启动:Thread-14
异步线程结束第二次请求,Postman会一直等待,直到后端服务报错。如下:
1
2
3
4
5Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.IllegalStateException: getWriter() has already been called for this response
Exception Processing ErrorPage[errorCode=0, location=/error]在第二次请求抛出异常后,短时间内执行第三次请求,能正常执行业务逻辑并响应。
同时会抛和步骤二的异常,还多了些其它异常。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14Cannot call sendError() after the response has been committed
Error reading request, ignored
Calling [asyncPostProcess()] is not valid for a request with Async state [MUST_ERROR]
Encountered a non-recycled request and recycled it forcedly.
#或者报错
接收请求线程开始:http-nio-80-exec-6
接收请求线程结束
异步线程启动:Thread-41
Exception in thread "Thread-41" java.lang.IllegalStateException: getOutputStream() has already been called for this response
猜想
分析抛出的异常信息,getWriter() 方法在关闭后又被调用了,在异步线程 Response 完成后调用了 close() 关闭输出流,Controller 的 testAsync 方法是 void 类型,不需要返回。getWriter() 关闭仍被二次调用就显的很奇怪。
猜测:getWriter()关闭后仍被二次调用是 Spring MVC内部逻辑导致。
Spring MVC 默认是 ModelAndView 模式,没有加 @RestController 和 @ResponseBody 注解,即使方法是 void 类型,Spring MVC 的 Handler 仍会走视图解析器(viewResolver)的逻辑,就会调输出流写出数据,只是返回 null。
猜测:如果 Controller 方法是 void 类型,且入参有 HttpServletResponse,Spring MVC 是不是就认为响应由客户自己处理,Spring MVC 的处理器 Handler 就不执行,就没有输出流的逻辑,是否就解决了问题。
验证
Controller 类使用 @RestController 注解或方法使用 @ResponseBody 注解,方法入参没有 HttpServletResponse。
异步线程写出数据后,使用 writer.close() 来关闭流。
服务启动后第一次请求正常,再次请求报错,说明 writer.close() 关闭流无效。错误如下。
1
java.lang.IllegalStateException: getWriter() has already been called for this response
Controller 类不使用 @RestController 注解和方法不使用 @ResponseBody 注解,方法增加入参 HttpServletResponse。
异步线程写出数据后,使用 writer.close() 来关闭流。
服务启动后第一次请求正常,再次请求报错,说明 writer.close() 关闭流无效。错误如下。
1
java.lang.IllegalStateException: getWriter() has already been called for this response
Controller 类使用 @RestController 注解或方法使用 @ResponseBody 注解,方法入参没有 HttpServletResponse。
异步线程写出数据后,使用 asyncContext.complete() 关闭流。
所有请求正常,正常响应数据,无任何报错。
Controller 类不使用 @RestController 注解,方法不使用 @ResponseBody 注解,方法入参增加 HttpServletResponse。
异步线程写出数据后,使用 asyncContext.complete() 关闭流。
所有请求正常,正常响应数据,无任何报错。
结论
异步线程中使用 writer.close() 来关闭流,无效,必须使用 asyncContext.complete() 关闭流。
Controller 类使用 @RestController 注解,或方法使用 @ResponseBody 注解。
Spring MVC 处理器 Handler 会走到消息转换器,就直接写输出响应数据。就不会走视图解析这个步骤。
Controller 方法是 void 类型,不使用@RestController 注解和 @ResponseBody 注解,入参有 HttpServletResponse,Spring MVC 会认为视图由 Handler 自己处理,不会走视图解析器这个步骤。
Controller 类使用 @RestController 注解,或方法使用 @ResponseBody 注解。方法入参有 HttpServletResponse 有写出数据,有返回值。
会报错,HttpServletResponse 写出数据会自动关闭输出流,有返回值,Spirng MVC 再返回再次获取 getWriter()就会报错。
1
java.lang.IllegalStateException: getWriter() has already been called for this response
Controller 方法是入参有 HttpServletResponse 可以和返回 ModelAndView 一起使用。
正确方式
(@RestController 或 @ResponseBody) + void 方法 + 异步线程 asyncContext.complete() 关闭流。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AsyncServlet {
/**
* @param servletRequest 请求
*/
public void testAsync(HttpServletRequest servletRequest) {
System.out.println("接收请求线程开始:" + Thread.currentThread().getName());
AsyncContext asyncContext = servletRequest.startAsync();
new Thread(() -> {
System.out.println("异步线程启动:" + Thread.currentThread().getName());
PrintWriter writer = null;
try {
ServletResponse response = asyncContext.getResponse();
response.setContentType("application/json;charset=UTF-8");
writer = response.getWriter();
HashMap<String, Object> map = new HashMap<>();
map.put("id", 100L);
map.put("body", "Hello World");
writer.println(JSON.toJsonString(map));
asyncContext.complete();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("异步线程结束");
}).start();
System.out.println("接收请求线程结束");
}void 方法 + HttpServletResponse 入参 + 异步线程 asyncContext.complete() 关闭流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class AsyncServlet {
/**
* @param servletRequest 请求
*/
public void testAsync(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
System.out.println("接收请求线程开始:" + Thread.currentThread().getName());
AsyncContext asyncContext = servletRequest.startAsync();
new Thread(() -> {
System.out.println("异步线程启动:" + Thread.currentThread().getName());
PrintWriter writer = null;
try {
ServletResponse response = asyncContext.getResponse();
response.setContentType("application/json;charset=UTF-8");
writer = response.getWriter();
HashMap<String, Object> map = new HashMap<>();
map.put("id", 100L);
map.put("body", "Hello World");
writer.println(JSON.toJsonString(map));
asyncContext.complete();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("异步线程结束");
}).start();
System.out.println("接收请求线程结束");
}
源码分析
Spring MVC 集成Servlet 3.0 AsyncContext异步请求异常分析
http://blog.gxitsky.com/2022/07/30/SpringMVC-39-Servlet3-AsyncContext-Exception/