EnterpriseTales

Multi-Browser-Tab-Support in Java EE 7

Arne Limburg, Lars Röwekamp

© Software & Support Media

Webentwickler kennen das Problem. Bei der Umsetzung bestimmter Use Cases (z. B. Wizards) ist es notwendig, Daten über mehrere Requests hinweg vorzuhalten. Vor Java EE 6 gab es da nur die Möglichkeit, die Daten @SessionScoped abzulegen. Wenn aber der Benutzer die Webanwendung in zwei oder mehr Browsertabs oder -Fenstern verwendet, werden diese Daten zwischen beiden Tabs oder Fenstern geteilt, was meist zu unerwünschten Effekten führt. Der mit JSF 2.0 neu hinzugekommene View Scope (den es seit JSF 2.2 auch als CDI Scope gibt) löst das Problem, solange man sich auf einer XHTML-Seite befindet. Möchte man aber Daten (innerhalb eines Tabs) über mehrere Seiten hinweg behalten und dabei nicht auf Multi-Browser-Tab-Support verzichten, muss man zu anderen Mitteln greifen. Wir werden in dieser Kolumne betrachten, welche Möglichkeiten es da gibt und worauf man dabei achten muss.

Die erste Lösungsmöglichkeit für das Problem, Daten zwar über mehrere Seiten hinweg vorzuhalten, aber sie dennoch nicht in die Session zu legen, kam mit Java EE 6 und CDI, und zwar mit dem Conversation Scope. Die Idee ist einfach: Über ein Objekt namens Conversation, das man sich injizieren lassen kann, kann gesteuert werden, wann der Lebenszyklus der @ConversationScoped Beans beginnt und wann er endet (Listing 1). Daten in @ConversationScoped Beans können dann mehrere Requests überleben, müssen aber nicht die gesamte Session vorgehalten werden. Es handelt sich also um einen Scope, der länger als ein Request (und auch länger als eine View) andauert, aber kürzer als eine Session.

Technisch wird die Conversation über einen Request-Parameter namens cid gesteuert. Dieser gibt an, in welcher Conversation sich ein Request befindet. Auf dieser Basis können dann die jeweils richtigen Bean-Instanzen ausgewählt werden. Mit dem Conversation Scope kann es dem Benutzer tatsächlich ermöglicht werden, mit derselben Anwendung in mehreren Browsertabs parallel zu arbeiten und dabei auf unterschiedliche Daten zuzugreifen. Es wird in jedem Tab eine eigene Conversation gestartet, und der Server kann über die cid identifizieren, von welchem Tab ein Request gerade kommt.

@ConversationScoped
public class MyWizard {

    @Inject
    private Conversation conversation;

    public void startWizard() {
        conversation.begin();
    }

    public void endWizard() {
        conversation.end();
    }
}

Das Client-Window

Die Idee, über einen Request-Parameter den aktuellen Browsertab bzw. das aktuelle Browserfenster zu identifizieren, wurde in JSF 2.2 aufgegriffen. Es wurde das Konzept des Client-Windows eingeführt. In der Applikation kann darüber identifiziert werden, aus welchem Client-Window (aka Browsertab oder Browserfenster) heraus der aktuelle Request ausgeführt wird. Auf das Client-Window kann über die neu eingeführte Methode getClientWindow() des ExternalContexts zugegriffen werden, der vom FacesContext geholt werden kann. Identifiziert wird das Client-Window genau wie die Conversation über einen Request-Parameter. In diesem Fall heißt er jfwid. Er wird automatisch zu jedem Link auf einer JSF-Seite hinzugerendert. Zusätzlich wird für POST-Requests ein Hidden-Input gerendert, der auch die Window-ID enthält.

Um abwärtskompatibel zu bleiben, ist das Feature des Client-Windows aber per Default ausgeschaltet und muss in der web.xml über den Contextparameter javax.faces.CLIENT_WINDOW_MODE aktiviert werden. Laut JSF-Spec müssen hier die Werte none und url unterstützt werden. Es steht JSF-Implementierungen aber frei, weitere Werte zu unterstützen.

