Spring Boot SSE: Real-time Updates වලට නියම විසඳුමක්! – SC Guide

Spring Boot SSE: Real-time Updates වලට නියම විසඳුමක්! – SC Guide

Spring Boot එක්ක Real-time Updates වලට Server-Sent Events (SSE) පාවිච්චි කරමු! SC Guide

අපි හැමෝම දන්නවනේ, අද කාලේ applications වලට Real-time updates නැතුව බෑ නේද? Facebook notification එකක් ආවත්, chat message එකක් ආවත්, live score එකක් බැලුවත්, මේ හැමදේටම අපිට ඕන වෙන්නේ ක්ෂණිකව update වෙන data. කලින් කාලේ මේවාට ගොඩක් වෙලාවට පාවිච්චි කළේ Polling කියන ක්‍රමය. ඒ කියන්නේ client එකෙන් server එකට නිතරම "අලුත් දේවල් තියෙනවද?" කියලා අහ අහා ඉන්නවා. හිතන්නකෝ, අපිට update එකක් එන්න අවස්ථාවක් තියෙද්දීත් අපි නිකරුනේ request කර කර ඉන්නවා කියන්නේ server එකටත් ඒක බරක්, client එකටත් නිකරුනේ resource නාස්තියක්.

ඉතින්, මේ ප්‍රශ්නෙට හොඳ විසඳුම් කීපයක්ම තියෙනවා. WebSockets කියන්නේ ඒ අතරින් ප්‍රධාන එකක්. ඒත් WebSockets වගේ advanced solutions ඕන වෙන්නේ නැති, server එකෙන් client එකට විතරක් data යවන්න ඕන අවස්ථාවලදී අපිට පාවිච්චි කරන්න පුළුවන් හරිම සරල, efficient ක්‍රමයක් තමයි Server-Sent Events (SSE) කියන්නේ. මේකේ ලොකුම වාසිය තමයි හරිම ලේසියි implement කරන්න. සාමාන්‍ය HTTP connection එකක් ඇතුලෙම මේක වැඩ කරන නිසා firewall settings එහෙම වෙනස් කරන්න යන්න ඕන වෙන්නෙත් නෑ.

අද අපි කතා කරමු Spring Boot එක්ක Server-Sent Events (SSE) කොහොමද පාවිච්චි කරන්නේ කියලා. ඔයාලා දැනටමත් Spring Boot එක්ක වැඩ කරන කෙනෙක් නම් මේක තවත් ලේසි වෙයි. හරිම ලේසියි වගේම හරිම බලගතු විදිහක් මේක Real-time updates handle කරන්න. අපි බලමුකෝ එහෙනම්, මේක කොහොමද වැඩ කරන්නේ, ඒ වගේම අපේ project එකකට මේක implement කරගන්නේ කොහොමද කියලා.

මොකක්ද මේ Server-Sent Events (SSE)?

Server-Sent Events (SSE) කියන්නේ web browser එකක් server එකකින් updates ගන්න පාවිච්චි කරන standard එකක්. සරලවම කිව්වොත්, client එක server එකට එක request එකක් යවනවා, ඊට පස්සේ server එක ඒ connection එක වහන්නේ නැතුව දිගටම client එකට data stream එකක් විදිහට යවනවා. මේක unidirectional communication (එක පැත්තකට විතරක් සිදුවන සන්නිවේදනයක්) එකක්. ඒ කියන්නේ data යවන්නේ server එකෙන් client එකට විතරයි. client එකට server එකට data යවන්න ඕන නම්, ඒකට වෙන ක්‍රමයක් පාවිච්චි කරන්න වෙනවා, නැත්නම් වෙනම request එකක් යවන්න වෙනවා.

