How to Build Server-Side Events with JAX-RS

Learn how to build server-side events with JAX-RS in this tutorial by Elder Moraes, the author of Java EE 8 Cookbook.

Usually, web applications rely on the events sent by the client. Basically, the server will do something only if it is asked to.

However, owing the evolution of technologies surrounding the internet (HTML5, mobile clients, smartphones, etc.), the server side had to evolve too, which gave birth to server-side events.

In this tutorial, you will learn how to use server-side events to update a user view.

Adding the Java EE dependency

Start by adding the Java EE dependency:

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

Building server-side events with JAX-RS

The first step is to build a REST endpoint to manage the server events you’ll use. To use REST, start by configuring it properly:

@ApplicationPath("webresources")
public class ApplicationConfig extends Application {

}

The following is quite a big chunk of code but fret not; the tutorial will split it up, so you can understand every part:

@Path("serverSentService")
@RequestScoped
public class ServerSentService {

    private static final Map<Long, UserEvent> POOL = 
    new ConcurrentHashMap<>();

    @Resource(name = "LocalManagedExecutorService")
    private ManagedExecutorService executor;

    @Path("start")
    @POST
    public Response start(@Context Sse sse) {

        final UserEvent process = new UserEvent(sse);

        POOL.put(process.getId(), process);
        executor.submit(process);

        final URI uri = UriBuilder.fromResource(ServerSentService.class).path
        ("register/{id}").build(process.getId());
        return Response.created(uri).build();
    }

    @Path("register/{id}")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    @GET
    public void register(@PathParam("id") Long id,
            @Context SseEventSink sseEventSink) {
        final UserEvent process = POOL.get(id);

        if (process != null) {
            process.getSseBroadcaster().register(sseEventSink);
        } else {
            throw new NotFoundException();
        }
    }

    static class UserEvent implements Runnable {

        private final Long id;
        private final SseBroadcaster sseBroadcaster;
        private final Sse sse;

        UserEvent(Sse sse) {
            this.sse = sse;
            this.sseBroadcaster = sse.newBroadcaster();
            id = System.currentTimeMillis();
        }

        Long getId() {
            return id;
        }