Öffnen in neuem Tab

So gut die Identifikation des Client-Windows über einen Request-Parameter auch funktioniert, hat sie doch einen Nachteil: Möchte man aus einem Tab heraus einen neuen Tab öffnen, wird der Parameter mit übertragen, und beide Tabs teilen sich eine Client-Window-ID, was gegen das Vorhaben, Browsertabs darüber identifizieren zu können, verstößt.

Entscheidet man sich bereits beim Page-Design dazu, dass ein bestimmter Link in einem neuen Tab geöffnet werden soll, so hilft ein Attribut weiter, das in JSF 2.2 neu zu h:link und h:button hinzugekommen ist. Es heißt disableClientWindow. Ist es auf true gesetzt, wird die jfwid nicht an den Link gerendert:

<h:link
    value="New Tab"
    outcome="newTab"
    disableClientWindow="true"
    target="_blank" />

Kompliziert wird das Client-Window-Handling allerdings, wenn nicht der Page-Designer, sondern der Anwender auf die Idee kommt, einen Link in einem neuen Tab zu öffnen. Es kann nämlich serverseitig nicht erkannt werden, ob der Benutzer normal mit der linken Maustaste auf einen Link geklickt hat, um den Link im aktuellen Tab zu öffnen, oder ob er (z. B. über einen Klick auf die rechte Maustaste und das Kontextmenü) den Link in einem neuen Tab öffnet. Der Web-Request, der vom Browser abgesendet wird, ist in beiden Fällen identisch.

Hier hilft JavaScript weiter, und eine bereits fertige Lösung für das Problem liefert die Open-Source-Bibliothek Apache DeltaSpike, die seit Kurzem in der Version 1.0.1 verfügbar ist.

Zum einen macht DeltaSpike das Feature des Client-Windows auch für alle JSF-Versionen ab 2.0 verfügbar, zum anderen werden auch zwei weitere Client-Window-Modi eingeführt: LAZY und CLIENTWINDOW. Im Modus LAZY wird im Prinzip derselbe Mechanismus verwendet, den auch JSF ab Version 2.2 wie beschrieben verwendet, nur dass der Request-Parameter hier dswid heißt. Zusätzlich wird die dswid aber clientseitig (über JavaScript) im window.name gespeichert. Jene Variable ist pro Tab/Fenster eindeutig und wird beim Öffnen in einem neuen Tab auch nicht vererbt.

Wird also ein GET-Request ohne dswid abgesetzt, kann über die Variable window.name clientseitig überprüft werden, ob bereits eine Window-ID für den aktuellen Tab vergeben ist. Ist das der Fall, wird sofort ein Seiten-Reload ausgelöst und derselbe GET-Request erneut abgesetzt, diesmal mit der korrekten dswid. Umgekehrt, wenn ein Link, der eine dswid enthält, in einem neuen Tab geöffnet wird, ist dort der window.name leer. Auch dann findet sofort ein Reload statt, diesmal nur ohne dswid im Request, wodurch serverseitig eine neue dswid generiert wird.

Die Erkennung verschiedener Tabs funktioniert durch diesen Mechanismus recht zuverlässig. Das einzige Problem bei dem Vorgehen ist lediglich, dass es in dem zweiten beschriebenen Fall vorkommen kann, dass genau für die Länge eines Requests zwei Tabs dieselbe Window-ID haben, weil der Mechanismus ja erst nach dem Request feststellt, dass es sich um einen neuen Tab handelt und dann den Reload anstößt. Daher auch der Name LAZY. Falls in diesem Request serverseitig Beans verändert wurden, kann das zu unerwartetem Verhalten führen.

