Spring MVC 3.2 技术预览(三):动手写一个异步Controller方法
引言
在前面的文章中,我介绍了 Servlet 3 与 Spring MVC 3.2 中支持异步的新特性,并探讨了一些实时更新的技术背景。在这篇文章中,我将展示 Spring MVC 3.2 新特性的技术细节,以及它们对 Spring MVC 请求生命周期的多方面影响。
异步 Controller 方法基础
如果需要将 Controller 层的方法转变为异步方法,只需将方法的返回值类型改为 Callable 即可。例如:
- 原本返回视图名
String的方法,可以改为返回Callable<String>。 - 原本返回
ResponseEntity的方法,可以改为返回Callable<ResponseEntity>。 - 其他返回值类型依此类推。
在这种处理方式中,除了 Controller 层方法会在另一个线程中处理完成外,其他工作方式没有发生任何变化。当方法改为异步处理后,保持处理逻辑的简单非常重要。因为即便只是将方法改为异步方式,仍有诸多相关问题需要考虑。
示例代码
GitHub 上 spring-mvc-showcase 项目的 spring-mvc-async 分支里,提供了许多 Controller 层异步方法的示例。
返回 @ResponseBody
例如下面的 @ResponseBody 方法,返回了视图名 String:
@RequestMapping(value="/response/annotation", method=RequestMethod.GET)
public @ResponseBody Callable<String> responseBody() {
return new Callable<String>() {
public String call() throws Exception {
// Do some work..
Thread.sleep(3000L);
return "The String ResponseBody";
}
};
}返回 ResponseEntity
以及下面的 ResponseEntity 方法:
@RequestMapping(value="/response/entity/headers", method=RequestMethod.GET)
public Callable<ResponseEntity<String>> responseEntityCustomHeaders() {
return new Callable<ResponseEntity<String>>() {
public ResponseEntity<String> call() throws Exception {
// Do some work..
Thread.sleep(3000L);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<String>(
"The String ResponseBody with custom header Content-Type=text/plain",
headers,
HttpStatus.OK);
}
};
}重定向视图
@RequestMapping(value="/uriTemplate", method=RequestMethod.GET)
public Callable<String> uriTemplate(final RedirectAttributes redirectAttrs) {
return new Callable<String>() {
public String call() throws Exception {
// Do some work..
Thread.sleep(3000L);
redirectAttrs.addAttribute("account", "a123"); // Used as URI template variable
redirectAttrs.addAttribute("date", new LocalDate(2011, 12, 31)); // Appended as a query parameter
return "redirect:/redirect/{account}";
}
};
}异常处理
添加了 @RequestMapping 注解和 @ResponseBody 注解的方法中,这些注解同样会应用到返回值 Callable 中。添加了 @ExceptionHandler 注解的方法也一样,它可以处理 Controller 层方法返回的 Callable 中抛出的异常。
package org.springframework.samples.mvc.exceptions;
import java.util.concurrent.Callable;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class ExceptionController {
@RequestMapping("/exception")
public @ResponseBody Callable<String> exception() {
return new Callable<String>() {
public String call() throws Exception {
// Do some work..
Thread.sleep(2000L);
throw new IllegalStateException("Sorry!");
}
};
}
@ExceptionHandler
public @ResponseBody String handle(IllegalStateException e) {
return "IllegalStateException handled!";
}
}在 GitHub 中提交的 这个版本,记录了其中全部更新的情况。
运行日志分析
如果你运行了上面的任意一个方法,将会在控制台看到如下信息:
16:19:23 [http-bio-8080-exec-3] DispatcherServlet - DispatcherServlet with name 'appServlet' processing ...
16:19:23 [http-bio-8080-exec-3] RequestMappingHandlerMapping - Looking up handler method for path /views/html
16:19:23 [http-bio-8080-exec-3] RequestMappingHandlerMapping - Returning handler method ...
16:19:23 [http-bio-8080-exec-3] DispatcherServlet - Exiting request thread and leaving the response open
16:19:23 [SimpleAsyncTaskExecutor-1] DispatcherServlet - Resuming asynchronous processing of ...
16:19:26 [SimpleAsyncTaskExecutor-1] DispatcherServlet - Rendering view ...
16:19:26 [SimpleAsyncTaskExecutor-1] JstlView - Added model object 'fruit'
16:19:26 [SimpleAsyncTaskExecutor-1] JstlView - Added model object 'foo'
16:19:26 [SimpleAsyncTaskExecutor-1] JstlView - Forwarding to resource [/WEB-INF/views/views/html.jsp]
16:19:26 [SimpleAsyncTaskExecutor-1] DispatcherServlet - Successfully completed request
16:19:26 [SimpleAsyncTaskExecutor-1] AsyncExecutionChainRunnable - Completing async request processing 从上面的日志信息中可以看出,Servlet 容器调用的线程马上就执行完了方法,而余下的处理内容则在 3 秒钟后由另外一个线程完成。除了线程切换之外,上面的日志信息与普通的请求处理信息是一样的。
线程池配置
正如上面的日志信息所示,返回值 Callable 会默认调用 SimpleAsyncTaskExecutor 类来处理。这个类非常简单,而且不会重用线程。
在实际的产品环境中,你可能需要使用 AsyncTaskExecutor 类来针对所处环境进行适当的配置,甚至有可能你已经有了一个配置好的 AsyncTaskExecutor 类。可以通过 RequestMappingHandlerAdapter 类中的 asyncTaskExecutor 属性来引用它。
超时设定
超时设定是我们需要考虑的一个非常重要的方面。因为 Servlet 容器可能会强制将一个超时的未完成异步请求关闭。你可以通过 RequestMappingHandlerAdapter 类中的 asyncRequestTimeout 属性指定超时时间。如果不指定超时时间,超时的时长将取决于 Servlet 容器所设定的时间。在 Tomcat 中,这个超时时间被默认设定为 10 秒钟(从 Servlet 容器调用的线程执行时就开始计时)。
在超时后仍然使用一个 request 或 response 的影响是不确定的。在实际使用中,Servlet 容器将尝试重用 request 和 response 对象。这样一来,避免在超时后仍然使用 request 和 response 将变得非常重要。
事实上,没有方法可以检测 request 是否已经超时。但是 Servlet API 中,当请求超时或网络出现问题时,将提供一个声明式的回调函数。Spring MVC 中自动注册了这个声明,因此可以得知一对请求响应是否不应该被使用。
想要完全理解上面的过程,可以参考其中涉及到的三个线程:
- 请求处理开始的线程(Servlet 容器调用的线程)。
- 执行 Callable 方法的线程(异步线程)。
- Servlet 容器向 Spring MVC 声明超时的线程。
异常处理
异步处理中 HandlerExceptionResolver 类和异常处理的机制没有太多不同。
- 当返回
Callable之前发生了异常,处理方式与普通异常一样。 - 当执行
Callable方法的过程中产生异常,处理方式也与普通异常一样,只不过将在当前线程(异步线程)中处理,并将仍然返回未处理的 500 状态的 response。
ThreadLocal 属性
Spring MVC 的一些部分和 Spring MVC 应用程序可能会依赖 ThreadLocal 存储来获取 request、locale 及其他信息。当以异步的方式执行 Callable 方法时,异步线程将不会自动拥有相同的 ThreadLocal 属性。
OpenSessionInViewFilter 和 OpenSessionInViewInterceptor 也被更新为以透明的方式工作。而当 Controller 层的方法使用了 @Transactional 注解时,方法返回时就将完成事务,而不会扩展到执行 Callable 方法的内部。如果 Callable 需要处理事务,则需要委托(delegate)一个事务组件。
拦截器处理
已注册的 HandlerInterceptor 实例将与异步请求协作工作。主要的区别是:
preHandle在 Servlet 容器线程开始的时候调用。postHandle和afterCompletion方法则在异步线程中调用。
在大多数情况下不会出现问题,除非 HandlerInterceptor 设置并清除了 ThreadLocal 属性。需要如此做的拦截器可能实现了 AsyncHandlerInterceptor 接口,这个接口为异步请求的处理添加了生命周期。
Servlet 过滤器
一些过滤器将正常工作。而其他的过滤器将尝试在 Servlet 容器线程退出后执行后置处理(post-processing)。这样的过滤器需要进行一定的修改,用来在异步线程中完成后置处理。
所有的 Spring 过滤器都已经按照要求(按照异步请求处理的要求)进行修改,来与异步请求协同工作。但是第三方的过滤器是否能够在 Spring MVC 下正常处理异步请求,取决于这些过滤器的实现细节。
总结
在我的下一篇文章中,我将使用一个基于接收外部事件(AMQP 消息)的示例,将其使用传统轮询的方式修改为使用长轮询的方式,用来在浏览器中显示实时信息。
说明:本文基于 Spring MVC 3.2 技术预览版编写,属于较早期的异步支持方案(基于 Callable)。现代 Spring 版本(如 Spring 5+)推荐使用 CompletableFuture、DeferredResult 或响应式编程模型(WebFlux)来处理异步请求,具体实现细节可能有所不同。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。