Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
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.
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.