Cometd 2.1 now supports annotations to define cometd services and clients. Annotations greatly reduces the boiler plate code required to write a cometd service and also links well with new cometd 2.x features such as channel initializers and Authorizers, so that all the code for a service can be grouped in one POJO class rather than spread over several derived entities. The annotation are some cometd specific ones, plus some standard spring annotations.
Server Side
This blog looks at the annotated ChatService example bundled with the 2.1.0 cometd release.
Creating a Service
A POJO (Plain Old Java Object) can be turned into a cometd service by the addition of the @Service class annotation:
package org.cometd.examples;
import org.cometd.java.annotation.Service;
@Service("chat")
public class ChatService
{
...
}
The service name passed is used in the services session ID, to assist with debugging.
The annotated version of the CometdServlet then needs be used and to be told the classes that it should instantiate as services and scan for annotations. This is done with a coma separated list of class names in the "services" init-parameter in the web.xml (or similar) as follows:
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.cometd.java.annotation.AnnotationCometdServlet</servlet-class>
...
<init-param>
<param-name>services</param-name>
<param-value>org.cometd.examples.ChatService</param-value>
</init-param>
</servlet>
Configuring a Channel
A service will frequently need to create, configure and Listen or subscribe to a channel. This can now be done atomically in cometd 2.x so that messages will not be recived before the channel is fully created and configured. For example the chat services configures 1 absolute channel and 2 wild card channel using the @Configure annotations:
@Configure ({"/chat/**","/members/**"})
protected void configureChatStarStar(ConfigurableServerChannel channel)
{
DataFilterMessageListener noMarkup =
new DataFilterMessageListener(_bayeux,new NoMarkupFilter(),new BadWordFilter());
channel.addListener(noMarkup);
channel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
}@Configure ("/service/members")
protected void configureMembers(ConfigurableServerChannel channel)
{
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
channel.setPersistent(true);
}
The @Configure annotation is roughly equivalent to calling the BayeuxServer#createIfAbsent method with the annotated method called as the Initializer and must take a ConfigurableServerChannel as an argument. The @Configure annotation can also take two boolean arguments: errorIfExists and configureIfExists, to determine how to handle the channel if it already exists.
The configuration methods for the chat service use the new Authorizer mechanism to define fine grained authorization of what clients can publish and subscribe to a channel. This is similar to the existing SecurityPolicy mechanism, but without the need for a centralized policy instance. An operation on a channel is permitted if it is granted by at least one Authorizer and denied by none, giving black/white list style semantics.
The configuration of the chat wildcard channels installs DataFilterMessageListeners for all /chat/** and all /members/** channels. These filters ensure that there is no markup or bad words published to these channels. To construct the listener, an instance to the BayeuxServer is needed to be passed to the constructor (used only for logging in this case). A service may obtain a reference to the BayeuxService using the @Inject annotation:
@Inject
private BayeuxServer _bayeux;
Adding a ChannelListener
A method of a service may be registered as a listener of a channel with the @Listener annotation:
@Listener("/service/members")
public void handleMembership(ServerSession client, ServerMessage message)
{
...
}
The @Listener annotation may also be passed the boolean argument receiveOwnPublishes, to control if messages published by the service session are filtered out. Note that a Listener is different to a subscription in that the service does not
subscribe to the channel, so it will not trigger any subscription
listeners nor be counted as a subscriber. There is also a @Subscription annotation available, but it is not used by the ChatService (and is typically more applicable when applied to client side cometd annotations).
Client Side
Annotations can also be used on the client side, if the java BayeuxClient is used, either for service testing or for the creation of a rich non-browser client UI:
@Service
class MyClient
{
@Session
private ClientSession session;
@PostConstruct
private void init()
{
...
}
@PreDestroy
private void destroy()
{
... }
@Listener("/meta/*")
public void handleMetaMessage(Message connect)
{
... }
@Subscription("/foo")
public void handeFoo(Message message)
{
... }
}
Note the use of @Session to inject the session used by the service and @PostConstruct and @PreDestroy for lifecycle events. These annotations are also available on the server side. On the client, the annotations are activated by an explicit call to an annotation processor:
ClientAnnotationProcessor processor = new ClientAnnotationProcessor(bayeuxClient);
...
MyClient mc = new MyClient();
processor.process(mc);
Conclusion
Annotations have made Cometd services much simpler to create and much easier to understand. Normally I’m not a big fan of annotations, as they frequently put too much configuration into the "code", but in this case, they are a perfect match for the semantic needed. In future, we’ll also look at making JAXB annotations work simply with the JSON mechanisms of cometd.