Spring MVC 集成Servlet 3.0 AsyncContext异步请求异常分析

在调试Spring MVC 集成 Servlet 3.0的 AsyncContext 做异步处理时,碰到一个平时未注意到和较少会触发的怪异问题,结合调试结果和自己的猜想,对比研究了下源码来解释此问题的根本原因。

前置

首先是 Controller 方法没入参 HttpServletResponse,只有 HttpServletRequest。

通过 HttpServletRequest 开启异步模式拿到 AsyncContext 后,在异步线程内通过 AsyncContext 取出 ServletResponse 用于写出响应数据。代码如下:

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
@Controller
@RequestMapping("/asyncServlet")
public class AsyncServlet {

/**
* @param request 请求
*/
@GetMapping("/testAsync")
public void testAsync(HttpServletRequest request) {
System.out.println("接收请求线程开始:" + Thread.currentThread().getName());
AsyncContext asyncContext = request.startAsync();
new Thread(() -> {
System.out.println("异步线程启动:" + Thread.currentThread().getName());
try {
ServletResponse response = asyncContext.getResponse();
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.println("Who is your daddy");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("异步线程结束");
}).start();
System.out.println("接收请求线程结束");
}
}

问题

  1. 服务启动后第一次请求总是正常。结果如下:

    1
    2
    3
    4
    接收请求线程开始:http-nio-80-exec-3
    接收请求线程结束
    异步线程启动:Thread-14
    异步线程结束
  2. 第二次请求,Postman会一直等待,直到后端服务报错。如下:

    1
    2
    3
    4
    5
    Servlet.service() for servlet [dispatcherServlet] threw exception

    java.lang.IllegalStateException: getWriter() has already been called for this response

    Exception Processing ErrorPage[errorCode=0, location=/error]
  3. 在第二次请求抛出异常后,短时间内执行第三次请求,能正常执行业务逻辑并响应。

    同时会抛和步骤二的异常,还多了些其它异常。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Cannot 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

猜想

  1. 分析抛出的异常信息,getWriter() 方法在关闭后又被调用了,在异步线程 Response 完成后调用了 close() 关闭输出流,Controller 的 testAsync 方法是 void 类型,不需要返回。getWriter() 关闭仍被二次调用就显的很奇怪。

  2. 猜测:getWriter()关闭后仍被二次调用是 Spring MVC内部逻辑导致。

    Spring MVC 默认是 ModelAndView 模式,没有加 @RestController 和 @ResponseBody 注解,即使方法是 void 类型,Spring MVC 的 Handler 仍会走视图解析器(viewResolver)的逻辑,就会调输出流写出数据,只是返回 null。

    猜测:如果 Controller 方法是 void 类型,且入参有 HttpServletResponse,Spring MVC 是不是就认为响应由客户自己处理,Spring MVC 的处理器 Handler 就不执行,就没有输出流的逻辑,是否就解决了问题。

验证

  1. Controller 类使用 @RestController 注解或方法使用 @ResponseBody 注解,方法入参没有 HttpServletResponse。

    异步线程写出数据后,使用 writer.close() 来关闭流

    服务启动后第一次请求正常,再次请求报错,说明 writer.close() 关闭流无效。错误如下。

    1
    java.lang.IllegalStateException: getWriter() has already been called for this response
  2. Controller 类不使用 @RestController 注解和方法不使用 @ResponseBody 注解,方法增加入参 HttpServletResponse。

    异步线程写出数据后,使用 writer.close() 来关闭流

    服务启动后第一次请求正常,再次请求报错,说明 writer.close() 关闭流无效。错误如下。

    1
    java.lang.IllegalStateException: getWriter() has already been called for this response
  3. Controller 类使用 @RestController 注解或方法使用 @ResponseBody 注解,方法入参没有 HttpServletResponse。

    异步线程写出数据后,使用 asyncContext.complete() 关闭流

    所有请求正常,正常响应数据,无任何报错。

  4. Controller 类不使用 @RestController 注解,方法不使用 @ResponseBody 注解,方法入参增加 HttpServletResponse。

    异步线程写出数据后,使用 asyncContext.complete() 关闭流

    所有请求正常,正常响应数据,无任何报错。

结论

  1. 异步线程中使用 writer.close() 来关闭流,无效,必须使用 asyncContext.complete() 关闭流

  2. Controller 类使用 @RestController 注解,或方法使用 @ResponseBody 注解。

    Spring MVC 处理器 Handler 会走到消息转换器,就直接写输出响应数据。就不会走视图解析这个步骤。

  3. Controller 方法是 void 类型,不使用@RestController 注解和 @ResponseBody 注解,入参有 HttpServletResponse,Spring MVC 会认为视图由 Handler 自己处理,不会走视图解析器这个步骤。

  4. Controller 类使用 @RestController 注解,或方法使用 @ResponseBody 注解。方法入参有 HttpServletResponse 有写出数据,有返回值。

    会报错,HttpServletResponse 写出数据会自动关闭输出流,有返回值,Spirng MVC 再返回再次获取 getWriter()就会报错。

    1
    java.lang.IllegalStateException: getWriter() has already been called for this response
  5. Controller 方法是入参有 HttpServletResponse 可以和返回 ModelAndView 一起使用。

正确方式

  1. (@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
    @Controller
    @RequestMapping("/asyncServlet")
    public class AsyncServlet {

    /**
    * @param servletRequest 请求
    */
    @ResponseBody
    @GetMapping("/testAsync")
    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("接收请求线程结束");

    }
  2. 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
    @Controller
    @RequestMapping("/asyncServlet")
    public class AsyncServlet {

    /**
    * @param servletRequest 请求
    */
    @GetMapping("/testAsync")
    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("接收请求线程结束");
    }

源码分析

  1. Spring MVC 中@RestController和@ResponseBody作用原理分析
  2. Spring MVC Controllere方法HttpServletResponse入参差异分析

Spring MVC 集成Servlet 3.0 AsyncContext异步请求异常分析

http://blog.gxitsky.com/2022/07/30/SpringMVC-39-Servlet3-AsyncContext-Exception/

作者

光星

发布于

2022-07-30

更新于

2023-03-06

许可协议

评论