Spring MVC 3.2 预览:向现有 Web 应用程序添加长轮询

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

上次更新于 2012 年 11 月 5 日 (Spring MVC 3.2 RC1)

在我上一篇文章中,我讨论了如何通过返回一个 Callable 来使 Spring MVC 控制器方法异步化,然后 Spring MVC 会在单独的线程中调用它。

但是,如果异步处理依赖于在 Spring MVC 不知道的线程中接收到一些外部事件,例如接收 JMS 消息、AMQP 消息、Redis 发布-订阅通知、Spring Integration 事件等,该怎么办? 我将通过修改 Spring AMQP 项目中的现有示例来探讨此场景。

示例

Spring AMQP 有一个 股票交易示例,其中 QuoteController 通过 Spring AMQP 的 RabbitTemplate 发送交易执行消息,并通过 Spring AMQP 的 RabbitMQ 监听器容器 以消息驱动的 POJO 样式接收交易确认和报价消息。

在浏览器中,该示例使用轮询来显示报价。 对于交易,初始请求提交交易,并返回一个确认 ID,然后用于轮询最终确认。 我已更新了该示例,以利用 Spring 3.2 Servlet 3 异步支持。 master 分支具有更改之前的代码,spring-mvc-async 分支具有更改之后的代码。 下面的图像显示了对报价请求频率的影响(使用 Chrome 开发者工具)

更改之前:传统轮询

更改之后:长轮询

正如您所看到的,使用常规轮询,会非常频繁地发送新请求(间隔几毫秒),而使用长轮询,请求可以间隔 5、10、20 秒或更长时间 - 显着减少了请求的总数,而不会损失延迟,即新报价出现在浏览器中的时间量。

获取报价

那么需要进行哪些更改? 从客户端的角度来看,传统轮询和长轮询是无法区分的,因此 HTML 和 JavaScript 没有更改。 从服务器的角度来看,请求必须被搁置,直到有新报价到达。 这就是控制器处理报价请求的方式



// Class field
private Map<String, DeferredResult> suspendedTradeRequests = new ConcurrentHashMap<String, DeferredResult>();

...

@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<List<Quote>> quotes(@RequestParam(required = false) Long timestamp) {

  final DeferredResult<List<Quote>> result = new DeferredResult<List<Quote>>(null, Collections.emptyList());
  this.quoteRequests.put(result, timestamp);

  result.onCompletion(new Runnable() {
    public void run() {
      quoteRequests.remove(result);
    }
  });

  List<Quote> list = getLatestQuotes(timestamp);
  if (!list.isEmpty()) {
    result.setResult(list);
  }

  return result;
}

在上面的示例中,控制器方法准备并返回一个 DeferredResult,如果报价已可用,它可以立即设置,或者稍后通过 RabbitMQ 接收到新报价时再设置。 DeferredResult 保存在一个 Map 中,当异步请求通过注册的 onCompletion 回调完成时,将从该 Map 中删除它。

这是在收到新报价时更新保存的 DeferredResult 实例的控制器方法



// Invoked in Spring AMQP's RabbitMQ listener container thread

public void handleQuote(Quote message) {
  // ...
  for (Entry<DeferredResult<List<Quote>>, Long> entry : this.quoteRequests.entrySet()) {
    List<Quote> newQuotes = getLatestQuotes(entry.getValue());
    entry.getKey().setResult(newQuotes);
  }
  // ...
}

当有新的报价到达时,上述方法会使用最新的报价更新每个保存的 DeferredResult。 由于 DeferredResult 最初是在 @ResponseBody 方法中创建的,因此报价将作为 JSON 写入响应正文。

超时

如果与 DeferredResult 关联的异步请求超时,该怎么办? 从浏览器的角度来看,每个请求都应该带来报价,如果达到超时时间,则应返回 0 个报价。

您可能已经注意到,在上面的示例代码中,DeferredResult 是使用两个构造函数参数创建的。 第一个是使用的超时值,第二个是在发生超时时使用的默认结果,在本例中是一个空列表。

执行交易

交易执行所需的更改遵循类似的模式。 不再发送一个请求来执行交易,然后轮询确认,而是发送一个请求来提交交易,然后等待确认。

下一篇也是最后一篇文章介绍了一个持久聊天示例。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

抢先一步

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

了解更多

获得支持

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

了解更多

即将到来的活动

查看 Spring 社区中所有即将到来的活动。

查看全部