Střípky z prototypování: WebSockets

Publikováno: 2.10.2017

V dnešním díle se podíváme na prototyp, jehož negativním výsledkem mohla být výměna GUI technologie, což by vedlo k refaktoringu cca 1/3 aplikace.

Celý článek

Text vyšel původně na autorově webu.

úvodním díle jsme se dívali na jednoduchý prototyp – jak spojit dvě etablované webové technologie: Wicket a Spring. Bylo to takové zahřívací kolo, ještě o nic nešlo. Spíše o to, připravit si prototypovací platformu, než vyřešit zapeklitý technický problém.

V dnešním díle se podíváme na prototyp, jehož negativním výsledkem mohla být výměna GUI technologie, což by vedlo k refaktoringu cca 1/3 aplikace.

Kontext

πάντα χωρεῖ καὶ οὐδὲν μένει ~ Ἡράκλειτος

Jak říká Hérakleitos z Efesu: „všechno se mění a nic nezůstává stejné“. To se tak stane, že zákazníkovi prodáte určité řešení. Všude – v odpovědi na RFQ, v kontraktu, na workshopech se zákazníkem – prezentujete, že GUI určité aplikace bude „wizard-like“.

Vyberete technologie, nastřelíte aplikační prototyp a začnete vyvíjet business features. Vývojáři studují a postupně si osvojují nové technologie. Svět je krásný…

A pak přijde UX designer a hodí vám do toho vidle. Řekne, že uživatelé milují skrolování a cool, že jsou Single Page Aplications (SPA). A vy si uvědomíte, že GUI technologie, kterou jste zodpovědně vybrali (možná i nějaká bezesná noc tam byla), se se změnou paradigmatu není schopná vyrovnat.

Use Case

Cíl tohoto prototypu byl přímočarý: zpropagovat data, která přijdou na server z bezstavové RESTové služby do prohlížeče konkrétního uživatele a překreslit určitou část obrazovky, aby se tato data zobrazila. S nadsázkou jsem tomu říkal: přidat „reactive-like“ chování.

Podmínkou samozřejmě bylo, aby zůstaly zachovány stávající technologie (tedy Spring a Wicket). Pro úplnost dodám, že Wicketovské komponenty jsou vždy stavové.

 

Implementace

Implementaci jde rozdělit do dvou kroků:

  1. Zpracování RESTového volání a propagaci dat do Wicket komponenty na serveru.
  2. Push dat ze serveru do konkrétního prohlížeče.

Observable Cache

První bod můžeme realizovat pomocí Observer patternu – do řešení přidáme další element, který zatím budeme nazývat observable cache (a více si o něm povíme v příštím díle). Observable cache nám bude fungovat jako synchronizační mechanismus:

  1. Wicket komponent se zarigistruje jako observer do observable cache.
  2. REST kontroler vloží data do observable cache.
  3. Observable cache notifikuje zaregistrované observery (wicket komponenty).

Jelikož observable cache je pro nás zatím abstraktní komponent, jehož technologie/implementace bude vybrána později (a navíc pro nás momentálně není podstatná), vytvoříme si ji pro začátek jako jednoduchou observable HashMapu. Pro začátek budeme chtít notifikace pro metody put a remove.

public class ObservableCache extends Observable {

    private Map<String, PersonInfo> cache = new HashMap<>();

    public void put(String key, PersonInfo person) {
        cache.put(key, person);
        setChanged();
        notifyObservers();
    }

    public boolean containsKey(String key) {
        return cache.containsKey(key);
    }

    public PersonInfo get(String key) {
        return cache.get(key);
    }

    public PersonInfo remove(String key) {
        PersonInfo person = cache.remove(key);
        setChanged();
        notifyObservers();

        return person;
    }
}

Následně zaregistrujeme Wicket komponentu (Panel) jako observera. Potřebná WebSocket logika půjde do metody update(). Prozatím ji necháme prázdnou, než probereme, jak se Wicket staví k WebSocketům.

public class PersonPanel extends Panel implements Observer {

    public PersonPanel(String id) {
        // Wicket stuff omitted.

        observableCache.addObserver(this);
    }
    
    @Override
    public void update(Observable observable, Object o) {
        // Wicket/WebSocket related logic
    }
}

Wicket a WebSockety

Je to taková matrjoška. Existuje WebSocket specifikace. Ta je implementována Java API for WebSocket (JSR 356). A Java API je pak obaleno Wicktovskou implementací/rozšířením. Wicketovská dokumentace je popsaná v referenční příručce v kapitole Native WebSockets. Některá témata zde chybí, ale je to dobrý začátek.

