Stock Ticker Demo Webapp Using Spring 4 Websocket



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 toraymondhlee’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)

stockticker

Environment / Tools

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.

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

 

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-messaging</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

 

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

 

<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.1.0</version>
  <scope>provided</scope>
</dependency>

 
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.3.2</version>
</dependency> 

web.xml

You need to use version 3 of the web.xml schema, and on DispatcherServlet config, set<async-supported>true</async-supported>. Here’ssrc/main/webapp/WEB-INF/web.xml

<?xmlversion="1.0"encoding="UTF-8"?>

<web-appversion="3.0"xmlns="http://java.sun.com/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

 

  <!-- Processes application requests -->

  <servlet>

    <servlet-name>appServlet</servlet-name>

    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <init-param>

      <param-name>contextConfigLocation</param-name>

      <param-value>/WEB-INF/servlet-context.xml</param-value>

    </init-param>

    <load-on-startup>1</load-on-startup>

    <async-supported>true</async-supported>

  </servlet>

 

  <servlet-mapping>

    <servlet-name>appServlet</servlet-name>

    <url-pattern>/</url-pattern>

  </servlet-mapping>

 

</web-app>

Setup Websocket Message Broker On Servlet Context XML

Apart from the standard Spring MVC config, one new stuff we’re introducing is theWebsocket 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:

<?xmlversion="1.0"encoding="UTF-8"?>

<beansxmlns="http://www.springframework.org/schema/beans"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xmlns:mvc="http://www.springframework.org/schema/mvc"

  xmlns:context="http://www.springframework.org/schema/context"

  xmlns:websocket="http://www.springframework.org/schema/websocket"

  xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd

        http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd

        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

 

  <mvc:annotation-driven/>

 

  <mvc:resourcesmapping="/resources/**"location="/resources/"/>

 

  <beanclass="org.springframework.web.servlet.view.InternalResourceViewResolver">

    <propertyname="prefix"value="/WEB-INF/"/>

    <propertyname="suffix"value=".jsp"/>

  </bean>

 

  <context:component-scanbase-package="com.gerrydevstory.stockticker"/>

 

  <websocket:message-brokerapplication-destination-prefix="/app">

    <websocket:stomp-endpointpath="/ws">

      <websocket:sockjs/>

    </websocket:stomp-endpoint>

    <websocket:simple-brokerprefix="/topic"/>

  </websocket:message-broker>


The Domain Object

Stock class is a simple POJO with code, price and time fields. I’ve also addedgetTimeStr() 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 implementsSerializable {

 

  privatestaticfinal long serialVersionUID = 1L;

  privateString code ="";

  privatedoubleprice = 0.0;

  privateDate time =newDate();

   

  publicStock() {

     

  }

   

  publicStock(String code,doubleprice) {

    this.code = code;

    this.price = price;

  }

   

  privateDateFormat df =newSimpleDateFormat("dd MMM yyyy, HH:mm:ss");

   

  publicString getTimeStr() {

    returndf.format(time);

  }

   

  /* standard getters & setters */

   

}

Broadcast Prices And Add / Remove Stock

At the core of this app is the HomeController class. There’s aupdatePriceAndBroadcast() method which is scheduler to run every 1 second usingTaskScheduler. This controller also has websocket handler method to add new stock and remove all. Note the usage of@MessageMapping annotation, it will make more sense once we go through the javascript part below.

package com.gerrydevstory.stockticker;

 

@Controller

public class HomeController {

 

  @AutowiredprivateSimpMessagingTemplate template; 

  privateTaskScheduler scheduler =newConcurrentTaskScheduler();

  privateList<Stock> stockPrices =newArrayList<Stock>();

  privateRandom rand =newRandom(System.currentTimeMillis());

   

  /**

   * Iterates stock list, update the price by randomly choosing a positive

   * or negative percentage, then broadcast it to all subscribing clients

   */

  privatevoidupdatePriceAndBroadcast() {

    for(Stock stock : stockPrices) {

      doublechgPct = rand.nextDouble() *5.0;

      if(rand.nextInt(2) ==1) chgPct = -chgPct;

      stock.setPrice(stock.getPrice() + (chgPct /100.0* stock.getPrice()));

      stock.setTime(newDate());

    }

    template.convertAndSend("/topic/price", stockPrices);

  }

   

  /**

   * Invoked after bean creation is complete, this method will schedule

   * updatePriceAndBroacast every 1 second

   */

  @PostConstruct

  privatevoidbroadcastTimePeriodically() {

    scheduler.scheduleAtFixedRate(newRunnable() {

      @Overridepublicvoid run() {

        updatePriceAndBroadcast();

      }

    },1000);

  }

   

  /**

   * Handler to add one stock

   */

  @MessageMapping("/addStock")

  publicvoidaddStock(Stock stock)throwsException {

    stockPrices.add(stock);

    updatePriceAndBroadcast();

  }

   

  /**

   * Handler to remove all stocks

   */

  @MessageMapping("/removeAllStocks")

  publicvoidremoveAllStocks() {

    stockPrices.clear();

    updatePriceAndBroadcast();

  }

   

  /**

   * Serve the main page, view will resolve to /WEB-INF/home.jsp

   */

  @RequestMapping(value ="/", method = RequestMethod.GET)

  publicString 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

<table>
  <thead><tr><th>Code</th><th>Price</th><th>Time</th></tr></thead>
  <tbodyid="price"></tbody>
</table>



Underneath that, I’ll also add few form input so you can add a new stock and remove everything


<pclass="new">
   Code: <inputtype="text"class="code"/>
   Price: <inputtype="text"class="price"/>
<buttonclass="add">Add</button>
<buttonclass="remove-all">Remove All</button>
</p>


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 onsrc/main/webapp/resources/stomp.js

<scriptsrc="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>

<scriptsrc="/stockticker/resources/stomp.js"></script>

<scriptsrc="https://code.jquery.com/jquery-1.11.0.min.js"></script>


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

functionrenderPrice(frame) {

  varprices = JSON.parse(frame.body);

  $('#price').empty();

  for(variinprices) {

    varprice = prices[i];

    $('#price').append(

      $('<tr>').append(

        $('<td>').html(price.code),

        $('<td>').html(price.price.toFixed(2)),

        $('<td>').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();

    varcode = $('.new .code').val();

    varprice = Number($('.new .price').val());

    varjsonstr = JSON.stringify({'code': code,'price': price });

    stompClient.send("/app/addStock", {}, jsonstr);

    returnfalse;

  });

});

 

// Register handler for remove all button

$(document).ready(function() {

  $('.remove-all').click(function(e) {

    e.preventDefault();

    stompClient.send("/app/removeAllStocks");

    returnfalse;

  });

});


Download And Try The Source Code

The source code of this demo app is available on github. Clone it using git:

?
1
git clone https://github.com/gerrytan/stockticker.git

Import it as Existing Maven Project in STS (File > Import > Existing Maven Project) and run on in-memoryTomcat 7.0.47 using following Run Configuration (Run > Run Configurations…):

tomcat-22-maven-sts

And this Tomcat container has to run on Java 7 to enable Websocket support.

tomcat-22-maven-sts-jdk



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值