走在前沿
VMware 提供培训和认证,以加速您的进步。
了解更多上次更新于 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,然后使用该 ID 轮询最终确认。我已经更新了该示例以利用 Spring 3.2 Servlet 3 异步支持。 主分支包含更改之前代码,而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
是使用两个构造函数参数创建的。第一个是要使用的超时值,第二个是如果发生超时要使用的默认结果,在本例中为空列表。
执行交易
交易执行所需的更改遵循类似的模式。与其发送一个请求来执行交易,然后轮询确认,不如发送一个请求提交交易,然后等待确认。
下一篇文章也是最后一篇文章介绍了一个持久聊天示例。