        SseBroadcaster getSseBroadcaster() {
            return sseBroadcaster;
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(5);
                sseBroadcaster.broadcast(sse.newEventBuilder().
                name("register").data(String.class, "Text from event " 
                                      + id).build());
                sseBroadcaster.close();
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

Here, you have a bean to manage the UI and help you with a better view of what’s happening in the server:

@ViewScoped
@Named
public class SseBean implements Serializable {

    @NotNull
    @Positive
    private Integer countClient;

    private Client client;

    @PostConstruct
    public void init(){
        client = ClientBuilder.newClient();
    }

    @PreDestroy
    public void destroy(){
        client.close();
    }

    public void sendEvent() throws URISyntaxException, InterruptedException {
        WebTarget target = client.target(URI.create("http://localhost:8080/
                                                    ch03-sse/"));
        Response response = 
        target.path("webresources/serverSentService/start")
                .request()
                .post(Entity.json(""), Response.class);

        FacesContext.getCurrentInstance().addMessage(null,
                new FacesMessage("Sse Endpoint: " + 
                response.getLocation()));

        final Map<Integer, String> messageMap = new ConcurrentHashMap<>
        (countClient);
        final SseEventSource[] sources = new 
        SseEventSource[countClient];

        final String processUriString = 
        target.getUri().relativize(response.getLocation()).
        toString();
        final WebTarget sseTarget = target.path(processUriString);

        for (int i = 0; i < countClient; i++) {
            final int id = i;
            sources[id] = SseEventSource.target(sseTarget).build();
            sources[id].register((event) -> {
                final String message = event.readData(String.class);

                if (message.contains("Text")) {
                    messageMap.put(id, message);
                }
            });
            sources[i].open();
        }

        TimeUnit.SECONDS.sleep(10);

        for (SseEventSource source : sources) {
            source.close();
        }

        for (int i = 0; i < countClient; i++) {
            final String message = messageMap.get(i);

            FacesContext.getCurrentInstance().addMessage(null,
                    new FacesMessage("Message sent to client " + 
                                     (i + 1) + ": " + message));
        }
    }

    public Integer getCountClient() {
        return countClient;
    }

    public void setCountClient(Integer countClient) {
        this.countClient = countClient;
    }

}

The UI code is a simple JSF page:

<h:body>
    <h:form>
        <h:outputLabel for="countClient" value="Number of Clients" />
        <h:inputText id="countClient" value="#{sseBean.countClient}" />

        <br />
        <h:commandButton type="submit" action="#{sseBean.sendEvent()}" 
         value="Send Events" />
    </h:form>
</h:body>

How the code works

If you remember, you started with the SSE engine, the ServerEvent  class, and a JAX-RS endpoint—these hold all the methods that you need.

Take a look at the first one :

    @Path("start")
    @POST
    public Response start(@Context Sse sse) {

        final UserEvent process = new UserEvent(sse);

        POOL.put(process.getId(), process);
        executor.submit(process);

        final URI uri = UriBuilder.fromResource(ServerSentService.class).
        path("register/{id}").build(process.getId());
        return Response.created(uri).build();
    }

The following are the important steps to remember:

  1. First thing’s first—this method will create and prepare an event to be sent by the server to the clients.
  2. Then, the just created event is put in a HashMap called POOL .
  3. Then the event is attached to a URI that represents another method in the same class (details are provided next).

Pay attention to this parameter:

@Context Sse sse

It brings the server-side events from the server context and lets you use it as you need. Of course, it is injected by CDI (yes, CDI is everywhere)!

Now take a look at the register()  method :

    @Path("register/{id}")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    @GET
    public void register(@PathParam("id") Long id,
            @Context SseEventSink sseEventSink) {
        final UserEvent event = POOL.get(id);

        if (event != null) {
            event.getSseBroadcaster().register(sseEventSink);
        } else {
            throw new NotFoundException();
        }
    }

This is the very method that sends the events to your clients. Check the @Produces  annotation; it uses the new media type SERVER_SENT_EVENTS .

The engine works, thanks to this small piece of code :

@Context SseEventSink sseEventSink

...

event.getSseBroadcaster().register(sseEventSink);

The SseEventSink  is a queue of events managed by the Java EE server, and it is served to you by injection from the context.

Then, you get the process broadcaster and register it to this sink, which means that everything that this process broadcasts will be sent by the server from SseEventSink .

Next, check the event setup :

    static class UserEvent implements Runnable {

        ...

        UserEvent(Sse sse) {
            this.sse = sse;
            this.sseBroadcaster = sse.newBroadcaster();
            id = System.currentTimeMillis();
        }

        ...

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(5);
                sseBroadcaster.broadcast(sse.newEventBuilder().
                name("register").data(String.class, "Text from event " 
                + id).build());
                sseBroadcaster.close();
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
    }

If you pay attention to this line, you’ll remember that this broadcaster was just used in the last class :

this.sseBroadcaster = sse.newBroadcaster();

Here, this broadcaster is brought by the Sse  object injected by the server.

This event implements the Runnable  interface, so you can use it with the executor (as explained before). So once it runs, you can broadcast to your clients :

sseBroadcaster.broadcast(sse.newEventBuilder().name("register").
data(String.class, "Text from event " + id).build());

This is exactly the message sent to the client. This could be whatever message you need.

Another class was also used to interact with Sse —the most important parts are highlighted below :

        WebTarget target = client.target(URI.create
        ("http://localhost:8080/ch03-sse/"));
        Response response = target.path("webresources/serverSentService
                                        /start")
                .request()
                .post(Entity.json(""), Response.class);

This is a simple code that you can use to call any JAX-RS endpoint.

And finally, here’s the most important part of this mock client :

for (int i = 0; i < countClient; i++) {
    final int id = i;
    sources[id] = SseEventSource.target(sseTarget).build();
    sources[id].register((event) -> {
        final String message = event.readData(String.class);

        if (message.contains("Text")) {
            messageMap.put(id, message);
        }
    });
    sources[i].open();
}

Each message that is broadcast is read here :

final String message = messageMap.get(i);

It could be any client you want, another service, a web page, a mobile client, or anything.

Now check your UI :

<h:inputText id="countClient" value="#{sseBean.countClient}" />
...
<h:commandButton type="submit" action="#{sseBean.sendEvent()}" value="Send Events" />

Note that using the countClient  field to fill the countClient  value in the client allows you to play around with as many threads as you want.

Also, it’s important to mention that SSE is not supported in MS IE/Edge web browsers and that it is not as scalable as web sockets. In case you want to have full cross-browser support on the desktop side and/or better scalability, then you should consider WebSockets instead. Fortunately, standard Java EE has been supporting WebSockets since 7.0.

If you found this tutorial interesting and relevant, you can read the book, Java EE 8 Cookbook for an in-depth coverage. Packed with easy-to-follow recipes, this book is your guide to becoming productive with Java EE 8.