领先一步
VMware 提供培训和认证,为您的进步注入强大动力。
了解更多最后更新于 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
的构造函数接受一个超时值和一个任务执行器。此外,它提供了 onTimeout
和 onCompletion
方法,允许您注册“超时”和“完成”回调。就像 try-catch 块中的“finally”一样,“完成”总是在异步请求完成时发生。“超时”回调发生在“完成”之前,可以选择一个备用值来完成处理,并通知 Callable
停止处理。
以下是超时场景中的事件序列
MvcAsyncTask
中的 Callable
Callable
MvcAsyncTask
收到回调通知Callable
取消处理为了完全理解上述场景,请考虑涉及的线程——请求处理开始的初始 Servlet 容器线程,执行 Callable
的 Spring MVC 管理线程,超时事件发生的 Servlet 容器线程,以及处理最终异步分派的 Servlet 容器线程。
异常
当 Callable
抛出异常时,它会像其他控制器方法抛出的异常一样,通过 HandlerExceptionResolver
机制处理。更详细的解释是,异常被捕获并保存,然后请求被分派到 Servlet 容器,在那里处理恢复并调用 HandlerExceptionResolver
链。这也意味着 @ExceptionHandler
方法将照常调用。
处理程序拦截
HandlerInterceptor
的 preHandle
方法照常从初始 Servlet 容器线程调用。如果控制器返回 Callable
并开始异步处理,则既没有结果,请求也未完成。因此,postHandle
和 afterCompletion
不会在初始 Servlet 容器线程中调用。相反,拦截器可以实现 AsyncHandlerInterceptor
(一个子接口)以及 afterConcurrentHandlingStarted
方法。在 Callable
完成且请求被分派到 Servlet 容器后,HandlerInterceptor
的所有方法都会在分派的线程中调用。
Servlet 过滤器
所有 Spring Framework Servlet 过滤器的实现都已根据需要在异步请求处理中进行修改。至于其他过滤器,有些会正常工作——通常是执行预处理的过滤器,而另一些则需要修改——通常是执行请求结束时后处理的过滤器。这类过滤器需要识别初始 Servlet 容器线程何时退出,为另一个线程继续处理让路,以及它们何时作为异步分派的一部分被调用以完成处理。
OpenSessionInViewFilter
和 OpenEntityManagerInViewFilter
已更新,以便在整个异步请求期间透明地工作。但是,如果直接在控制器方法上使用 @Transactional
,事务将在控制器方法返回时立即完成,而不会延伸到 Callable
的执行。如果 Callable
需要执行事务性工作,应委托给具有 @Transactional
方法的 bean。
下一篇文章通过修改 Spring AMQP 项目中响应 AMQP 消息并向浏览器发送更新的现有示例,探讨了使用 DeferredResult
进行异步处理。