JAX-RS 2.1 New Feature Introduction: Server Sent Events Update
I don't know what this says about my memory, but I was doing a little JAX-RS programming today and when I googled to remind myself of the particular feature I was using, I ran into my own blog post about that feature. It took me a moment to recall that I had written that earlier blog post. I wonder if I'll forget writing this one as well?
Anyhow, both that post and the other one I found, written by JAX-RS Spec Lead Pavel Bucek <https://blogs.oracle.com/pavelbucek/jax-rs-21-server-sent-events> are now stale with respect to the final version of JAX-RS 2.1. I looked into the GlassFish Samples for a similar and simple example, but found none, so I took the opportunity to add two samples based on updated content from Pavel's and my blog posts. Please clone it to see the full source code. This blog post documents these two super simple samples.
The Simplest JAX-RS SSE Example
- @Path("/")
- public class SSESimpleResource {
- @Resource
- private ManagedExecutorService executor;
- @GET
- @Path("eventStream")
- @Produces(MediaType.SERVER_SENT_EVENTS)
- public void eventStream(
- @Context SseEventSink eventSink,
- executor.execute(() -> {
- try (SseEventSink sink = eventSink) {
- }
- }
- });
- }
- }
Line 4 is a magic injection of
the ManagedExecutorService
.
This class is only available in the full profile of Java EE, not the
web profile. This sort of service is essential for doing SSE because
it's the easiest way to hand off the socket for the incoming SSE
subscription request to a service that can handle the scale of such a
resource intensive thing.
Line 9 and neighboring annotations make it so the HTTP GET Request
to "/eventStream" will return content-type text/event-stream. Lines
11 and 12 are more magic to get handles to the JAX-RS 2.1 APIs for
SSE. SseEventSink
lets you send stuff down to the
browser. Sse
is a factory for other classes in the SSE
API. Line 14 is a try-with-resources that lets the sink be
autoclosable.
Let's take a look at the corresponding HTML.
- <script type="text/javascript">
- if(typeof(EventSource) !== "undefined") {
- var source = new EventSource("app/eventStream");
- source.onmessage = function(event) {
- document.getElementById("result").innerHTML += event.data + "<br>";
- };
- } else {
- document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
- }
- </script>
- <h1>SSE sample</h1>
- <div id="result"></div>
The JavaScript on lines 1 - 10 connects to the server when the page loads and installs an SSE event handler that appends the data of each SSE to the div on line 14.
Broadcasting to Multiple Clients
- @Path("/")
- public class SSEBroadcastResource {
- @Context Sse sse;
- private static SseBroadcaster sseBroadcaster;
- @PostConstruct
- public void postConstruct() {
- getBroadcaster(sse);
- }
- private static SseBroadcaster getBroadcaster(Sse sse) {
- if (null == sseBroadcaster) {
- sseBroadcaster = sse.newBroadcaster();
- }
- return sseBroadcaster;
- }
- @GET
- @Path("subscribe")
- @Produces(MediaType.SERVER_SENT_EVENTS)
- eventSink.send(sse.newEvent("welcome!"));
- getBroadcaster(sse).register(eventSink);
- }
- @POST
- @Path("broadcast")
- @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
- }
- }
Another example in Pavel's Blog that didn't quite work as written is
the SseBroadcaster
example. Above is one that works. Note the use of the static getBroadcaster()
method and forcing it to be pre-invoked before the resource is put into service through the use of @PostConstruct
on lines 7 - 10.
As with the first example, the GET request sets up the SSE, sending
an initial event. The new hting is handing off the servicing of the
SSE to the handy SseBroadcaster
on line 24.
This resource also listens for POST requests, on lines 27 - 32. It takes the form data and simply broadcasts it out to the SSE. The HTML is next.
- <script type="text/javascript">
- var i = 1;
- if(typeof(EventSource) !== "undefined") {
- var source = new EventSource("app/subscribe");
- source.onmessage = function(event) {
- document.getElementById("result").innerHTML += event.data + "<br>";
- document.getElementById("input").value = i++ + " " +
- navigator.userAgent;
- };
- } else {
- document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
- }
- function submit() {
- var params = "data=" + document.getElementById("input").value;
- var url = document.getElementById("form").action;
- var xhr = new XMLHttpRequest();
- xhr.open("POST", url);
- xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
- xhr.send(params);
- }
- </script>
- <form id="form" method="POST" action="app/broadcast" >
- <input id="input" type="hidden" name="data">
- </form>
Lines 1 - 12 are mostly the same as in the first example. Lines 14 -
20 are new. The submit()
function uses Ajax to send a
POST to the server (lines 23 - 28 in the Java listing). The value of
the data
form data comes from the hidden field, and
is initialized on line 7. I just
use navigator.userAgent
for novelty.
Each click of the POST
button causes a POST to be sent
to the server, which in turn causes an SSE to be sent back to the
client. For extra fun, you could open up two tabs on the same page
and see that pressing POST on one causes the data to be updated on
both pages.
Thanks to Pavel Bucek and Santigo Pericas-Geertsen for stewarding the JAX-RS community to deliver a very useful piece of Java EE 8.