In case you haven’t heard what Websocket is, long story short it’s a brand new cool technique of asynchronous client-server communication for web application. Instead of periodic / long ajax polling, newer browsers allow you to have a persistent socket (almost like TCP) where both client and server can send messages anytime.
Yes the all-new Spring 4 came with shiny Websocket support! Here’s a stock ticker app to get you started (thanks to raymondhlee’s article for the inspiration). This app will let you add/remove a stock code and update its price every second (by randomly adding / subtracting some percentage)
Environment / Tools
- Java 7
- Tomcat 7.0.47
- Servlet API 3
- Spring Framework 4.0.2.RELEASE
Maven Dependencies
Most dependencies are similar to Spring MVC but there’s few addition required to support websocket. You also need to use Servlet 3.
org.springframework spring-context ${org.springframework-version} org.springframework spring-webmvc ${org.springframework-version} org.springframework spring-messaging ${org.springframework-version} org.springframework spring-websocket ${org.springframework-version} javax.servlet javax.servlet-api 3.1.0 provided com.fasterxml.jackson.core jackson-databind 2.3.2
web.xml
You need to use version 3 of the web.xml schema, and on DispatcherServlet config, set
appServlet org.springframework.web.servlet.DispatcherServlet contextConfigLocation /WEB-INF/servlet-context.xml 1 true appServlet /
Setup Websocket Message Broker On Servlet Context XML
Apart from the standard Spring MVC config, one new stuff we’re introducing is the Websocket Message Broker. The message broker will help us listening, mapping and sending messages. Note that as suggested by Spring docs we’re using STOMP message protocol and SockJS to support non-websocket browser.
Also note following configs:
- Stomp endpoint: /ws
- Message broker app prefix: /app
- Message queue prefix: /topic
Here’s src/main/webapp/WEB-INF/servlet-context.xml:
The Domain Object
Stock class is a simple POJO with code, price and time fields. I’ve also added getTimeStr() to format the time as string and additional constructor.
package com.gerrydevstory.stockticker; import java.io.Serializable; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; public class Stock implements Serializable { private static final long serialVersionUID = 1L; private String code = ""; private double price = 0.0; private Date time = new Date(); public Stock() { } public Stock(String code, double price) { this.code = code; this.price = price; } private DateFormat df = new SimpleDateFormat("dd MMM yyyy, HH:mm:ss"); public String getTimeStr() { return df.format(time); } /* standard getters & setters */ }
Broadcast Prices And Add / Remove Stock
At the core of this app is the HomeController class. There’s a updatePriceAndBroadcast() method which is scheduler to run every 1 second using TaskScheduler. This controller also has websocket handler method to add new stock and remove all. Note the usage of annotation, it will make more sense once we go through the javascript part below.
package com.gerrydevstory.stockticker; public class HomeController { private SimpMessagingTemplate template; private TaskScheduler scheduler = new ConcurrentTaskScheduler(); private ListstockPrices = new ArrayList (); private Random rand = new Random(System.currentTimeMillis()); /** * Iterates stock list, update the price by randomly choosing a positive * or negative percentage, then broadcast it to all subscribing clients */ private void updatePriceAndBroadcast() { for(Stock stock : stockPrices) { double chgPct = rand.nextDouble() * 5.0; if(rand.nextInt(2) == 1) chgPct = -chgPct; stock.setPrice(stock.getPrice() + (chgPct / 100.0 * stock.getPrice())); stock.setTime(new Date()); } template.convertAndSend("/topic/price", stockPrices); } /** * Invoked after bean creation is complete, this method will schedule * updatePriceAndBroacast every 1 second */ private void broadcastTimePeriodically() { scheduler.scheduleAtFixedRate(new Runnable() { public void run() { updatePriceAndBroadcast(); } }, 1000); } /** * Handler to add one stock */ ("/addStock") public void addStock(Stock stock) throws Exception { stockPrices.add(stock); updatePriceAndBroadcast(); } /** * Handler to remove all stocks */ ("/removeAllStocks") public void removeAllStocks() { stockPrices.clear(); updatePriceAndBroadcast(); } /** * Serve the main page, view will resolve to /WEB-INF/home.jsp */ (value = "/", method = RequestMethod.GET) public String home() { return "home"; } }
Client Side Stuff
To render the stock prices, I created an empty HTML table. The idea is we will empty the table and fill it in with new prices per update
Code | Price | Time |
---|
Underneath that, I’ll also add few form input so you can add a new stock and remove everything
Code: Price:
The javascript stuff is a bit complicated. Apart from JQuery, there are 2 libraries used here: StompJS and SockJS. As opposed of using direct API, SockJS provides fallback for older browser not supporting websocket. StompJS provides higher level abstraction of sending and receiving messages in STOMP protocol.
StompJS did not come with CDN, so I had to manually download it and place it on src/main/webapp/resources/stomp.js
Next is the inline script block. Here I used SockJS to connect to the Spring websocket STOMP endpoint /ws (recall servlet-context.xml above). My webapp context path is /stockticker.
//Create stomp client over sockJS protocol var socket = new SockJS("/stockticker/ws"); var stompClient = Stomp.over(socket); // Callback function to be called when stomp client is connected to server var connectCallback = function() { stompClient.subscribe('/topic/price', renderPrice); }; // Callback function to be called when stomp client could not connect to server var errorCallback = function(error) { alert(error.headers.message); }; // Connect to server via websocket stompClient.connect("guest", "guest", connectCallback, errorCallback);
The connectCallback function above registers renderPrice callback when a message is sent to /topic/price. This function empties the result HTML table and re-add the cells with new stock price
// Render price data from server into HTML, registered as callback // when subscribing to price topic function renderPrice(frame) { var prices = JSON.parse(frame.body); $('#price').empty(); for(var i in prices) { var price = prices[i]; $('#price').append( $('').append( $(' ').html(price.code), $(' ').html(price.price.toFixed(2)), $(' ').html(price.timeStr) ) ); } } And lastly, utilising JQuery let’s create handlers for adding and removig stocks
// Register handler for add button $(document).ready(function() { $('.add').click(function(e){ e.preventDefault(); var code = $('.new .code').val(); var price = Number($('.new .price').val()); var jsonstr = JSON.stringify({ 'code': code, 'price': price }); stompClient.send("/app/addStock", {}, jsonstr); return false; }); }); // Register handler for remove all button $(document).ready(function() { $('.remove-all').click(function(e) { e.preventDefault(); stompClient.send("/app/removeAllStocks"); return false; }); });Download And Try The Source Code
The source code of this demo app . Clone it using git:
git clone .gitImport it as Existing Maven Project in STS (File > Import > Existing Maven Project) and run on in-memory Tomcat 7.0.47 using following Run Configuration (Run > Run Configurations…):
And this Tomcat container has to run on Java 7 to enable Websocket support.
Gerry's software development journey of trial, errors and re-trials