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 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的构造函数接受超时值和任务执行器。此外,它提供了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 容器线程中调用。相反,拦截器可以实现子接口AsyncHandlerInterceptorafterConcurrentHandlingStarted方法。在Callable完成并将请求分派到 Servlet 容器后,HandlerInterceptor的所有方法都将在分派线程中调用。

Servlet 过滤器

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

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

下一篇文章探讨了通过修改来自 Spring AMQP 项目的现有示例来使用DeferredResult进行异步处理,该示例对 AMQP 消息做出反应并将更新发送到浏览器。

获取 Spring 时事通讯

通过 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

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

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部