I am trying to better understand how the following spring mvc 3.2 application works: https://github.com/rstoyanchev/spring-mvc-chat
My question is about the deferredResult Spring MVC class. I noticed that at a given time there are as many entries in the chatRequests
Map as there are users connected to the chat application.
Say there are 3 users connected to the chat application. You will see that when user #3 posts a message (see postMessage method below), then the for loop (in postMessage method) iterates three times. I can't figure out why that is.
I am including sample code below.
Code for controller:
@Controller
@RequestMapping("/mvc/chat")
public class ChatController {
private final ChatRepository chatRepository;
private final Map<DeferredResult<List<String>>, Integer> chatRequests = new ConcurrentHashMap<DeferredResult<List<String>>, Integer>();
@Autowired
public ChatController(ChatRepository chatRepository) {
this.chatRepository = chatRepository;
}
@RequestMapping(method = RequestMethod.GET)
@ResponseBody
public DeferredResult<List<String>> getMessages(@RequestParam int messageIndex) {
final DeferredResult<List<String>> deferredResult = new DeferredResult<List<String>>(null, Collections.emptyList());
this.chatRequests.put(deferredResult, messageIndex);
deferredResult.onCompletion(new Runnable() {
@Override
public void run() {
chatRequests.remove(deferredResult);
}
});
List<String> messages = this.chatRepository.getMessages(messageIndex);
if (!messages.isEmpty()) {
deferredResult.setResult(messages);
}
return deferredResult;
}
@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public void postMessage(@RequestParam String message) {
this.chatRepository.addMessage(message);
// Update all chat requests as part of the POST request
// See Redis branch for a more sophisticated, non-blocking approach
for (Entry<DeferredResult<List<String>>, Integer> entry : this.chatRequests.entrySet()) {
List<String> messages = this.chatRepository.getMessages(entry.getValue());
entry.getKey().setResult(messages);
}
}
}
Javascript code:
$(document).ready(function() {
function ChatViewModel() {
var that = this;
that.userName = ko.observable('');
that.chatContent = ko.observable('');
that.message = ko.observable('');
that.messageIndex = ko.observable(0);
that.activePollingXhr = ko.observable(null);
var keepPolling = false;
that.joinChat = function() {
if (that.userName().trim() != '') {
keepPolling = true;
pollForMessages();
}
}
function pollForMessages() {
if (!keepPolling) {
return;
}
var form = $("#joinChatForm");
that.activePollingXhr($.ajax({url: form.attr("action"), type: "GET", data: form.serialize(), cache: false,
success: function(messages) {
console.log(messages);
for (var i = 0; i < messages.length; i++) {
that.chatContent(that.chatContent() + messages[i] + "\n");
that.messageIndex(that.messageIndex() + 1);
}
},
error: function(xhr) {
if (xhr.statusText != "abort" && xhr.status != 503) {
resetUI();
console.error("Unable to retrieve chat messages. Chat ended.");
}
},
complete: pollForMessages
}));
$('#message').focus();
}
that.postMessage = function() {
if (that.message().trim() != '') {
var form = $("#postMessageForm");
$.ajax({url: form.attr("action"), type: "POST",
data: "message=[" + that.userName() + "] " + $("#postMessageForm input[name=message]").val(),
error: function(xhr) {
console.error("Error posting chat message: status=" + xhr.status + ", statusText=" + xhr.statusText);
}
});
that.message('');
}
}
that.leaveChat = function() {
that.activePollingXhr(null);
resetUI();
this.userName('');
}
function resetUI() {
keepPolling = false;
that.activePollingXhr(null);
that.message('');
that.messageIndex(0);
that.chatContent('');
}
}
//Activate knockout.js
ko.applyBindings(new ChatViewModel());
});
and html page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Chat</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Chat</h1>
<form id="joinChatForm" th:action="@{/mvc/chat}" data-bind="visible: activePollingXhr() == null">
<p>
<label for="user">User: </label>
<input id="user" name="user" type="text" data-bind="value: userName"/>
<input name="messageIndex" type="hidden" data-bind="value: messageIndex"/>
<button id="start" type="submit" data-bind="click: joinChat">Join Chat</button>
</p>
</form>
<form id="leaveChatForm" th:action="@{/mvc/chat}" data-bind="visible: activePollingXhr() != null">
<p>
You're chatting as <strong data-bind="text: userName"></strong>
<button id="leave" type="submit" data-bind="click: leaveChat">Leave Chat</button>
</p>
</form>
<div data-bind="visible: activePollingXhr() != null">
<textarea rows="15" cols="60" readonly="readonly" data-bind="text: chatContent"></textarea>
</div>
<form id="postMessageForm" th:action="@{/mvc/chat}" data-bind="visible: activePollingXhr() != null">
<p>
<input id="message" name="message" type="text" data-bind="value: message" />
<button id="post" type="submit" data-bind="click: postMessage">Post</button>
</p>
</form>
</body>
<script type="text/javascript" src="../../../resources/js/jquery-1.7.2.min.js" th:src="@{/resources/js/jquery-1.7.2.min.js}"></script>
<script type="text/javascript" src="../../../resources/js/knockout-2.0.0.js" th:src="@{/resources/js/knockout-2.0.0.js}"></script>
<script type="text/javascript" src="../../../resources/js/chat.js" th:src="@{/resources/js/chat.js}"></script>
</html>
In order to understand what DeferredResult is doing you need to understand Servlet 3.0 Async concept.
Using Servlet 3.0 you can take AsyncContext from request, store it in kind of Collection.
AsyncContext aCtx = request.startAsync(request, response);
as the result your Application Container Thread will be released.
Make some operation on separate thread and write result back to the Servlet response:
aCtx.getResponse().getWriter().print(result);
From that point of your DeferredResult
works absolutely the same.
Small example:
Now consider that every 5 second you're getting a quote from third party service. And you have clients which are long polling your server every in order to get updated what.
You Have your Controller method:
/** put deferred result to some HashSet. This is the same logic as you
store async context in servlet 3.0, those are clients who are waiting for
response
**/
@RequestMapping(value="/getQuote.do", method=RequestMethod.GET)
@ResponseBody
public DeferredResult<String> getQuote(){
final DeferredResult<String> deferredResult = new DeferredResult<String>();
someMap.put(deferredResult);
return deferredResult;
}
now let's see the method outside of controller which gets quote and returns response to the client.
function getQuoteAndUpdateClients(){
String quote = getUpdatedQuoteFromThirdPartyService();
for (DeferredResult<String> deferredResult: someMap){
deferredResult.setResult(quote);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With