Spring MVC 3.2 预览版:使控制器方法异步化

工程 | Rossen Stoyanchev | 2012 年 5 月 10 日 | ...

最后更新于 2012 年 11 月 5 日 (Spring MVC 3.2 RC1)

之前文章中,我介绍了 Spring MVC 3.2 中基于 Servlet 3 的异步功能,并讨论了实时更新的技术。在本文中,我将深入探讨更多技术细节,并讨论异步处理如何融入 Spring MVC 请求生命周期。

简单回顾一下,您可以通过将任何现有的控制器方法修改为返回 Callable 来使其异步化。例如,返回视图名称的控制器方法可以改为返回 Callable<String>。返回名为 Person 对象的 @ResponseBody 方法可以改为返回 Callable<Person>。对于任何其他控制器返回值类型也是如此。

核心思想是,您对控制器方法工作原理已有的了解尽可能保持不变,只是剩余的处理将在另一个线程中发生。谈到异步执行时,保持简单非常重要。正如您将看到的,即使是这种看似简单的编程模型变化,也有相当多的事情需要考虑。

已更新 spring-mvc-showcase 以支持 Spring MVC 3.2。请查看 CallableController。您可能期望,诸如 @ResponseBody@ResponseStatus 之类的方法注解也适用于 Callable 的返回值。从 Callable 中抛出的异常会被处理,就像它们是由控制器抛出的一样,在这种情况下使用 @ExceptionHandler 方法进行处理。依此类推。

如果您通过浏览器中的“异步请求”选项卡执行 CallableController 的其中一个方法,您应该会看到类似于下面的输出

08:25:15 [http-bio-8080-exec-10] DispatcherServlet - DispatcherServlet with name 'appServlet' processing GET request for [...]
08:25:15 [http-bio-8080-exec-10] RequestMappingHandlerMapping - Looking up handler method for path /async/callable/view
08:25:15 [http-bio-8080-exec-10] RequestMappingHandlerMapping - Returning handler method [...]
08:25:15 [http-bio-8080-exec-10] WebAsyncManager - Concurrent handling starting for GET [...]
08:25:15 [http-bio-8080-exec-10] DispatcherServlet - Leaving response open for concurrent processing
08:25:17 [MvcAsync1] WebAsyncManager - Concurrent result value [views/html]
08:25:17 [MvcAsync1] WebAsyncManager - Dispatching request to resume processing
08:25:17 [http-bio-8080-exec-6] DispatcherServlet - DispatcherServlet with name 'appServlet' resumed processing GET request for [...]
08:25:17 [http-bio-8080-exec-6] RequestMappingHandlerMapping - Looking up handler method for path /async/callable/view
08:25:17 [http-bio-8080-exec-6] RequestMappingHandlerMapping - Returning handler method [...]
08:25:17 [http-bio-8080-exec-6] RequestMappingHandlerAdapter - Found concurrent result value [views/html]
08:25:17 [http-bio-8080-exec-6] DispatcherServlet - Rendering view [...] in DispatcherServlet with name 'appServlet'
08:25:17 [http-bio-8080-exec-6] JstlView - Added model object 'fruit' of type [java.lang.String]
08:25:17 [http-bio-8080-exec-6] JstlView - Added model object 'foo' of type [java.lang.String]
08:25:17 [http-bio-8080-exec-6] JstlView - Forwarding to resource [/WEB-INF/views/views/html.jsp]
08:25:17 [http-bio-8080-exec-6] DispatcherServlet - Successfully completed request

请注意,初始 Servlet 容器线程在记录了并发处理已开始的消息后会迅速退出。这是因为控制器方法返回了一个 Callable。第二个线程——由 Spring MVC 通过 AsyncTaskExecutor 管理——调用 Callable 以生成一个值,在本例中是基于字符串的视图名称,然后请求被分派回 Servlet 容器。最后,在第三个 Servlet 容器线程(分派)中,通过渲染选定的视图来完成处理。如果您查看时间戳,您会注意到从初始线程退出到 Callable 准备就绪之间存在 2 秒的模拟延迟。

注意:如果您不熟悉 Servlet 3 异步 API,异步分派类似于转发,但转发发生在同一线程中,而分派是从应用程序线程用于在 Servlet 容器线程中恢复处理。

