Resteasy 3.5 introduced Server-Sent Events (SSE), and there weren’t any good resources for showing how to get it up-and-running, so I thought I’d put together a quick how-to guide.
Add your dependencies
Up your org.jboss.resteasy:resteasy-jaxrs
version to 4.0.0.Beta2
. This should automatically pull in a JAX-RS API version 2.1
unless you’ve specified a version of JAX-RS directly. If you have, upgrade to 2.1
. This will provide access to the SSE component of JAX-RS 2.1 (SseEventSink
, Sse
, etc.).
Define an SSE method
@Path("test") @Produces({MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_JSON}) public interface TestService { @GET @Path("{id}/events") @Produces(MediaType.SERVER_SENT_EVENTS) void subscribe(@PathParam("id") String id, @Context SseEventSink sink, @Context Sse sse);// It's ok to put JAX-RS annotations on the interface. Recommended, in fact. @POST @Path("test") TestEntity save(TestEntity testEntity); @GET @Path("{value}") @Produces({MediaType.TEXT_PLAIN}) String call(@PathParam("value") String input); }
Implement that biz:
public void subscribe(String id, SseEventSink sink, Sse sse) { service.execute( new Thread( () -> { try { sink.send( sse.newEventBuilder() .name("domain-progress") .data(String.class, "starting domain " + id + " ...") .build()); Thread.sleep(200); sink.send(sse.newEvent("domain-progress", "50%")); Thread.sleep(200); sink.send(sse.newEvent("domain-progress", "60%")); Thread.sleep(200); sink.send(sse.newEvent("domain-progress", "70%")); Thread.sleep(200); sink.send(sse.newEvent("domain-progress", "99%")); Thread.sleep(200); sink.send(sse.newEvent("domain-progress", "Done.")) .thenAccept( (Object obj) -> { sink.close(); }); } catch (final InterruptedException e) { e.printStackTrace(); } })); }
Write a test-case:
@Test void ensureSseWorks() throws InterruptedException { ResteasyWebTarget path = ((ResteasyWebTarget) webTarget).path(TestService.class).path("1/events"); SseEventSource source = SseEventSource.target(path).reconnectingEvery(10, TimeUnit.SECONDS).build(); try (SseEventSource s = source) { System.out.println("a"); s.register( e -> { System.out.println("d"); System.out.println(e.readData(String.class)); System.out.println("e"); }, System.out::println); System.out.println("b"); s.open(); System.out.println("c"); Thread.sleep(1000); } }
Experience failure
Failure 1
If you’ve done all that, it won’t work. The first error you’ll get is that your @Context SseEventSink
is null. Unhork yourself by adding
@Bean public SseEventSinkInterceptor sseEventSinkInterceptor() { return new SseEventSinkInterceptor(); }
to your Spring configuration.
Failure 2
The second failure you’ll experience is on the client-side: No MessageBodyReader for “text/event-stream”. This is fixed by adding a
@Bean public SseEventProvider sseEventOutputProvider() { return new SseEventProvider (); }
to your Spring client configuration.
Experience success
Ahh, delicious success
a b c d starting domain 1 ... e d 50% e d 60% e d 70% e d 99% e d Done. e
Sunshower-Test
Plugging Sunshower again, you can get all this goodness by annotating your test-class with @io.sunshower.test.ws.EnableJAXRS
if you have io.sunshower.test:test-ws:1.0.0-SNAPSHOT
as a dependency.