The Trade Monitor example demonstrates the low latency that can be achieved by using Comet and GWT event handling along with with JavaScript integration with Dojo Cometd.
This is basically identical to the Chat Example's
web.xml. The Spring JS ResourceServlet is defined to serve JavaScript files
like Dojo and the Dojox library. The SpringContinuationCometdServlet handles
Bayeux publish and subscribe requests and is mapped to '/cometd/*'.
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>monitor</display-name>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/web-application-context.xml
</param-value>
</context-param>
<filter>
<filter-name>encoding-filter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Serves static resource content from .jar files such as spring-faces.jar -->
<servlet>
<servlet-name>resources</servlet-name>
<servlet-class>org.springframework.js.resource.ResourceServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.springbyexample.cometd.continuation.SpringContinuationCometdServlet</servlet-class>
<init-param>
<param-name>asyncDeliver</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>trade-monitor</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<!-- Map all /resources requests to the Resource Servlet for handling -->
<servlet-mapping>
<servlet-name>resources</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>cometd</servlet-name>
<url-pattern>/cometd/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>trade-monitor</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
The context:component-scan registers the bayeux server TradeMonitorService and
the bayeux bean configures the Bayeux instance used by the Bayeux services and servlet.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="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">
<context:component-scan base-package="org.springbyexample.springindepth.web.cometd" />
<bean id="bayeux"
class="org.springbyexample.cometd.continuation.SpringContinuationBayeux"
p:timeout="300000"
p:interval="0"
p:maxInterval="10000"
p:multiFrameInterval="2000"
p:logLevel="0"
p:directDeliver="true">
</bean>
</beans>
The Trade Monitor Bayeux Service is just a test class that randomly generates different buys and sells for any registered symbols, keeps track of the summary information for each symbol, and publishes the summary information to anyone subscribed to the symbols channel.
The TradeMonitorService is annotated with @Component so it is
registered as a bean by the context:component-scan in the bayeux-context.xml.
It's constructor is marked with @Autowired the Bayeux created in the XML configuration file
will be injected into the constructor. There is also an initialization method with @PostConstruct on it
that means it should be called during Spring's initialization lifecycle. The init() adds the
starting prices and ranges for symbols. It also initializes the timer to broadcast the server list used for
the menu and also to publish the randomly generated trades.
Example 7.11. TradeMonitorService Initialization
Excerpt from src/main/java/org/springbyexample/springindepth/web/cometd/monitor/TradeMonitorService.java
@Component
public class TradeMonitorService extends BayeuxService {
...
/**
* Constructor
*/
@Autowired
public TradeMonitorService(Bayeux bayeux) {
super(bayeux, "monitor");
}
/**
* Init sending monitor test messages.
*/
@PostConstruct
protected void init() {
Bayeux bayeux = getBayeux();
final Channel serversChannel = bayeux.getChannel("/monitor/servers", true);
final Client client = getClient();
addChannel(NYSE_GATEWAY, ATT_SYMBOL, "AT&T", 24, 20, 25);
addChannel(NYSE_GATEWAY, GM_SYMBOL, "General Motors", 5, 5, 9);
addChannel(NYSE_GATEWAY, IBM_SYMBOL, IBM_SYMBOL, 80, 80, 100);
addChannel(NYSE_GATEWAY, MS_SYMBOL, "Morgan Stanley", 17, 10, 26);
addChannel(NYSE_GATEWAY, NYX_SYMBOL, "NYSE Euronext", 27, 23, 32);
addChannel(NYSE_GATEWAY, PG_SYMBOL, "Proctor & Gamble", 63, 57, 64);
addChannel(NASDAQ_GATEWAY, JAVA_SYMBOL, "Sun Microsystems, Inc.", 4, 4, 12);
addChannel(NASDAQ_GATEWAY, ORCL_SYMBOL, "Oracle Corporation", 17, 16, 23);
addChannel(NASDAQ_GATEWAY, GOOG_SYMBOL, "Google Inc.", 331, 330, 510);
addChannel(NASDAQ_GATEWAY, MSFT_SYMBOL, "Microsoft Corporation", 21, 21, 28);
addChannel(NASDAQ_GATEWAY, YHOO_SYMBOL, "Yahoo! Inc.", 12, 12, 20);
// start now, publish every 10 seconds
timer.schedule(new TimerTask() {
@Override
public void run() {
publishServerList(serversChannel, client);
}
}, 0, 5000);
// start now, publish every second
timer.schedule(new TimerTask() {
@Override
public void run() {
publishTrades(client);
}
}, 0, 200);
}
...
}
Trades are randomly generated, occasionally skipping some symbols, with a volume of 500-1000 and a price
movement of up to a dollar. The trade also randomly generates whether or not it is a buy or a sell.
The SummaryInfo will change the buy to a sell or a sell to a buy if the symbol
has passed it's range set during initialization.
Example 7.12. TradeMonitorService Publish Trades
Excerpt from src/main/java/org/springbyexample/springindepth/web/cometd/monitor/TradeMonitorService.java
protected void publishTrades(Client client) {
Random random = new Random();
for (Channel channel : hTrades.keySet()) {
SummaryInfo summary = hTrades.get(channel);
// randomly skip a symbols trade if it's not IBM, P&G, or Google
if (IBM_SYMBOL.equals(summary.getSymbol()) ||
PG_SYMBOL.equals(summary.getSymbol()) ||
GOOG_SYMBOL.equals(summary.getSymbol()) ||
random.nextInt(5) < 4) {
int volume = 500 + random.nextInt(500);
double price = random.nextDouble();
boolean buy = random.nextBoolean();
summary.increment(volume, price, buy);
channel.publish(client, summary.getTradeSummaryMap(), null);
}
}
}
The GWT Client uses Dojo Cometd to subscribe to the list of servers and symbols they have. These are displayed in the left hand menu. When a symbol is clicked on, this subscribes to the symbol's Bayeux channel and adds a summary widget to the screen. Whenever updates are received for any Bayeux message the JavaScript uses a GWT callback to display the information.
This adds a summary widget for the symbol that was clicked to the screen and also subscribes to it's channel.
The DeferredCommand frees up the event processing thread in the browser and queues the processing
to be done later. This can create a more responsive environment for the user by letting the browser
handle events in a more timely manner.
There is a unique key for each symbol based on the server name and symbol name. This is used to check in a
Map if there is a already a widget on the screen for this symbol or not. If there isn't,
A new SummaryWidget is created. This is the custom GWT component used to display summary information.
Besides passing in the server and symbol name, it also creates a SummaryCloseListener which handles
removing the widget and unsubscribing from the symbol's Bayeux channel if the summary window is closed.
The widget is added to the Map of widgets. This Map is used to manage the active
summary widgets. Then the method subscribes to the symbol's Bayeux channel for updates.
Example 7.13. Subscribe Code Example (excerpt from AppBase.java)
Excerpt from web/trade-monitor-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/monitor/client/AppBase.java
protected void addSummaryWidget(final String serverName, final String symbol) {
DeferredCommand.addCommand(new Command() {
public void execute() {
// server name and symbol
String key = getTradeKey(serverName, symbol);
if (!hSummaryWidgets.containsKey(key)) {
SummaryWidget summary = new SummaryWidget(serverName, symbol, key, new SummaryCloseListener(key));
centerTable.add(summary);
hSummaryWidgets.put(key, summary);
// subscribe this summary to it's feed
subscribe(getChannel(serverName, symbol));
}
}
});
}
The subscribe method passes in two callbacks to the native JavaScript's monitor.subscribe function.
The $wnd indicates to the GWT compiler that this is a call to native JavaScript. The first callback is
to display the list of servers and their symbols and the second is to display any incoming trade summary information.
Example 7.14. Subscribe Code Example (excerpt from App.java)
Excerpt from web/trade-monitor-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/monitor/client/App.java
protected native void subscribe() /*-{
// Keep reference to self for use inside closure
var app = this;
$wnd.monitor.subscribe(
function(servers) {
app.@org.springbyexample.springindepth.web.gwt.monitor.client.App::displayServers(Lcom/google/gwt/core/client/JsArray;)(servers);
},
function(trade) {
app.@org.springbyexample.springindepth.web.gwt.monitor.client.App::displayTrade(Lorg/springbyexample/springindepth/web/gwt/monitor/client/bean/TradeSummaryInfo;)(trade);
}
);
}-*/;
The subscribe function just sets the two callbacks passed from GWT to local variables
and subscribes to any existing subscriptions. The default one configured is the one with the
list of available servers and symbols for display in the menu. Each subscription
is given the callback of the monitor variable and processMessageEvent function.
Example 7.15. Subscribe Code Example (excerpt from monitor.js)
Excerpt from src/main/webapp/js/monitor.js
subscribe: function(displayCallback, tradeDisplayCallback) {
monitor.displayCallback = displayCallback;
monitor.tradeDisplayCallback = tradeDisplayCallback;
for (var i = 0; i < monitor.subscriptions.length; i++) {
dojox.cometd.subscribe(monitor.subscriptions[i], monitor, "processMessageEvent");
}
},
When a trade summary event comes in from the server, the processMessageEvent function
in monitor.js will be called since it was registered as the callback
for all Bayeux subscriptions. It will then process the appropriate GWT callback to display the information.
If there isn't any message data, the function returns. If there is and it is the subscription to
/monitor/servers (the main server list channel), it calls the monitor.displayCallback
callback. Otherwise the monitor.tradeDisplayCallback is called to display the trade summary update for a symbol.
Example 7.16. Display Trade Summary Code Example (excerpt from monitor.js)
Excerpt from src/main/webapp/js/monitor.js
processMessageEvent: function(message) {
if (!message.data) {
return;
}
if (message.channel == "/monitor/servers") {
monitor.displayCallback(message.data);
} else {
monitor.tradeDisplayCallback(message.data);
}
}
The monitor.tradeDisplayCallback callback from monitor.js is a reference to
AppBase.displayTrade(final TradeSummaryInfo trade). It gets the key for the trade, looks up the summary widget,
and call update on it. The JavaScript data passed into GWT is mapped into TradeSummaryInfo which extends
TradeInfo, but TradeInfo extends JavaScriptObject. Mapping JavaScript data to a GWT class
is just creating native methods that return the correct value from the JavaScript data instance
(ex: public final native String getSymbol() /*-{ return this.symbol; }-*/;).
Example 7.17. Display Trade Summary Code Example (excerpt from AppBase.java)
Excerpt from web/trade-monitor-webapp/src/main/java/org/springbyexample/springindepth/web/gwt/monitor/client/AppBase.java
public void displayTrade(final TradeSummaryInfo trade) {
String key = getTradeKey(trade.getServerName(), trade.getSymbol());
SummaryWidget summary = hSummaryWidgets.get(key);
summary.update(trade);
}
The project can be checked out from the subversion repository using this command or by using your IDE.
$ svn co http://svn.springbyexample.org/springindepth/trunk/web/trade-monitor-webapp/ trade-monitor-webapp
![]() | Running Examples |
|---|---|
Currently when making modifications to GWT classes, manually deleting the generated files from the GWT directory under src/main/webapp (ex: rc/main/webapp/org.springbyexample.springindepth.web.gwt.monitor.App/) in Eclipse is necessary even though the Groovy script in the Maven build does this. Then to run the changes, run mvn clean package jetty:run. |