воскресенье, 22 января 2012 г.

Разбираемся в GWT JavaScript Native Interface (JSNI)

     Когда вы пишете код на Java (особенно, если это касается системного программирования), иногда вам нужно выполнить код вне JVM. Например, вам нужно получить доступ к библиотеке написанной на другом языке. Для этого в Java нужно объявить native метод и обеспечить реализацию этого метода на другом языке(например С). Этот механизм наз. Java Native Interface (JNI). Вы можете сделать тоже самое с GWT джава кодом на клиенте, только вместо кода на C, нативным языком для браузера будет JavaScript. Вот так Google придумала название JavaScript Native Interface. Иногда это очень полезно, смешать написанный вручную JavaScript в Java коде. Например, функциональность самого низкого уровня некоторых основных классов GWT написаны на JavaScript. 
  Написание и использование JSNI методов является мощной техникой, но следует использовать с осторожностью. JSNI код является менее кроссбраузерным, большая вероятность утечки памяти, меньше поддается Java-инструментам и является непростой задачей для оптимизации компилятором. 
     JSNI предоставляет такие возможности как: 
- выполнение Java методов непостредственно в JavaScript
- оборачивание безопасных Java методов вокруг существующих JavaScript
- вызов JavaScript в Java коде и наоборот
- чтение и запись Java полей из JavaScript
- использование debug режима как для Java так и для JavaScript(скриптовый отладчик)
  JSNI методы объявляются как native и содержат JavaScript код в спец. формате блока комментариев, который начинается с /*-{ и заканчивается */-} . JSNI методы называются также как и любые норм. методы Java. Синтаксис JSNI является директивой для Java к JavaScript компилятору для принятия любого текста между заявленным блоком комментариев как правильный JavaScript код и внедряет его в созданные GWT файлы. Во время компиляции GWT компилятор выполняет некоторую проверку синтаксиса JavaScript кода внутри метода, затем генерирует код интерфейса для преобразования аргументов метода и возвращаемых значений. 
     Простой пример JSNI метода, который выводит JavaScript диалоговое окно:
public static native void alert(String msg) /*-{
    $wnd.alert(msg);
}-*/;

* This source code was highlighted with Source Code Highlighter.
    Обратите внимание, что код не ссылается на объект JavaScript окна непосредственно внутри метода. При обращении к окну браузера и объектам документа из JSNI, вы должны ссылаться на них как $wnd и $doc соответственно. Этот скомпилированный скрипт работает во вложенном фрейме, $wnd и $doc автоматически инициализируются правильно ссылаясь на окно и документ хостовой страницы. 
     Посмотрим на другой пример, который возбуждает исключение:
public static native int exampleThrewException() /*-{
    return "not a number";
}-*/;
try {
    int value = exampleThrewException();
    GWT.log("We got a value " + value, null);
} catch(Exception ex) {
    GWT.log("JSNI method exampleThrewException() threw an exception", ex);
}

* This source code was highlighted with Source Code Highlighter.
     Этот пример компилируется как Java, а его синтаксис проверяется GWT компилятором как валидный JavaScript. Но если запустить пример в режиме разработки он выбросит след. исключение.
com.google.gwt.dev.shell.HostedModeException: Something other than an int was returned from JSNI method '@com.dmitrynikol.webstorage.gwt.client.WebStorageGwtApp::exampleThrewException()': JS value of type string, expected int
    at com.google.gwt.dev.shell.JsValueGlue.getIntRange(JsValueGlue.java:266)
    at com.google.gwt.dev.shell.JsValueGlue.get(JsValueGlue.java:144)
    at com.google.gwt.dev.shell.ModuleSpace.invokeNativeInt(ModuleSpace.java:247)
    at com.google.gwt.dev.shell.JavaScriptHost.invokeNativeInt(JavaScriptHost.java:75)
    …....

* This source code was highlighted with Source Code Highlighter.
  В этом случае, ни Java IDE, ни GWT компилятор не скажет вам, что существует несоответствие между типом кода внутри JSNI метода и Java декларацией. Сгенерированный код интерфейса на GWT поймал проблему в рантайме в режиме разработки. При запуске кода в рабочем режиме исключение увидеть не можно, т.к. динамическая типизация JavaScript скрывает проблемы такого рода. 
     Как получить доступ к Java-методам и полям из JavaScript?
     Бывает очень полезно манипулировать Java объектами в пределах JavaScript реализации JSNI метода. Но, поскольку JavaScript использует динамическую типизацию, а Java использует статическую типизацию, вы должны использовать специальный синтаксис.
     Вызов Java-методов из JavaScript
     Вызов Java-методов из JavaScript похожа на вызов Java-методов из C в JNI. JSNI заимствует JNI подход сигнатуры метода для различения перегруженных методов. Вызовы JavaScript в Java методах имеет след. вид:
     [instance-expression]@class-name::method-name(param-signature)(arguments) 
- instance-expression - должен присутствовать при вызове метода экземпляра и должен отсутствовать при вызове статического метода
- class-name - полное имя класса в котором объявляется метод
- param-signature - внутренняя сигнатура Java метода определенная в JNI Type Signatures, но без задней сигнатуры возвращаемого типа метода, т.к. в нем нет необходимости при выборе перезагрузки
- arguments - список аргументов для передачи в вызываемый метод
     Вызов Java конструктора из JavaScript
Сравним на примере Java выражения против JSNI выражений:
class Main {
    public Main() {/** */}
    public Main(int i) {/** */}

    static class StaticInner {
        public StaticInner() {/** */}
    }

    class TestInner {
        public TestInner(int i) {/** */}
    }
}

* This source code was highlighted with Source Code Highlighter.
- new Main() equals
     @com.dmitrynikol.webstorage.gwt.client.Main::new()()
- newStaticInner() equals
     @com.dmitrynikol.webstorage.gwt.client.Main.StaticInner::new()()
- mainInstance.new TestInner(135) equals
     @com.dmitrynikol.webstorage.gwt.client.Main.TestInner: :new(Lcom/dmitrynikol/webstorage/gwt/client/Main;I)(mainInstance,135)
     Доступ к Java полям из JavaScript 
     Рассмотрим на примере доступ к статическим полям и полям экземпляра класса из JSNI:
class JSNI {
    String instanceField = "test";
    static double staticField = 12345;

    public void instanceMethod(String value) {
        /** just use 'value' */ Window.alert("execute instanceMethod");
    }

    public static void staticMethod(String value) {
        /** just use 'value' */ Window.alert("execute staticMethod");
    }

    /**
     * different situation of accessing Java fields from JavaScript by JSNI
     */
    public native void execute(JSNI jsni, String value) /*-{
        // call instance method runInstanceMethod() on this
        this.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceMethod(Ljava/lang/String;)(value);
        
        // call instance method runInstanceMethod() on jsni
        jsni.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceMethod(Ljava/lang/String;)(value);
        
        // call static method staticMethod()
        @com.dmitrynikol.webstorage.gwt.client.JSNI::staticMethod(Ljava/lang/String;)(value);
        
        // read instance field on this
        var valueFromInstanceField = this.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceField;
        $wnd.alert(valueFromInstanceField);
        
        // write instance field on jsni
        jsni.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceField = value + " value";
        $wnd.alert(jsni.@com.dmitrynikol.webstorage.gwt.client.JSNI::instanceField);
        
        // access to static field (without qualifier as you can see)
        $wnd.alert(@com.dmitrynikol.webstorage.gwt.client.JSNI::staticField + 1);
    }-*/;
}

* This source code was highlighted with Source Code Highlighter.
     Вызов Java метода из JavaScript
   Иногда необходимо получить доступ к методу или конструктору определенному в GWT во внешнем JavaScript коде. Этот код может быть написанный вручную и включен в другой js файл или это может быть частью сторонней библиотеки. В этом случае, GWT компилятор не сможет создать интерфейс между пользовательским JavaScript кодом и сгенерированным JavaScript через GWT напрямую. Эту взаимосвязь можно выполнить назначив метод через JSNI как внешний, глобально видимое JavaScript имя, которое может ссылаться на написанный вручную JavaScript.
.....
public static int recalculate(int period, float rate, int overtime) {
    /** ... */
}

public static native void exportStaticMethod() /*-{
    $wnd.count = $entry(@com.dmitrynikol.webstorage.gwt.client.JSNI::recalculate(IFI));
}-*/;
.....

* This source code was highlighted with Source Code Highlighter.
     Следует обратить внимание на то, что ссылка на экспортируемый метод была завернута в вызов метода $entry. Эта ф-ция гарантирует, что Java-производный метод выполняется с неперехваченным установленным обработчиком исключений.
     При инициализации приложения просто нужно вызвать ClassName.exportStaticMethod() из стартовой GWT точки. Это позволит присвоит ф-цию переменной в окне объекта наз. count.
     Параметры и возвращаемые типы JSNI методов объявлены как Java типы. Посмотрим на картинке конкретные правила того, как значения проходящие в и из JSNI кода должны быть обработаны.

     Исключения и JSNI
     Исключения могут быть брошены во время выполнения любого нормального Java кода или JavaScript кода в JSNI методе. Когда исключение генерируется в JSNI методе оно распространяется вверх по стеку вызова и улавливается в Java catch блоке. Выброшенное JavaScript исключение заворачивается в объект JavaScriptException в то время, когда оно было поймано. Этот объект обертки содержит только имя класса и описание исключения JavaScript, которое произошло. Рекомендуется обрабатывать JS исключения в JS коде, а Java исключения в Java коде. Java исключения могут безопасно сохранять идентичность распространяющихся через JSNI методы.
Например:
    1. Java метод firstExecute() вызывает JSNI метод nativeMethod()
    2. nativeMethod() внутри вызывает Java метод secondExecute()
    3. а secondExecute() бросает исключение
     Исключение из secondExecute() будет распространятся через nativeMethod() и может быть поймано в firstExecute() методе. Исключение будет сохранять свой тип и идентификацию.

3 комментария:

  1. Спасибо за хороший обзор!
    А в строке @com.dmitrynikol.webstorage.gwt.client.Main.TestInner: :new(Lcom/dmitrynikol/webstorage/gwt/client/Main;I)
    I - это такая сокращенная запись для java.lang.Integer ?

    ОтветитьУдалить
  2. да, описание сигнатуры параметров можно посмотреть в JNI Type Signatures - http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html#wp16432

    ОтветитьУдалить
  3. это же перевод статьи http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html

    ОтветитьУдалить