මේක WebSockets වලට වඩා වෙනස් වෙන්නේ මෙන්න මේකයි. WebSockets කියන්නේ bidirectional communication එකක්. ඒ කියන්නේ client එකටත් server එකටත් එකම connection එකෙන් data යවන්න පුළුවන්. ඒත් SSE හදලා තියෙන්නේ server එකෙන් client එකට updates යවන්න විතරක්. ඉතින්, notification system එකක්, live feed එකක්, stock price updates වගේ දේවලට SSE හරිම ගැලපෙනවා.

SSE වල වාසි මොනවද?

  • සරලයි (Simplicity): WebSockets වලට වඩා implement කරන්න හරිම ලේසියි. සාමාන්‍ය HTTP connections විදිහටම මේක වැඩ කරන නිසා වෙනම Protocol එකක් ගැන හිතන්න ඕන නෑ.
  • නැවත සම්බන්ධ වීම (Built-in Reconnection): Client එකේ EventSource API එකට connection එකක් නැති වුණොත්, ඒක automatically reconnection කරන්න try කරනවා. මේක හරිම පහසුවක්, විශේෂයෙන්ම unstable network conditions වලදී.
  • HTTP මත පදනම් වීම (HTTP-based): HTTP Protocol එක මත පදනම් වෙලා තියෙන නිසා existing HTTP infrastructure එකත් එක්ක කිසිම ගැටළුවක් නැතුව වැඩ කරනවා. Firewalls, proxies වලට මේක අලුත් දෙයක් නෙවෙයි.
  • Standardized: W3C standard එකක් විදිහට මේක නිර්මාණය කරලා තියෙනවා.
  • සම්පත් කාර්යක්ෂමතාව (Resource Efficiency): Polling වලට වඩා ගොඩක් resource efficient. නිකරුනේ request කර කර ඉන්න ඕන නෑ. server එකෙන් data එනකොට විතරක් client එක alert වෙනවා.

සාමාන්‍යයෙන් SSE messages වලට data, event, id, සහ retry කියලා fields හතරක් තියෙනවා. මේවා text format එකෙන් එනවා. client එකේ EventSource JavaScript API එක මේ messages parse කරගෙන අපිට data access කරන්න හරිම පහසු කරනවා.

Spring Boot එක්ක SSE පාවිච්චි කරන්නේ ඇයි?

Spring Boot කියන්නේ Java developers ලට කිරි-කජු වගේ නේද? Spring ecosystem එකේ තියෙන බලය සහ පහසුව නිසා ගොඩක් අය applications develop කරන්න Spring Boot පාවිච්චි කරනවා. එහෙම තියෙද්දි, Spring Boot එක්ක SSE implementation එක හරිම පහසුයි. විශේෂයෙන්ම Spring WebFlux module එකත් එක්ක ඒක තවත් ලේසි වෙනවා.

Spring Framework එකේ reactive programming වලට තියෙන support එක නිසා SSE වගේ data streams handle කරන එක හරිම ලේසි වෙනවා. Spring WebFlux කියන්නේ Spring Ecosystem එකේ reactive web framework එක. මේක Netty වගේ non-blocking servers මත වැඩ කරන නිසා, එක thread එකකින් concurrent connections ගොඩක් handle කරන්න පුළුවන්. SSE කියන්නේ stream එකක් නිසා, reactive programming concept එක මේකට හරිම ගැලපෙනවා.

Spring Boot වල වාසි:

  • සම්බන්ධීකරණය (Seamless Integration): Spring MVC (blocking) සහ Spring WebFlux (non-blocking) දෙකටම SSE implement කරන්න පුළුවන්. produces = MediaType.TEXT_EVENT_STREAM_VALUE කියලා simple annotation එකක් දැම්මම ඇති.
  • Reactive Streams Support: Spring WebFlux වල තියෙන Flux සහ Mono කියන reactive types පාවිච්චි කරලා අපිට හරිම ලේසියෙන් data streams නිර්මාණය කරගන්න පුළුවන්. මේවා asynchronous nature එක නිසා SSE වගේ continuous streams වලට ඉතාම සුදුසුයි.
  • සරල කේතය (Concise Code): boilerplate code එක අඩුයි. හරිම ටික කේතයකින් SSE endpoint එකක් හදාගන්න පුළුවන්.
  • Robust Ecosystem: Spring ecosystem එකේ තියෙන සියලුම features, (Dependency Injection, AOP, Security etc.) SSE applications වලටත් පාවිච්චි කරන්න පුළුවන්.

ඉතින්, ඔයාලට දැනටමත් Spring Boot project එකක් තියෙනවා නම්, Real-time updates අවශ්‍ය නම්, SSE කියන්නේ බලන්නම ඕන විසඳුමක්. අපි දැන් බලමුකෝ, කොහොමද මේක implement කරන්නේ කියලා.

Project එකක් හදලා බලමුකෝ!

අපි දැන් Spring Boot project එකක් හදලා ඒකට SSE endpoint එකක් add කරලා බලමු. මේකෙන් අපිට පේනවා කාලයත් එක්ක කොහොමද server එකෙන් client එකට data stream වෙන්නේ කියලා.

පියවර 1: Spring Boot Project එකක් හදමු

මුලින්ම, අපි Spring Initializr (https://start.spring.io/) එකට ගිහින් අලුත් Spring Boot project එකක් හදමු. මේකට පහළ තියෙන Dependencies ටික add කරගන්න:

  • Spring Web (හෝ Spring Reactive Web, ඔයාලා Spring WebFlux පාවිච්චි කරනවා නම්)
  • අපි simple project එකක් හදන නිසා මේ dependency එක ඇති. Project එක generate කරලා Download කරගෙන, ඔයාලගේ IDE (IntelliJ IDEA, VS Code, Eclipse වගේ එකක) open කරගන්න.

පියවර 2: SSE Controller එකක් හදමු

දැන් අපි Spring Boot application එකට SSE endpoint එකක් add කරමු. මේ සඳහා, අපි SseController කියලා Java class එකක් හදමු. මේ controller එකේ එක @GetMapping method එකක් තියෙනවා, ඒකෙන් continuous stream එකක් විදිහට data යවනවා.

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
import reactor.util.function.Tuple2;

import java.time.Duration;
import java.time.LocalTime;
import java.util.concurrent.atomic.AtomicInteger;

@RestController
public class SseController {

    // Simple SSE endpoint sending a string every second
    @GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> sendEvents() {
        // Flux.interval(Duration.ofSeconds(1)) creates a Flux that emits a long value every second.
        // We then map this long to a string message.
        return Flux.interval(Duration.ofSeconds(1))
                .map(sequence -> "Event " + sequence + " at " + LocalTime.now());
    }

    // SSE endpoint sending custom ServerSentEvent objects every 2 seconds
    // This allows sending ID, event name, and data separately.
    @GetMapping(path = "/custom-events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<org.springframework.http.codec.ServerSentEvent<String>> sendCustomEvents() {
        return Flux.interval(Duration.ofSeconds(2))
                .map(sequence -> org.springframework.http.codec.ServerSentEvent.<String>builder()
                        .id(String.valueOf(sequence))
                        .event("custom-message") // Custom event name
                        .data("Here's a custom event: " + LocalTime.now())
                        .build());
    }

    // Example using Sinks.Many for pushing events from another part of the application
    private final Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();

    // Endpoint to subscribe to manually pushed events
    @GetMapping(path = "/manual-events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> manualEvents() {
        return sink.asFlux();
    }

    // Endpoint to trigger manual events (e.g., from an admin panel or another service)
    @GetMapping("/trigger-manual-event")
    public String triggerManualEvent() {
        sink.tryEmitNext("Manual event triggered at " + LocalTime.now());
        return "Manual event sent!";
    }
}

මේ කේතය ගැන පොඩ්ඩක් කතා කරමු. අපි @RestController annotation එක පාවිච්චි කරලා මේ class එක RESTful controller එකක් විදිහට define කරනවා. @GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE) කියන එක තමයි මෙතන ප්‍රධාන දේ. produces = MediaType.TEXT_EVENT_STREAM_VALUE කියන්නේ මේ endpoint එකෙන් එවන data stream එක SSE format එකට කියලා client එකට කියන එක.

