The chat example allows a client to send and receive messages published to a chat channel and also to see a list of the other chat clients. The example has GWT & Dojo Cometd integration on the client and Spring Bayeux integration on the server. The GWT part of the client handles display and client interaction, but calls out to external JavaScript to let Dojo Cometd handle Bayeux publish/subscribe on the client. The Spring by Example Web Module is used for it's Bayeux support to configure the Bayeux Chat Service.
To briefly explain Comet & Bayeux, Comet is server side push basically. Instead of a standard HTTP request that makes a request and immediately receives a response, Comet either has an open connection to stream data to the client or long polling is used. Long polling, which is what the current Bayeux implementation uses, makes a request and the server doesn't respond until an event occurs on the server. Bayeux is a protocol that clearly defines a number of things, but one of it's primary goals is providing low latency over HTTP. It provides a publish/subscribe API for handling events. Bayeux also has implementation in more than just Java and JavaScript.
The chat example is based on the Dojo Cometd chat example and a slightly modified version of the example is also available for comparison in the web application. The only changes made to the Dojo Cometd example was just enough to get it integrated into the Tiles page and changing the JavaScript since the URL for subscribing to the chat service is slightly different. The actual Bayeux Chat Service, which will be shown further in the example, has only minor changes compared to the original to integrate it with Spring.
The project uses Spring by Example Web Module's
Spring Bayeux Integration for Comet on Jetty. This has support
for creating and initializing the main Bayeux
instance in Spring and then the Spring Bayeux servlet will retrieve
it from the Spring context. Also, Bayeux Services can then have the Bayeux
instance injected along with
Hibernate, Web Services, etc. A message driven POJO could even then publish a message event to Bayeux clients.
Note | |
---|---|
Currently this example only runs on Jetty since it uses the Bayeux implementation provided by Jetty. Tomcat also has Comet support and it should be easy to port a Bayeux implementation to it if one doens't already exist. There isn't currently a standard API to suspend and resume HTTP requests, but the Servlet 3.0 Draft Specification is standardizing this. Hopefully it will be finalized relatively soon and most major servlet engines will have a Servlet 3.0 implementation. Jetty already has a pre-release 7.0 version implementing the Servlet 3.0 specification as it currently is. |
This is basically the same as a standard Spring MVC web configuration, but two things to note are the Spring JS
ResourceServlet
and the SpringContinuationCometdServlet
handles Bayeux requests. The
ResourceServlet
is used for serving static files from jars. The Spring JS libraries contain Dojo, and are actually served from
there. The Dojox JavaScript libraries are in the dojox-resources.jar. The SpringContinuationCometdServlet
handles all 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>chat</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>chat</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>chat</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 will register the Bayeux Chat Service.
The bayeux bean class is SpringContinuationBayeux
.
This is the main instance that will control of Bayeux's publish and subscribe on the server.
It will be used by all Bayeux services and by the Bayeux servlet. Basic values like
timeout can be set (p-namespace is being used for setter injection, shorcut for using property element).
The Bayeux implementation also supports filtering of any messages. This example has a filter defined to not
allow any markup to be sent and also has two basic regular expression filters.
One matches 'Spring by Example' (either a capital or lower case 's' for 'spring', 'b' for 'by', and 'e' for 'example')
or 'sbe' (upper or lower case) and changes it to 'Spring by Example'. The other corrects typos for 'the' and 'spring'.
<?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.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"> <property name="filters"> <value> <![CDATA[ [ { "channels": "/**", "filter" : "org.mortbay.cometd.filter.NoMarkupFilter", "init" : {} }, { "channels": "/chat/*", "filter" : "org.mortbay.cometd.filter.RegexFilter", "init" : [ [ "[Ss]pring [Bb]y [Ee]xample","Spring by Example" ], [ "[Ss][Bb][Ee]","Spring by Example" ] ] }, { "channels": "/chat/**", "filter" : "org.mortbay.cometd.filter.RegexFilter", "init" : [ [ "teh ","the "], [ "sring ","spring "] ] } ] ]]> </value> </property> </bean> </beans>
The ChatService
is basically the same as the one from the Dojo Cometd example.
The key differences are that the class is annotated with @Component
and the
constructor is annotated with @Autowired
. So the service is picked up by the
context:component-scan because of the @Component
annotation
and the Bayeux
instance configured in the XML configuration is injected into the
constructor because of the @Autowired
annotation. At this point the class just
calls super
in it's constructor to give the Bayeux
instance to it's
parent and working on the service is the same. So it is very easy to modify any Bayeux
service example to be Spring configured and gain the full advantage of IoC and DI.
Example 1. ChatService
@Component public class ChatService extends BayeuxService { final Logger logger = LoggerFactory.getLogger(ChatService.class); final ConcurrentMap<String, Set<String>> _members = new ConcurrentHashMap<String, Set<String>>(); /** * Constructor */ @Autowired public ChatService(Bayeux bayeux) { super(bayeux, "chat"); subscribe("/chat/**", "trackMembers"); } /** * Tracks chat clients. */ public void trackMembers(Client joiner, String channel, Map<String, Object> data, String id) { if (Boolean.TRUE.equals(data.get("join"))) { Set<String> m = _members.get(channel); if (m == null) { Set<String> new_list = new CopyOnWriteArraySet<String>(); m = _members.putIfAbsent(channel, new_list); if (m == null) { m = new_list; } } final Set<String> members = m; final String username = (String) data.get("user"); members.add(username); joiner.addListener(new RemoveListener() { public void removed(String clientId, boolean timeout) { members.remove(username); logger.info("members: " + members); } }); logger.info("Members: " + members); send(joiner, channel, members, id); } } }