Abhilfe schafft hier der zweite Modus, den DeltaSpike mitliefert: CLIENTWINDOW. Im Prinzip verhält er sich wie der Modus LAZY, nur dass er bei jedem GET-Request zunächst eine kleine Zwischenseite ausliefert, die die Prüfung übernimmt, ob es sich um einen neuen oder einen bekannten Tab handelt. Erst wenn diese Erkennung erfolgt ist, wird der tatsächliche JSF-Request abgesetzt. Mit diesem Vorgehen können auch alle Problemfälle abgedeckt werden, und jeder JSF-Request wird mit der korrekten dswid ausgeführt. Nachteil ist die erwähnte Zwischenseite; sie kann beim Laden einer Seite dazu führen, dass der Benutzer ein Flackern wahrnimmt. Das kann natürlich dadurch reduziert werden, dass die Seite in der Hintergrundfarbe der Applikation ausgeliefert wird, was sich in DeltaSpike konfigurieren lässt. Wenn ein HTML5-fähiger Browser zum Einsatz kommt, geht JavaScript sogar noch weiter. Es speichert die vorherige Seite im Local Storage und zeigt sie dann auf der Zwischenseite an.

Auf Basis dieses Window-Handlings bietet DeltaSpike dann diverse CDI Scopes an, mit denen pro Tab gearbeitet werden kann, z. B den Window Scope, den ViewAccessScope und den Grouped Conversation Scope.

Fazit

Die ideale Lösung, die dafür sorgt, dass bei jedem Request der korrekte Browsertab ermittelt werden kann, gibt es aktuell noch nicht. Das liegt daran, dass die Browser die Info „Die Antwort dieses Requests wird in einem neuen Fenster geöffnet“ leider nicht mit zum Server schicken. Eine Erkennung des korrekten Client-Windows erfordert daher häufig mindestens einen Zusatz-Request.

Die hier vorgestellte Lösung der Open-Source-Bibliothek Apache DeltaSpike liefert dazu das passende Framework, mit dem man sich dann darauf verlassen kann, dass man sich im richtigen Tab befindet.

Bleibt zu hoffen, dass ein solcher Mechanismus Einzug in den nächsten JSF-Standard hält, oder noch besser, dass in der nächsten HTML-Spezifikation Platz für eine Tab-/Fenstererkennung ist. Mit etwas Glück könnte die Info „Dieser Request wird in einem neuen Tab/Fenster geöffnet“ dann vom Browser mit den entsprechenden Requests mitgeschickt werden und alle hier beschriebenen Probleme würden der Vergangenheit angehören.

Geschrieben von
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Lars Röwekamp
Lars Röwekamp
Lars Röwekamp ist Gründer des IT-Beratungs- und Entwicklungsunternehmens open knowledge GmbH, beschäftigt sich im Rahmen seiner Tätigkeit als „CIO New Technologies“ mit der eingehenden Analyse und Bewertung neuer Software- und Technologietrends. Ein besonderer Schwerpunkt seiner Arbeit liegt derzeit in den Bereichen Enterprise und Mobile Computing, wobei neben Design- und Architekturfragen insbesondere die Real-Life-Aspekte im Fokus seiner Betrachtung stehen. Lars Röwekamp, Autor mehrerer Fachartikel und -bücher, beschäftigt sich seit der Geburtsstunde von Java mit dieser Programmiersprache, wobei er einen Großteil seiner praktischen Erfahrungen im Rahmen großer internationaler Projekte sammeln konnte.
Kommentare
  1. Alex2015-08-26 16:52:12

    I am using JSF 1 and MyFaces and trying to handle the usage of multiple tabs.

    I am using different foreign languages and reading these over a ResourceBundle from the properties files.

    If I perform a search, then open a new tab (second tab) on my web browser everything is OK. Then on the new tab if I click on the link representing the previous visited page, I got all my ResourceBundles null.

    try {
    ResourceBundle bundle = getBundle(FacesContext.getCurrentInstance(), getLocale(), getBundleName());
    result = bundle.getString(key);
    } catch (RuntimeException e) {
    // it comes here
    }

    Bundle is NULL because of FacesContext.

    Does everyone know how this problem can be fixed?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.