TaskExecutor 配置

默认情况下,Spring MVC 使用 SimpleAsyncTaskExecutor 来执行由控制器方法返回的 Callable 实例。在生产环境中,您必须将其替换为针对您的环境适当配置的 AsyncTaskExecutor 实现。MVC Java 配置和 MVC 命名空间都提供了配置 AsyncTaskExecutor 和一般异步请求处理的选项。您也可以直接配置 RequestMappingHandlerAdapter

超时值

如果异步请求在一定时间内未能完成处理,Servlet 容器会触发一个超时事件,如果未处理,则会完成响应。您可以通过 MVC Java 配置、MVC 命名空间或直接在 RequestMappingHandlerAdapter 上配置超时值。如果未配置,超时值将取决于底层 Servlet 容器。在 Tomcat 上,超时时间为 10 秒,它从初始 Servlet 容器线程完全退出后开始计时。

MvcAsyncTask

如果您想为特定的控制器方法自定义超时值或任务执行器怎么办?对于这种情况,您可以将 Callable 包装在 MvcAsyncTask 实例中MvcAsyncTask 的构造函数接受一个超时值和一个任务执行器。此外,它提供了 onTimeoutonCompletion 方法,允许您注册“超时”和“完成”回调。就像 try-catch 块中的“finally”一样,“完成”总是在异步请求完成时发生。“超时”回调发生在“完成”之前,可以选择一个备用值来完成处理,并通知 Callable 停止处理。

以下是超时场景中的事件序列

  1. 控制器方法返回一个包装在 MvcAsyncTask 中的 Callable
  2. Spring MVC 在单独的线程中开始执行 Callable
  3. Servlet 容器线程退出(并且超时周期开始)
  4. MvcAsyncTask 收到回调通知
  5. 回调代码选择一个备用值并通知 Callable 取消处理
  6. 请求被分派回容器,使用备用值完成处理

为了完全理解上述场景,请考虑涉及的线程——请求处理开始的初始 Servlet 容器线程,执行 Callable 的 Spring MVC 管理线程,超时事件发生的 Servlet 容器线程,以及处理最终异步分派的 Servlet 容器线程。

异常

Callable 抛出异常时,它会像其他控制器方法抛出的异常一样,通过 HandlerExceptionResolver 机制处理。更详细的解释是,异常被捕获并保存,然后请求被分派到 Servlet 容器,在那里处理恢复并调用 HandlerExceptionResolver 链。这也意味着 @ExceptionHandler 方法将照常调用。

处理程序拦截

HandlerInterceptorpreHandle 方法照常从初始 Servlet 容器线程调用。如果控制器返回 Callable 并开始异步处理,则既没有结果,请求也未完成。因此,postHandleafterCompletion 不会在初始 Servlet 容器线程中调用。相反,拦截器可以实现 AsyncHandlerInterceptor(一个子接口)以及 afterConcurrentHandlingStarted 方法。在 Callable 完成且请求被分派到 Servlet 容器后,HandlerInterceptor 的所有方法都会在分派的线程中调用。

Servlet 过滤器

所有 Spring Framework Servlet 过滤器的实现都已根据需要在异步请求处理中进行修改。至于其他过滤器,有些会正常工作——通常是执行预处理的过滤器,而另一些则需要修改——通常是执行请求结束时后处理的过滤器。这类过滤器需要识别初始 Servlet 容器线程何时退出,为另一个线程继续处理让路,以及它们何时作为异步分派的一部分被调用以完成处理。

OpenSessionInViewFilterOpenEntityManagerInViewFilter 已更新,以便在整个异步请求期间透明地工作。但是,如果直接在控制器方法上使用 @Transactional,事务将在控制器方法返回时立即完成,而不会延伸到 Callable 的执行。如果 Callable 需要执行事务性工作,应委托给具有 @Transactional 方法的 bean。

下一篇文章通过修改 Spring AMQP 项目中响应 AMQP 消息并向浏览器发送更新的现有示例,探讨了使用 DeferredResult 进行异步处理。

订阅 Spring 邮件列表

订阅 Spring 邮件列表,保持联系

订阅

领先一步

VMware 提供培训和认证,为您的进步注入强大动力。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部