sendEvents() method එකෙන් Flux<String> එකක් return කරනවා. Flux කියන්නේ Reactor library එකේ තියෙන type එකක්. මේකෙන් 0..N items stream කරන්න පුළුවන්. Flux.interval(Duration.ofSeconds(1)) කියන්නේ සෑම තත්පරයකටම වරක් long value එකක් generate කරන Flux එකක්. අපි ඒ value එක string එකකට convert කරලා වර්තමාන වෙලාවත් එක්ක client එකට යවනවා.

sendCustomEvents() method එකේදී අපි org.springframework.http.codec.ServerSentEvent object එකක් පාවිච්චි කරනවා. මේකෙන් අපිට event ID, event name, සහ actual data වෙන් වෙන්ව යවන්න පුළුවන්. client එකට මේවා `event.id`, `event.event`, `event.data` විදිහට access කරන්න පුළුවන්.

Sinks.Many භාවිතය වැදගත් වෙන්නේ application එකේ වෙන තැනකින් (උදා: service layer එකකින්, වෙන thread එකකින්) events push කරන්න අවශ්‍ය නම්. triggerManualEvent() වැනි method එකකින් අපිට අවශ්‍ය වෙලාවට events stream එකට inject කරන්න පුළුවන්.

පියවර 3: Client-side එකෙන් කොහොමද ගන්නේ?

දැන් අපි SSE endpoint එකක් හැදුවානේ. Client-side එකෙන් මේ data stream එක consume කරන්නේ කොහොමද කියලා බලමු. මේකට JavaScript වල built-in EventSource API එක පාවිච්චි කරන්න පුළුවන්. මේක හරිම සරලයි.

ඔයාලගේ project එකේ src/main/resources/static folder එක ඇතුලට index.html කියලා file එකක් හදලා මේ කේතය දාගන්න:

