Last updated on November 5th, 2012 (Spring MVC 3.2 RC1)
In my last post I discussed how to make a Spring MVC controller method asynchronous by returning a Callable which is then invoked in a separate thread by Spring MVC.
But what if async processing depended on receiving some external event in a thread not known to Spring MVC -- e.g. receiving a JMS message, an AMQP message, a Redis pub-sub notification, a Spring Integration event, and so on? I'll explore this scenario by modifying an existing sample from the Spring AMQP project.
The Sample
Spring AMQP has a stock trading sample where a QuoteController
sends trade execution messages via Spring AMQP's RabbitTemplate and receives trade confirmation and price quote messages via Spring AMQP's RabbitMQ listener container in message-driven POJO style.
In the browser, the sample uses polling to display price quotes. For trades, the initial request submits the trade and a confirmation id is returned that is then used to poll for the final confirmation. I've updated the sample to take advantage of the Spring 3.2 Servlet 3 async support. The master branch has the code before and the spring-mvc-async branch has the code after the change. The images below show the effect on the frequency of price quote requests (using the Chrome developer tools):
Before the change: traditional polling
After the change: long poll
As you can see with regular polling new requests are sent very frequently (milliseconds apart) while with long polling, requests can be 5, 10, 20, or more seconds apart -- a significant reduction in the total number of requests without the loss of latency, i.e. the amount of time before a new price quote appears in the browser.
Getting Quotes
So what changes were required? From a client perspective traditional polling and long polling are indistinguishable so the HTML and JavaScript did not change. From a server perspective requests must be held up until new quotes arrive. This is how the controller processes a request for quotes:
// 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…