Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Reactive kliens Spring WebFlux backendhez

Szűrő megjelenítése

Reactive kliens Spring WebFlux backendhez

Az előző cikkemben a Leaflet stack állapotát monitorozó pici reactive service elkészítéséről volt szó - az endpoint már létezik (sőt, már használatban is van éles környezetben), és persze a hozzá tartozó kliens is már implementálásra került, méghozzá a rendszer management alkalmazásaként üzemelő Leaflet Management System (LMS) részeként. A koncepció mindössze annyi volt, hogy az admin felület egyik menüpontja (a dashboard) alatt a rendszer meghívja a státuszokat visszaadó reactive endpointot, mindezt az oldalletöltés akadályozása nélkül, majd egy-egy kártyán megjeleníti a futó alkalmazások verzióját - amennyiben egy adott alkalmazás éppen nem fut, úgy a kártyán egy erre utaló üzenet jelenik meg. Mivel ez a működés JavaScript használatát igényelte, UI fejlesztő kollegám segítségét kértem az admin rendszer már meglévő JavaScript magjának bővítésében. Ennek eredménye lett a hamarosan bemutatása kerülő kliens implementáció. A teljesség kedvéért azonban a cikkben egy második, Java oldali, szintén WebFlux-ra épülő klienst is bemutatok majd.

Visszatekintés az Observer tervezési mintára

Korábban hosszasan taglaltam, hogyan is működik az Observer tervezési minta, és hogyan kapcsolódik a reactive szoftverfejlesztéshez. A backend, illetve a benne implementált reactive endpointok ez esetben a Publisher szerepét töltik be - a kliens pedig ennek megfelelően a Subscriber lesz a történetben. A kliens tehát feliratkozik a backend által emittált értékekre, majd azokat a megfelelő módon feldolgozza. A feldolgozás a Reactive Stream specifikáció szerinti onNext, onError és onComplete műveletekkel történik. A folyamat természetesen a feliratkozással kezdődik, mely ez esetben a HTTP kapcsolat megnyitását jelenti a backend felé - lényegében egy standard REST kérést küldünk az endpointra, ami elindítja a streamet.

A kliens implementálása

Mivel az admin alkalmazás jelenlegi JavaScript magja NodeJS-ben van felépítve, ezért az annak megfelelő megközelítésről lesz szó az alábbiakban. Mindenekelőtt szükségünk lesz két új függésre, melyek az alábbiak:

  • EventSource, vagy annak a TypeScript kompatibilis forkja, az NGEventSource: ez a library felelős a HTTP endpoint meghívásáért.
  • RxJS: az Rx reactive extension libraryk JavaScript verziója, mellyel az emittált értékekre fog a kliensünk feliratkozni, és azokat a tetszőlegesen implementált callbackjeinkkkel kezelni.

Bár az EventSource önmagában is el tudná látni a feladatot, érdemes az RxJS segítségével egy második absztrakciós réteget implementálni, így jobban el tudjuk választani a backend felől érkező eventek feldolgozását, és a saját kliensünk reakcióit. A kliens így az alábbi lépéseket fogja végrehajtani:

  • A kliens komponens inicializálási lépése először megkeresi a dashboard UI szekcióját a DOM-ban. Amennyiben az nem található (tehát a felhasználó épp nem a dashboard oldalt nézi), az inicializálás megszakad, nem történik hívás a status service felé.
  • Ellenkező esetben történik még egy vizsgálat, hogy engedélyezve van-e a funkció, és ha igen, létrehoz egy (RxJS) Observable objektumot, majd feliratkozik rá.
  • A feliratkozás eredményeképp a status service egyenként fogja visszaadni JSON dokumentumok formájában az egyes alkalmazások állapotát. Egy-egy ilyen emittált állapot-dokumentum alapján az alkalmazáshoz tartozó állapot-kártyát frissíti a kliens.
let dashboard = document.querySelector(this.selector);

// a dashboard-on vagyunk és engedélyezett a státusz kérés?
if (dashboard && this.stackStatusConfig.enabled) {

    // observable létrehozása és feliratkozás
    // az eredmény a "status" nevű változóba kerül majd
    // az egy paraméteres subscribe metódusnak az onNext eseményre
    // adható meg eseménykezelő (lambda kifejezés formájában)
    this.createStatusObservable().subscribe(status => {

        // előre renderelt (üres) státuszkártya megkeresése...
        dashboard.querySelector(`#svcStatus_${status.app.abbreviation}`)
            
            // ... és a kártya frissítése
            .outerHTML = this.getStatusBox(status);
    });
}

A lényeg és a hangsúly persze az Observable létrehozásán van. Először is egy EventSourcePolyfill objektumot példányosítunk, mely paraméterül a status service URL-jét és néhány opcionális paramétert vár. A példányosítással a HTTP kapcsolat megnyílik, a kliens figyelni kezdi a streamet. A streamen emittált értékekre pedig két eseménykezelő (onmessage - ez az onNext megfelelője - és az onerror) fogja végrehajtani a megfelelő műveleteket. Emittált üzenet esetén a művelet értelemszerűen a státusz dokumentum deszerializálása, illetve annak emittálása a subscription felé. Hiba esetén a streamet lezárjuk. onComplete státuszra ez esetben nem volt szükség, persze, ha a kliens valamilyen speciális műveletet kell, hogy végrehajtson a stream lezárásakor, akkor implementálnunk kell egy eseménykezelőt erre az esetre is.

