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.
Text vyšel původně na autorově webu.
V ú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ů:
- Zpracování RESTového volání a propagaci dat do Wicket komponenty na serveru.
- 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:
- Wicket komponent se zarigistruje jako observer do observable cache.
- REST kontroler vloží data do observable cache.
- 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í:
- V konstruktoru přidat na komponentu WebSocketBehavior.
- Z metody update broadcastovat IWebSocketPushMessage.
- V metodě onEvent zprávu přefiltrovat.
- Nechat komponent (nebo jeho část) překreslit.
- 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:
- WicketAppFilter (JavaxWebSocketFilter)
- ObservableCache (observable cache)
- PersonPanel (WebSocketBehavior, WebSocket broadcasting)
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.