<!DOCTYPE html>
<html>
<head>
    <title>Spring Boot SSE Client Demo</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .event-container { border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; background-color: #f9f9f9; }
        .event-container p { margin: 5px 0; }
        .event-header { font-weight: bold; color: #333; }
    </style>
</head>
<body>
    <h1>Spring Boot SSE Demo</h1>

    <h2>/events Endpoint</h2>
    <div id="events-container-simple" class="event-container">
        <p class="event-header">Simple Events:</p>
    </div>

    <h2>/custom-events Endpoint</h2>
    <div id="events-container-custom" class="event-container">
        <p class="event-header">Custom Events:</p>
    </div>

    <h2>/manual-events Endpoint</h2>
    <div id="events-container-manual" class="event-container">
        <p class="event-header">Manual Events:</p>
    </div>
    <button onclick="triggerManualEvent()">Trigger Manual Event</button>

    <script>
        // For /events endpoint
        const simpleEventSource = new EventSource('/events');

        simpleEventSource.onmessage = function(event) {
            const el = document.createElement('p');
            el.textContent = 'Received: ' + event.data;
            document.getElementById('events-container-simple').appendChild(el);
            simpleEventSource.readyState === EventSource.CLOSED ? console.log('Simple EventSource closed') : null;
        };

        simpleEventSource.onerror = function(error) {
            console.error("Simple EventSource failed:", error);
            simpleEventSource.close(); // Important to close on error if not handled specifically
        };

        // For /custom-events endpoint
        const customEventSource = new EventSource('/custom-events');

        customEventSource.addEventListener('custom-message', function(event) {
            const el = document.createElement('p');
            el.textContent = `Custom Event (ID: ${event.id}, Event: ${event.event}): ${event.data}`;
            document.getElementById('events-container-custom').appendChild(el);
        });

        customEventSource.onerror = function(error) {
            console.error("Custom EventSource failed:", error);
            customEventSource.close();
        };

        // For /manual-events endpoint
        const manualEventSource = new EventSource('/manual-events');

        manualEventSource.onmessage = function(event) {
            const el = document.createElement('p');
            el.textContent = 'Manual Event Received: ' + event.data;
            document.getElementById('events-container-manual').appendChild(el);
        };

        manualEventSource.onerror = function(error) {
            console.error("Manual EventSource failed:", error);
            manualEventSource.close();
        };

        // Function to trigger manual events
        function triggerManualEvent() {
            fetch('/trigger-manual-event')
                .then(response => response.text())
                .then(data => console.log(data))
                .catch(error => console.error('Error triggering manual event:', error));
        }

    </script>
</body>
</html>

මේ කේතය ගැනත් පොඩ්ඩක් බලමු. new EventSource('/events') කියන්නේ server එකේ /events endpoint එකට connection එකක් හදනවා. EventSource API එකට events handle කරන්න methods දෙකක් තියෙනවා:

  • onmessage: මේක default event handler එක. server එකෙන් data: line එකක් එක්ක message එකක් එවන හැම වෙලාවකම මේ method එක call වෙනවා.
  • addEventListener('event-name', function): මේක පාවිච්චි කරන්නේ custom event names handle කරන්න. අපි server එකෙන් event: custom-message කියලා යැව්වොත්, client එකේ custom-message කියලා addEventListener එකක් දාන්න පුළුවන්.
  • onerror: connection එකේ මොනවා හරි error එකක් ආවොත් මේ method එක call වෙනවා.

පියවර 4: Project එක Run කරලා බලමු!

දැන් ඔයාලගේ Spring Boot application එක run කරන්න. (Main class එකේ main method එක run කරන්න).

ඊට පස්සේ, ඔයාලගේ web browser එක open කරලා http://localhost:8080 (හෝ ඔයාලගේ application එක run වෙන port එක) කියන URL එකට යන්න. එතකොට ඔයාලට පෙනෙයි සෑම තත්පරයකටම වරක් server එකෙන් එන updates browser එකේ display වෙනවා. DevTools වල Network tab එක බලලා, XHR/Fetch filter එක අයින් කරලා, EventStream එක බලන්න පුළුවන්. ඒකෙ Headers, Response එහෙම check කරන්න පුළුවන්.

Manual events trigger කරන්න, "Trigger Manual Event" button එක click කරන්න. එතකොට /trigger-manual-event endpoint එකට request එකක් ගිහින්, ඒකෙන් /manual-events stream එකට event එකක් push කරනවා.

මතක තියාගන්න ඕන දේවල් සහ Tips

අපි දැන් Spring Boot එක්ක SSE පාවිච්චි කරන්නේ කොහොමද කියලා කෝඩ් කරලාම බැලුවනේ. හැබැයි තවත් පොඩි පොඩි දේවල් ටිකක් තියෙනවා, හොඳටම වැඩ කරන application එකක් හදනකොට මතක තියාගන්න ඕන.

1. Reconnection Logic

EventSource API එකේ built-in reconnection logic එකක් තියෙනවා. connection එක disconnect වුණොත්, ඒක automatically reconnect වෙන්න try කරනවා. Server එකට පුළුවන් retry: [milliseconds] කියලා message එකක් යවලා client එක reconnect වෙන්න ඕන වෙලාව control කරන්න. නමුත් සාමාන්‍යයෙන් default behavior එක හොඳටම ඇති.

2. Error Handling

Server-side එකේදී errors handle කිරීම ගොඩක් වැදගත්. Flux එකකදී, doOnError, onErrorResume වගේ operators පාවිච්චි කරලා errors catch කරලා, stream එක නිසි විදිහට terminate කරන්න පුළුවන්, නැත්නම් default value එකක් return කරන්න පුළුවන්. Client-side එකේ EventSource.onerror event handler එකෙන් client එකට එන errors handle කරන්න පුළුවන්.

3. Keep-alive Messages (Heartbeats)

සමහර වෙලාවට proxies, load balancers, හෝ firewalls අක්‍රියව පවතින connections disconnect කරන්න පුළුවන්. මේක වළක්වන්න, අපිට regular intervals වලට "keep-alive" messages (heartbeats) යවන්න පුළුවන්. මේවා සාමාන්‍යයෙන් data නැති comment lines (:) විදිහට යවනවා. Flux.interval එකක් පාවිච්චි කරලා මේවා යවන්න පුළුවන්.

@GetMapping(path = "/heartbeat-events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> sendHeartbeatEvents() {
    return Flux.interval(Duration.ofSeconds(5)) // Send something every 5 seconds
            .map(sequence -> {
                if (sequence % 10 == 0) { // Send actual data every 50 seconds
                    return "data: Actual data at " + LocalTime.now() + "\n\n";
                } else {
                    return ":ping\n\n"; // Send a comment (heartbeat) otherwise
                }
            });
}

4. Concurrency සහ Scalability

Spring WebFlux non-blocking නිසා concurrency හොඳට handle කරනවා. හැබැයි, ඔයාලා shared state එකක් maintain කරනවා නම්, thread-safety ගැන සැලකිලිමත් වෙන්න ඕන. Scalability සම්බන්ධයෙන්, SSE connections server එකේ resources පරිභෝජනය කරනවා. ගොඩක් connections තියෙනවා නම්, web server එකේ max concurrent connections සීමාවන් ගැන හිතන්න. Microservices architecture එකකදී, SSE endpoints වෙනම service එකක තියලා scalability වැඩි කරගන්න පුළුවන්. Load balancers පාවිච්චි කරනකොට, client එක එකම server එකකට යවන්න (sticky sessions) අවශ්‍ය වෙන්න පුළුවන්, ඔයාලා stateful SSE connections maintain කරනවා නම්.

5. Security

SSE endpoints secure කරන්න Spring Security වගේ frameworks පාවිච්චි කරන්න පුළුවන්. සාමාන්‍ය REST endpoints වගේම authentication සහ authorization අවශ්‍ය වෙනවා. संवेदनशील data SSE හරහා යවනවා නම් HTTPS පාවිච්චි කිරීම අනිවාර්යයි.

අවසාන වශයෙන්

අද අපි Spring Boot එක්ක Server-Sent Events (SSE) පාවිච්චි කරන්නේ කොහොමද කියලා කතා කළා. SSE කියන්නේ Real-time updates වලට හරිම සරල, efficient සහ බලගතු විසඳුමක්. විශේෂයෙන්ම server එකෙන් client එකට data updates විතරක් අවශ්‍ය වෙන scenarios වලට SSE ඉතාම හොඳින් ගැලපෙනවා. WebSockets වලට වඩා සරල වුණත්, මේකෙන් අපිට අවශ්‍ය ගොඩක් Real-time needs satisfy කරගන්න පුළුවන්.

Spring Boot, Reactive programming (Flux) වලට තියෙන support එකත් එක්ක SSE implement කරන එක හරිම ලේසියි. කෝඩ් කරද්දීත් හරිම clean විදිහට අපිට මේ stream logic එක ලියන්න පුළුවන්.

ඉතින්, ඔයාලත් මේක try කරලා බලන්න. ඔයාලගේ project වලට SSE කොහොමද ගලපගන්නේ කියලා හිතලා බලන්න. මොනවා හරි ප්‍රශ්න තියෙනවා නම්, පහළින් comment එකක් දාන්න. අපි උදව් කරන්නම්! මේ වගේ තවත් ලිපි ඔයාලට ගේන්න අපි බලාපොරොත්තු වෙනවා. Real-time updates ලෝකයේ ඔයාලගේ ගමනට මේ ලිපියෙන් පොඩි හරි උදව්වක් වෙන්න ඇති කියලා හිතනවා.

තවත් මේ වගේ අලුත් දෙයක් ගැන කතා කරමු ඊළඟ ලිපියෙන්! ඔයාලට සුභ දවසක්!