Server-Sent Events in Spring Boot

Server-Sent Events in Spring Boot

Communicating to the client when something happen in the back-end is a great way to react to changes or simply to display the latest data. In this article we’ll see how to configure and use server-sent events in Spring Boot.

What is Server-Sent Events?

Server-Sent Events, or SSE, is a technology that allows servers to push real-time client updates over a single HTTP connection. SSE establish a long-lived HTTP connection between the server and the client. Once the connection is established, the server can send data to the client anytime without requiring the client to make additional requests.

Project Dependencies

For this project we’ll need the following dependencies:

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>3.2.0</version>
</parent>
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-webflux</artifactId>
	</dependency>
</dependencies>

Configuring Server-Sent Events

To handle SSE we’ll be using Spring class SseEmitter, which is a specialization of ResponseBodyEmitter for sending Server-Sent Events.

@Service
public class SseEmitterService {

	private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();

	public SseEmitter add(SseEmitter emitter) {
		this.emitters.add(emitter);
		emitter.onCompletion(() -> {
			this.emitters.remove(emitter);
		});
		emitter.onTimeout(() -> {
			emitter.complete();
			this.emitters.remove(emitter);
		});
		return emitter;
	}

	public void send(SseEventBuilder obj) {
		List<SseEmitter> failedEmitters = new ArrayList<>();
		this.emitters.forEach(emitter -> {
			try {
				emitter.send(obj);
			} catch (IOException e) {
				emitter.completeWithError(e);
				failedEmitters.add(emitter);
			}
		});
		this.emitters.removeAll(failedEmitters);
	}

}

The above class is responsable to store the emitters and dispatch the events. When we add a new emitter we make sure that either on completion or on timeout we remove it from our active emitters. Everytime we send a message, if any of our emitters fail we remove them from the array and we keep only the active ones.

Now let’s create the SSE controller for handling connections.

@RestController
public class SseController {

	private final SseEmitterService sseEmitterService;

	public SseController(SseEmitterService sseEmitterService) {
		this.sseEmitterService = sseEmitterService;
	}

	@GetMapping(path = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	SseEmitter createConnection() {
		SseEmitter emitter = sseEmitterService.add(new SseEmitter(-1L));
		return emitter;
	}

}

In the above code we simply add the new emitter to our list and return it. Note two things, first that we’re setting the timeout as infinite. It’s not a problem since we’re removing it if it fails. Second, the response type is TEXT_EVENT_STREAM_VALUE, as per specification.

Now let’s create another controller, but this time for publishing the events.

@RestController
public class CategoryController {

	private final SseEmitterService sseEmitterService;

	private final List<String> categories = new ArrayList<>();
	
	public CategoryController(SseEmitterService sseEmitterService) {
		this.sseEmitterService = sseEmitterService;
	}

	@PostMapping("/categories/{category}")
	public void addCategory(@PathVariable String category) {
		categories.add(category);
		sseEmitterService.send(SseEmitter.event()
				.id(String.valueOf(categories.size()))
				.name("add-category")
				.data(categories));
	}
	
}

In the above controller we set up an endpoint, which publish an event of type add-category after adding a new category to the list.

Consuming Server-Sent Events with WebClient

Spring made it easy to consume server-sents events, all we have to do is using WebClient with Flux, which is a reactive representation of a stream of events.

@Component
public class SseConsumer {

	@PostConstruct
	void connect() {
		var uri = "http://localhost:8080/sse";
		var type = new ParameterizedTypeReference<ServerSentEvent<String>>(){};
		Flux<ServerSentEvent<String>> eventStream = WebClient.create()
				.get()
				.uri(uri)
				.accept(MediaType.TEXT_EVENT_STREAM)
				.retrieve()
				.bodyToFlux(type)
				.retryWhen(Retry.backoff(3, Duration.of(10, ChronoUnit.SECONDS)));
		eventStream.subscribe(content -> {
			System.out.println("Event Content: "+content);
		}, error -> {
			System.out.println("Event Error: "+error);
		});
	}

}

In the above class we establish a connection to the endpoint we previosuly created. In case it fails, we specify retry strategy, which in this case is backoff, with 3 maximum attempts and a minimum backoff of 10 seconds. Then we subscribe to the stream, handling both the event and the error.

Consuming Server-Sent Events with Browser

Handling events on the front-end side is easy as well. You just need to establish a connection and handle the incoming data. First, in your resources folder create a folder called static. Inside it, create an index.html and paste the following code:

<!DOCTYPE html>
<html>
  <head>
    <title>Page Title</title>
  </head>
  <body>
    <ul id="list"></ul>
    <script>
      const evtSource = new EventSource("http://localhost:8080/sse");
      evtSource.addEventListener("add-category", event => {
        const newElement = document.createElement("li");
        const eventList = document.getElementById("list");
        newElement.textContent = `message: ${event.data}`;
        eventList.appendChild(newElement);
      })
    </script>
  </body>
</html>

The above code simply define an event source and listen to the add-category events. Every time an event occur we add a new element to the list, showing the data.

Testing the Application

Now let’s run the app and see if we can establish a connection and display the events. Open your browser and access http://localhost:8080. After that, open Postman and make a POST request to http://localhost:8080/categories/test.

Now in your console you should see something like this:

Event Content: ServerSentEvent [id = '1', event='add-category', retry=null, comment='null', data=["test"]]

While on your browser this:

Server-Sent Events in Spring Boot

Conclusion

In this article we’ve seen the main concepts of SSE and how it works with Spring Boot. With few lines of code we built a good solution which can be used to notify about running jobs, processed documents or any other kind of event. The source code of this project is available on GitHub.

Lorenzo Miscoli

Software Developer specialized in creating and designing web applications. I have always loved technology and dreamed of working in the IT world, to make full use of my creativity and realize my ideas.
Scroll to Top