Building a UI with SSE support
samson baraka
Lead Software Engineer | Backend Engineer | ERP & CRM Systems Engineer | Agile Practitioner | Microservices | AI Driven Systems | Java & Spring Developer | Enterprise Application Developer | Web developer
In order to complete our use case we need an HTML page with some JavaScript code to communicate with the server. For the sake of conciseness, we will strip all HTML tags and leave only the minimum that is required to achieve a result, as follows:
<body>
<ul id="events"></ul>
<script type="application/javascript">
function add(message) {
const el = document.createElement("li");
el.innerHTML = message;
document.getElementById("events").appendChild(el);
}
var eventSource = new EventSource("/temperature-stream");
eventSource.onmessage = e => {
const t = JSON.parse(e.data);
const fixed = Number(t.value).toFixed(2);
add('Temperature: ' + fixed + ' C');
}
eventSource.onopen = e => add('Connection opened');
eventSource.onerror = e => add('Connection closed');
</script>
</body>
Here, we are using the EventSource object pointed at /temperature-stream. This handles incoming messages by invoking the onmessage() function, error handling, and reaction to the stream opening, which are done in the same fashion. We should save this page as index.html and put it in the src/main/resources/static/ folder of our project. By default, Spring Web MVC serves the content of the folder through HTTP. Such behavior could be changed by providing a configuration that extends the WebMvcConfigurerAdapter class.
Verifying application functionality
After rebuilding and completing our application's startup, we should be able to access the mentioned web page in a browser at the following address: https://localhost:8080 (Spring Web MVC uses port 8080 for the web server as the default one. However, this can be changed in the application.properties file using the configuration line server.port=9090). After a few seconds, we may see the following output:
Connection opened
Temperature: 14.71 C
Temperature: 9.67 C
Temperature: 19.02 C
Connection closed
Connection opened
Temperature: 18.01 C
Temperature: 16.17 C
As we can see, our web page reactively receives events, preserving both client and server resources. It also supports auto-reconnect in the case of network issues or timeouts. As the current solution is not exclusive to JavaScript, we may connect with other clients, for example, curl. By running the next command in a terminal, we receive the following stream of raw, but not formatted, events:
领英推荐
> curl https://localhost:8080/temperature-stream
data:{"value":22.33210856124129}
data:{"value":13.83133638119636}
Criticism of the solution
At this point, we may praise ourselves for implementing a resilient reactive application using only a few dozen code lines (including HTML and JavaScript). However, the current solution has a few issues. First of all, we are using the Publish-Subscribe infrastructure provided by Spring. In Spring Framework, this mechanism was initially introduced for handling application life cycle events and was not intended for high-load, high-performance scenarios. What would happen when we need thousands or even millions of separate streams instead of one stream of temperature data? Will Spring's implementation be able to handle such a load efficiently?
Furthermore, one significant downside of such an approach lies in the fact that we are using an internal Spring mechanism to define and implement our business logic. This leads to a situation in which some minor changes in the framework may break our application. Besides, it is hard to unit test our business rules without running the application context. It is also reasonable to mention an application that has a lot of methods that are decorated with the @EventListener annotation, and without an explicit script that describes the whole workflow in one concise piece of code.
Furthermore, SseEmitter has a notion for errors and the ends of streams, whereas @EventListener does not. So, to signal the end of the stream or error between components, we have to define some special objects or class hierarchies, and it is easy to forget about handling them. Also, such specific markers may have slightly different semantics in different situations, complicating the solution and reducing the attractiveness of the approach.
One more drawback worth highlighting is that we allocate the thread pool to asynchronously broadcast temperature events. In the case of a genuinely asynchronous and reactive approach (framework), we wouldn't have to do this.
Our temperature sensor generates only one stream of events without regard to how many clients are listening. However, it also creates them when nobody listens. That may lead to a waste of resources, especially when creation actions are resource-hungry. For example, our component may communicate with real hardware and reduce hardware lifespan at the same time.
To address all of these issues, as well as others, we need a reactive library that is specially designed for such purposes. Fortunately, we have a few of these. In the next episode, we will look at RxJava, which was the first widely adopted reactive library and changed the way we build reactive applications using Java.