Push notifications with JEE 6

For most modern webapps push notifications have become a standard requirement. WebSockets would be my first choice, however it has only become available with JEE 7. It will take some time until JEE 7 compliant servers reach main stream production environments.

In the mean time we can achieve similar behavior with Servlet 3.0, which has become part of the JEE 6 specification, either by polling or long polling.

Polling relies on the client repeatedly sending requests to the server, polling for new messages. This will often result in empty responses, to avoid this unnecessary traffic we can hold on to the request until a new message is available or time-out is reached (long polling).

Effectively long polling requires the client to send a request, which is kept alive by the server until a response is available or a time out occurs. It is up to the client to initiate another request in both cases.

To do this we have to accept incoming GET requests and set asyncSupported to true. This stops the response object from being committed on method exit.

Calling startAsync() on the response returns an AsyncContext object which holds the request and response objects. The AsyncContext is registered with the Notification bean for further processing. This allows the requesting thread to be freed up immediately on method exit, instead of waiting for the response to be completed.

The HTTP code 202 is sent to the client to signal that the request has been accepted and is awaiting processing.

@WebServlet(urlPatterns = {"/longpolling"}, asyncSupported = true, loadOnStartup = 1)
public class SampleLongPollingRegistration extends HttpServlet {

    @EJB
    private SampleLongPollingNotifier notifier;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType(MediaType.TEXT_PLAIN);
        response.setStatus(202);
        response.setHeader("Pragma", "no-cache");
        response.setCharacterEncoding("UTF-8");
        response.flushBuffer();

        final AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(30000);

        notifier.addAsyncContext(asyncContext);
    }
}

From here the AsyncContext will be handled by different threads which in our case it is a triggered by a CDI event firing.
In our example we are “pushing” the same event to multiple clients, hence we registered the AsyncContext with a singleton bean.

As you can see we are listening for CDI events that fires an object of type Date. For each CDI event we loop through all registered peers and write the event to the response and complete the async request (as mentioned, it is up to the client to initiate another request).

@Singleton
public class SampleLongPollingNotifier {

    private final Queue<AsyncContext> peers = new ConcurrentLinkedQueue();

    public void notifyPeers(@Observes Date date) {
        for (AsyncContext ac : peers) {
            try {
                final ServletOutputStream os = ac.getResponse().getOutputStream();
                os.println(date.toString());
                ac.complete();
            } catch (IOException ex) {
                // Nothing ToDo: Connection was most likely closed by client.
            } finally {
                peers.remove(ac);
            }
        }
    }

    public void addAsyncContext(final AsyncContext ac) {
        ac.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) throws IOException {
                peers.remove(ac);
            }

            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                peers.remove(ac);
            }

            @Override
            public void onError(AsyncEvent event) throws IOException {
                peers.remove(ac);
            }

            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {
            }
        });
        peers.add(ac);
    }
}

To avoid unnecessary exceptions, I have added a AsyncListener to the AsyncContext that removes it in case of error or time-out.

For demonstration purposes I am using the JEE 6 @Schedule to fire CDI events every 10 seconds.

@Singleton
public class EventGenerator {
    @Inject
    private Event<Date> dateEvent;

    @Schedule(hour = "*", minute = "*", second = "*/10")
    public void fireScheduledEvent() {
        dateEvent.fire(new Date());
    }
}

The client side can be implemented easily using plain xhtml and jquery.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>JEE 6 Long Polling Sample Application</title>
        <script src="js/libs/jquery/jquery.js" />
    </head>
    <body>
        Long Polling result:
        <div id="longPollingResult"></div>;
        <script>
            var output = document.getElementById('longPollingResult');
            (function poll() {
               $.ajax({url: "http://localhost:8080/examplejee6longpolling/longpolling", 
                    success: function(data) {
                        output.innerHTML = data;
                    }, dataType: "text", complete: poll, timeout: 30000});
            })();
        </script>
    </body>
</html>

You can find the project on github.

Related resources:
JSR 315: Java Servlet 3.0 Specification
Asynchronous processing support in Servlet 3.0

Acknowledgements

I would like to thank Björn Sonntag and Robert Meyer for reviewing my posts.

2 thoughts on “Push notifications with JEE 6”

  1. Hi Stephan,

    I think your ajax call is missing one property (async: true), otherwise it will only show the first date.

Leave a Reply to sknitelius Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.