Server Sent Events with Spring – Push Notifications

Introduction

We have seen a popular choice for sending real time data from server to client in web application is using WebSocket in our previous tutorials Messaging with STOMP over WebSockets using Spring and ActiveMQ and Spring Boot WebSocket AngularJS Gradle Example. WebSocket opens bidirectional connections between server and client. So both server and client can send messages. But sometimes we face situations, where the application needs only one way communication, i.e., sending data from server to the client and for this Spring provides a simpler solution using Server Sent Events (SSE). SSE is a technology that allows you to stream data from server to the browser  (Push Notifications) within one HTTP connection in one direction. For example, pushing stock price changes in real-time or showing progress of long-running process or real time showing of cricket or football scores on display board etc.

Browser Support

SSE are supported in most modern browsers. Only Microsoft’s IE and Edge browsers do not have a built in implementation. But there is a way out because Server-Sent Events uses common HTTP connections and can therefore be implemented with the following libraries to support IE and Edge browsers.

https://github.com/remy/polyfills/blob/master/EventSource.js by Remy Sharp
https://github.com/rwldrn/jquery.eventsource by Rick Waldron
https://github.com/amvtek/EventSource by AmvTek
https://github.com/Yaffle/EventSource by Yaffle

Prerequisites

Knowledge of Spring
Knowledge of Java
Have Java 8 installed and configured
Have Gradle 4 installed and configured
Eclipse IDE

Example

In the following example we create a Spring Boot application that sends the random Java’s UUID message with timestamp as SSE to the client. Ideally ou would like to display some meaningful data to the client. So you can always modify the code as per your requirements. The client is a simple html page that displays these values. Spring introduced support for Server Sent Events(SSE) with version 4.2 (Spring Boot 1.3).

First create a Gradle based spring boot project using the below build.gradle script

buildscript {
	ext {
					springBootVersion = '1.5.9.RELEASE'
	}
    repositories {
    	mavenLocal()
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'

repositories {
	mavenLocal()
    mavenCentral()
}
    
dependencies {
	compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
}

Once the project is created and build is done, create below a scheduled service that reads the greeting message every three second and creates an instance of the Notification class and publishes it with Spring’s event bus infrastructure.

package com.jeejava.service;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Service
@EnableScheduling
public class NotificationService {

	final DateFormat DATE_FORMATTER = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss a");

	final List emitters = new CopyOnWriteArrayList<>();

	public void addEmitter(final SseEmitter emitter) {
		emitters.add(emitter);
	}

	public void removeEmitter(final SseEmitter emitter) {
		emitters.remove(emitter);
	}

	@Async
	@Scheduled(fixedRate = 5000)
	public void doNotify() throws IOException {
		List deadEmitters = new ArrayList<>();
		emitters.forEach(emitter -> {
			try {
				emitter.send(SseEmitter.event()
						.data(DATE_FORMATTER.format(new Date()) + " : " + UUID.randomUUID().toString()));
			} catch (Exception e) {
				deadEmitters.add(emitter);
			}
		});
		emitters.removeAll(deadEmitters);
	}

}

We are pushing data every 5 secs to the client. For more information on @Async please read Spring Asynchronous Execution using @Async .

Next we create a RestController class that handles the EventSource GET request from the client. The get handler needs to return an instance of the class SseEmitter. Each client connection is represented with its own instance of SseEmitter. Spring does not give you tools to manage these SseEmitter instances. In this application we store the emitters in a simple list(emitters) and add handlers to the emitter’s completion and timeout event to remove them from the list.

package com.jeejava.rest.controller;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import com.jeejava.service.NotificationService;

@RestController
public class NotificationController {

	@Autowired
	NotificationService service;

	final List emitters = new CopyOnWriteArrayList<>();

	@GetMapping("/notification")
	public ResponseEntity doNotify() throws InterruptedException, IOException {
		final SseEmitter emitter = new SseEmitter();
		service.addEmitter(emitter);
		service.doNotify();
		emitter.onCompletion(() -> service.removeEmitter(emitter));
		emitter.onTimeout(() -> service.removeEmitter(emitter));
		return new ResponseEntity<>(emitter, HttpStatus.OK);
	}

}

By default, Spring Boot with the embedded Tomcat server keeps the SSE HTTP connection open for 60 seconds. An application can change that with an entry to the application.properties file

spring.mvc.async.request-timeout=-1 #-1 means infinity

We don’t want to start Tomcat server on random port so change the server port at application.properties file

server.port=9999

Create below main class to start up the application

package com.jeejava.main;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.jeejava")
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

Client Part

The client opens the SSE connection with

const eventSource = new EventSource('http://localhost:9999/notification');

and registers a message listener that parses the JSON and sets the innerHTML of three dom elements to display the received data.

The whole HTML file is given below

<!DOCTYPE html>
<html>
<head>
<title>Server Notifications</title>
<script>
	function initialize() {
		const eventSource = new EventSource('http://localhost:9999/notification');
		eventSource.onmessage = e => {
			const msg = e.data;
			document.getElementById("greet").innerHTML = msg;
		};
		
		eventSource.onopen = e => console.log('open');
		eventSource.onerror = e => {
			if (e.readyState == EventSource.CLOSED) {
				console.log('close');
			}
			else {
				console.log(e);
			}
		};
		
		eventSource.addEventListener('second', function(e) {
			console.log('second', e.data);
		}, false);               
	}
	window.onload = initialize;
</script>
</head>
<body>
	<div id="greet"></div>
</body>
</html>

Testing the application

Now while you run the client file in any modern browsers like Chrome, Firefox etc. then you may face below problem

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource spring boot html

To resolve this issue please write below servlet filter

package com.jeejava.filter;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class CorsFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
			throws ServletException, IOException {
		res.setHeader("Access-Control-Allow-Origin", "*");
		res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
		res.setHeader("Access-Control-Max-Age", "3600");
		res.setHeader("Access-Control-Allow-Headers",
				"X-PINGOTHER,Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization");
		res.addHeader("Access-Control-Expose-Headers", "xsrf-token");
		if ("OPTIONS".equals(req.getMethod())) {
			res.setStatus(HttpServletResponse.SC_OK);
		} else {
			chain.doFilter(req, res);
		}
	}
}

Now refresh the client file in browser, you will get uninterrupted message being displayed

server sent events spring

Thanks for reading.

Soumitra Roy Sarkar

I am a professional Web developer, Enterprise Application developer, Software Engineer and Blogger. Connect me on Roy Tutorials Twitter Facebook  Google Plus Linkedin

Leave a Reply

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