createStatusObservable() {
    return new Observable((observer) => {

        // HTTP kérés a status service számára
        let eventSource = new EventSourcePolyfill(this.stackStatusConfig.discoverEndpoint, this.eventSourceInitDict);

        // sikeres üzenetfogadás esetén az event változó data mezőjében
        // kapjuk meg a nyers (JSON) dokumentumot
        eventSource.onmessage = (event) => {

            // a JSON dokumentumot deszerializáljuk...
            let value = JSON.parse(event.data);

            // ... majd átadjuk az observernek
            observer.next(value)
        };

        // a 0 "readyState" azt jelenti, hogy a kliens még éppen csak csatlakozni próbált
        // ha már ekkor error váltódik ki, akkor nem is próbálkozunk tovább, zárjuk az EventSource-ot
        eventSource.onerror = () => {
            if (eventSource.readyState === 0) {
                eventSource.close();
                observer.complete();
            }
        };
    });
}

A visszatérő státuszdokumentumok formátuma az alábbi:

{
  "app": {
    "name": "Leaflet Content Front Application",
    "abbreviation": "LCFA"
  },
  "build": {
    "time": "2019-05-28T18:06:43.607Z",
    "version": "1.1.0.11"
  },
  "up": true
}

Ennek megfelelően a getStatusBox metódus az up flag alapján dönti el, milyen kártyát rendereljen a kliens (a két metódus implementációja a cikk végén linkelt repositoryban megtekinthető).

getStatusBox(status) {
    return status.up
        ? this.createSuccessfulStatusBox(status)
        : this.createFailureStatusBox(status);
}

Alternatív kliens

A Spring WebFlux nem csak szerver-oldali megoldás, hanem klienst is biztosít a WebFlux alapú reactive service-ek fejlesztéséhez. Így egységes API-t használva lehet elkészíteni a reactive kommunikációs csatornát két vagy több Spring Boot alapú alkalmazás között. Az alábbi példa a Flux API kliens oldali használatát mutatja be:

// standard Flux kliens létrehozása (érdemes beanként definiálni)
WebClient webClient = WebClient.builder()
        .baseUrl("http://localhost:8080")
        .build();

// GET hívás indítása az uri paraméterben megadott Flux endpointra
webClient.get()
        .uri("/path/to/flux/endpoint")
        .retrieve()

         // a fogadott response body deszerializálása
         // ettől a ponttól ugyanazt a Flux API-t használjuk, amit szerver oldalon is
        .bodyToFlux(TargetClass.class)

         // események kezelése
        .doOnNext(System.out::println)
        .doOnError(throwable -> System.out.println("Error: " + throwable.getMessage()))
        .doOnComplete(() -> System.out.println("No more data"))

        // a hívás a service felé csak a subscribe metódus meghívásakor történik meg!
        .subscribe();

Konfiguráció

Felmerült még egy érdekes probléma a kliens implementálása során, mégpedig az, hogy a mögöttes (Spring Boot alapú) alkalmazásból szerettem volna a beállításokat átadni számára. Szerencsére a megoldás egészen kézenfekvő, de mindenképpen említésre méltó. A Thymeleaf képes template-elni JavaScript kódot is - és ebben az esetben pont ezt a lehetőséget kell kihasználnunk. A kliens számára szükséges beállítások ez esetben egy beanből érkeznek, persze, template variable-ként is átadhatóak. A trükk a th:inline="javascript" attribútum használata egy standard, lehetőleg a headben elhelyezett script tagben.

<script type="text/javascript" th:inline="javascript">
    const stackStatusConfig = {
        enabled: /*[ [${@stackStatusConfigModel.enabled}]]*/ false,
        discoverEndpoint: /*[ [${@stackStatusConfigModel.discoverEndpoint}]]*/ null
    };
</script>

A stackStatusConfig konstans fogja majd tartalmazni a kliens beállítait, melyet a kliens komponens konstruktora fog beolvasni (alább látható majd a példa). A trükk második része az objektum fieldjeinek neve és értéke közötti, kommentnek látszó szöveg. Valójában a Thymeleaf a /*[ [ ... ]]*/ részt (szóközök nélkül, amúgy a Thymeleaf most is megpróbálta feldolgozni... :) ) prepocessor direktívaként kezeli, és a benne levő kifejezést ki fogja értékelni. Ebben az esetben egy URL és egy flag lesz az eredmény. Ha a kiértékelés sikertelen (vagy a template-et, mint natural template használjuk éppen, tehát nincs Thymeleaf feldolgozás), akkor a kifejezés jobb oldalán álló konstansok lesznek a fieldek értékei.

Mivel ez a kódrészlet csak a dashboard view file-ában található meg, a kliens komponens konstruktorát fel kellett készíteni annak hiányára:

constructor() {

    // ...

    /* eslint-disable no-undef */
    this.stackStatusConfig = typeof stackStatusConfig === 'undefined'
        ? {enabled: false}
        : stackStatusConfig;
}

Amennyiben a konstans nem létezik (tehát nem a dashboard oldalon áll a felhasználó), egy alapértelmezett objektumot adunk át, mely le fogja tiltani a status service meghívását. Ellenkező esetben használjuk a létező konstansot. (Megjegyzés: ES Lint használata esetén szükség lesz az undefined-kiértékelés feletti "kommentre", különben - a beállításoktól függően akár - a fordítás sikertelen lehet.

Teljes kliens implementáció

Kommentek
Hozzászólok

Még senki nem szólt hozzá ehhez a bejegyzéshez.