To, co Wicket k WebSocketům přidává a co také využijeme pro náš případ (a co také chybí v dokumentaci), je „broadcastování“ WebSocket zpráv. V základě to umožňuje poslat WebSocket událost všem komponentům, které mají definované WebSocketBehavior. Komponent pak může přijatou událost dále filtrovat a rozhodnout se, jestli na ni reagovat.

To, že náše aplikace bude WebSocket-ready, nám zajistí náhrada klasického WicketFiltru za JavaxWebSocketFilter:

@WebFilter(value = "/*",
    initParams = {
        @WebInitParam(name = "applicationFactoryClassName",
                      value = "org.apache.wicket.spring.SpringWebApplicationFactory")
    })
public class WicketAppFilter extends JavaxWebSocketFilter {
}

Tady trochu odbočím od WebSocketů k servletům. Aby WebSockety fungovaly, musí je podporovat servlet kontejner, ve kterém aplikace poběží (všechny moderní kontejnery by to měly umět).

V rámci popisovaných prototypů probíhá deployment jako součást buildu, do embedovaného servlet kontejneru, který nám poskytuje výborný Gradle plugin Gretty (k dispozici jsou Jetty a Tomcat). Bohužel, našel jsem tady pravděpodobně bug – WebSockety nefungují v embedovaném Jetty, takže je potřeba používat embedovaný Tomcat. (Ve standalone Jetty funguje všechno jak má, takže to bude problém Gretty.)

Zpátky k WebSocketům a Wicketu. Nyní, po notifikaci z observable cache, chceme z metody updatebroadcastovat WebSocketEvent a opět ji odchytit ve Wicket panelu, který budeme chtít překreslit. Data pro model komponentu si vytáhneme přímo z cache.

Pokud se podíváme na tento proces z hlediska kódu, potřebujeme ve Wicket komponentu aktualizovat pár věcí:

  1. V konstruktoru přidat na komponentu WebSocketBehavior.
  2. Z metody update broadcastovat IWebSocketPushMessage.
  3. V metodě onEvent zprávu přefiltrovat.
  4. Nechat komponent (nebo jeho část) překreslit.
  5. V rámci překreslení dojde ke znovu-načtení modelu.
public class PersonPanel extends Panel implements Observer {

    @SpringBean
    private ObservableCache observableCache;

    public PersonPanel(String id) {
        super(id);
        // Wicket component stuff, e.g. children
        
        setDefaultModel(new CompoundPropertyModel<>(getModel()));

        add(new WebSocketBehavior() {});

        observableCache.addObserver(this);
    }

    private IModel<PersonInfo> getModel() {
        return new LoadableDetachableModel<PersonInfo>() {
            @Override
            protected PersonInfo load() {
                return observableCache.get(personID);
            }
        };
    }

    @Override
    public void onEvent(IEvent<?> event) {
        if (event.getPayload() instanceof WebSocketPushPayload) {
            WebSocketPushPayload wsEvent = (WebSocketPushPayload) event.getPayload();
            IWebSocketPushMessage message = wsEvent.getMessage();

            if (message instanceof WebSocketPersonMessage) {
                WebSocketPersonMessage personMessage = (WebSocketPersonMessage) message;
                // do some filtering

                wsEvent.getHandler().add(wrapper);
            }
        }
    }

    @Override
    public void update(Observable observable, Object o) {
        // do some filtering

        WebSocketSettings webSocketSettings = WebSocketSettings.Holder.get(getApplication());
        WebSocketPushBroadcaster broadcaster = new WebSocketPushBroadcaster(webSocketSettings.getConnectionRegistry());
        broadcaster.broadcastAll(getApplication(), new WebSocketPersonMessage(personID));
    }
}

 

Prototype repozitory

Pokud si budete chtít prototyp spustit a trochu si s ním pohrát, naklonujte si následující Bitbucket repository. Součástí prototypu je SoapUI projekt, s připraveným REST requestem, kterým si můžete poslat data do prohlížeče.

Klíčové třídy:

Poučení

Občas se ukáže, že řešení, které jsme odkládali jako nejzažší možné, je nakonec to správné. Nebo jediné funkční. Je potřeba zvážit potenciální důsledky – např. přepisování GUI vrstvy versus pozdější perfomance problémy (kolik WebSocket spojení bude v produkci otevřených? Jaký bude objem broadcastovaných zpráv? apod.)

Zvolili jsme na počátku dostatečně flexibilní technologii, aby uspokojila i nároky v úvodu zmiňovaného Hérakleita z Efesu? (Eh, chtěl jsem říci šíleného UX designera.) Malý, rychlý prototyp může dát na tyto otázky odpověď. Nebo aspoň vyznačit cestu, kudy ne.

Příště

V pokračování střípků se podíváme na to, čím nahradit observable cache. Můžete se těšit na sebevražedný deathmatch Neo4j vs. Infinispan.

Repository všech prototypů

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace