GWT: JsInterop – JavaScript Interoperability


Mit GWT lassen sich Frontend-Webanwendungen über die Programmiersprache Java entwickeln. Der Code wird hierbei in Java geschrieben und anschließend nach JavaScript compiliert. Dies bietet eine Reiche von Vorteilen, welche weit über die Typsicherheit von Java hinaus gehen. Nichtsdestotrotz gibt es auch eine Reihe von Nachteilen. In frühen Versionen war vor allem die Interoperabilität mit Funktionen und Bibliotheken aus anderen JavaScript-Quellen ein Fallstrick. Der Umgang mit JSNI-Methoden ist nicht gerade simpel und über das Debuggen möchte ich gar nicht erst reden. Seit Version 2.8.0 stehen Entwicklern mit JsInterop jedoch Tools bereit, welche die Interoperabilität erheblich erleichtern.

Um die Funktionalität von JsInterop zu verdeutlichen beginnen wir mit einem kleinen Beispiel, welches eine JavaScript-Funktion über JSNI ausführt. Innerhalb der Funktion wird eine Klasse instantiiert und eine Klassenmethode aufgerufen. Die Methode selbst gibt eine Warnmeldung auf dem Browser aus. Wer Probleme hat die JsInterop-Annotationen zu verwenden und undefined-Warnungen erhält, der sollte -generateJsInteropExports in seinen Dev-Parametern setzen.

package com.examples.client;
public class AlertSystem {

    public String message;

    public void alert() {
        Window.alert(message);
    }
}
    public static native void alert(String msg) /*-{
        var alertSystem = @com.examples.client.AlertSystem::new()();
        alertSystem.@com.examples.client.AlertSystem::message = msg;
        alertSystem.@com.examples.client.AlertSystem::alert()();
    }-*/;

@JsType

Der JS-Code im oberen Beispiel kann dank JsInterop erheblich vereinfacht werden. Dazu annotieren wir die obere Klasse mit @JsType. Die Annotation sagt dem Kompiler, dass jede Instanz der Klasse mit den original Methoden und Parameternamen zu erstellen ist.

package com.examples.client;
@JsType
public class AlertSystem {

    public String message;

    public void alert() {
        Window.alert(message);
    }
}

Die Klasse AlertSystem ist nun unter com.examples.client.AlertSystem zu erreichen. Im normalen JavaScript ist das nicht nötig, innerhalb von JSNI müssen wir jedoch darauf aufpassen über $wnd auf den namespace zurück zu greifen.

public static native void alert(String msg) /*-{
    var alertSystem = new $wnd.com.examples.client.AlertSystem();
    alertSystem.message = msg;
    alertSystem.alert();
}-*/;

Unser AlertSystem kann nun wie eine Java-Klasse direkt in JavaScript benutzt werden. Wir müssen allerdings darauf aufpassen, dass der Kontrakt, welcher für JavaScript erstellt wurde nur für AlertSystem gilt. Leitet eine Klasse AlertSystem ab oder erweitert sie, dann sind die neuen Methoden nicht ohne eine Annotation in JavaScript verfügbar. Ein Überschreiben der Methoden ist jedoch ohne erneute Annotation möglich.

@JsProperty, @JsMethod und @JsConstructor

Mit den Annotationen @JsProperty, @JsMethod und @JsConstructor können wir die Verfügbarkeit nach eigenen Wünschen verfeinern. @JsType macht letztendlich nichts anderes, als diese Annotationen auf alle öffentlichen Methoden und Member einer Klasse anzuwenden. Möchten wir das selbst bestimmen, so können wir die Annotationen selbst setzten. Für die letztendliche Kompilation ist das kein Unterschied.

package com.examples.client;
public class AlertSystem {

    @JsConstructor
    public AlertSystem() {
    }

    @JsProperty
    public String message;

    @JsMethod
    public void alert() {
        Window.alert(message);
    }
}

name und namespace

Die Annotationen @JsProperty, @JsMethod und @JsType können mit dem Attribut name umbenannt werden. In JavaScript können diese dann über den neuen Namen erreicht werden. @JsType kann dabei ohne Probleme mit anderen Annotationen kombiniert werden.

@JsType kann zusätzlich über das Attribute namespace das package umbenennen.

package com.examples.client;
@JsType(name="WarningSystem", namespace = "examples")
public class AlertSystem {

    @JsProperty(name="msg")
    public String message;

    @JsMethod(name="warn")
    public void alert() {
        Window.alert(message);
    }
}
public static native void alert(String msg) /*-{
    var alertSystem = new $wnd.examples.WarningSystem();
    alertSystem.msg = msg;
    alertSystem.warn();
}-*/;

Native Implementierung

Die Annotation @JsType erstellt wie oben beschrieben einen Kontrakt zwischen der Welt von java und JavaScript. Die Implementierung des Kontraktes muss jedoch nicht in Javascript erfolgen. Eine Implementierung in JavaScript ist genauso möglich. Das erlaubt es uns ganze Klassen in JavaScript zu schreiben und diese anschließend wie gewohnt in Java zu benutzen. So ist es beispielsweise möglich eine Api über JavaScript anzubinden ohne dabei auf JSNI-Methoden zurück greifen zu müssen.

package com.examples.client;
@JsType(isNative = true)
public class AlertSystem {

    public String message;

    public native void alert();
}

public static void alert(String msg){
    AlertSystem alertSystem = new AlertSystem();
    alertSystem.message = msg;
    alertSystem.alert();
}