领先一步
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 方法。等等。
如果您通过浏览器中的“Async Requests”选项卡执行 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”一样,“completion”总是在异步请求完成时发生。“timeout”回调发生在“completion”之前,并且可以选择一个备用值来完成处理,以及通知 Callable 停止处理。
以下是超时场景中的事件顺序
MvcAsyncTask 中的 CallableCallableMvcAsyncTask 被通知回调Callable 取消处理要完全理解上述场景,请考虑涉及的线程——请求处理开始的初始 Servlet 容器线程,执行 Callable 的 Spring MVC 管理的线程,引发超时事件的 Servlet 容器线程,以及处理最终异步分派的 Servlet 容器线程。
异常
当 Callable 引发异常时,它会通过 HandlerExceptionResolver 机制进行处理,就像任何其他控制器方法引发的异常一样。更详细的解释是,捕获并保存异常,然后将请求分派到 Servlet 容器,在那里恢复处理并调用 HandlerExceptionResolver 链。这也意味着 @ExceptionHandler 方法将像往常一样被调用。
Handler 拦截
HandlerInterceptor 的 preHandle 方法像往常一样从初始 Servlet 容器线程调用。如果控制器返回一个 Callable 并开始异步处理,则没有结果,请求也没有完成。因此,postHandle 和 afterCompletion 不会在初始 Servlet 容器线程中调用。相反,拦截器可以实现 AsyncHandlerInterceptor(一个子接口)和 afterConcurrentHandlingStarted 方法。在 Callable 完成并将请求分派到 Servlet 容器之后,HandlerInterceptor 的所有方法都将在分派的线程中调用。
Servlet 过滤器
所有 Spring Framework Servlet 过滤器实现都已根据需要进行了修改,以在异步请求处理中工作。至于其他过滤器,有些将工作——通常是那些进行预处理的,而其他过滤器需要修改——通常是那些在请求结束时进行后处理的。这些过滤器需要识别初始 Servlet 容器线程何时退出,为另一个线程继续处理让路,以及何时作为异步分派的一部分被调用以完成处理。
OpenSessionInViewFilter 和 OpenEntityManagerInViewFilter 已更新,以便在整个异步请求的生命周期内透明工作。但是,如果直接在控制器方法上使用 @Transactional,事务将在控制器方法返回时完成,并且不会扩展到 Callable 的执行。如果 Callable 需要执行事务性工作,它应该委托给具有 @Transactional 方法的 bean。
下一篇文章 探讨了使用 DeferredResult 进行异步处理,通过修改 Spring AMQP 项目的一个现有示例,该示例响应 AMQP 消息并将更新发送到浏览器。