领先一步
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 3.2 更新了 spring-mvc-showcase。请查看 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 项目的现有示例来使用DeferredResult
进行异步处理,该示例对 AMQP 消息做出反应并将更新发送到浏览器。