2014年4月24日 星期四

外部 JS 呼叫 Java static method

內容有點無腦,先把結論寫在前面:

外部 JS(官方文件稱為「handwritten JS」,其實不同 GWT Module 就滿足這個條件)要呼叫 Java 的 static method,必須先透過 JSNI 設定 $wnd.methodName = @fooPackage.FooClass::javaMethodName(*),後續使用 $wnd.methodName() 來達到目的。

關鍵在於 JSNI 中:

  • javaMethodName() 後頭不需再加 ()
  • javaMethodName() 的參數某些情況下可以省略 field descriptor,直接用 * 代替。

update:感謝 darkk6(ptt.cc)提醒,讓我發現我不但死腦筋,而且還少測了一種寫法… 所以文章就要重新翻修了 (艸

前天被問到一個問題:「同一個 host page 上兩個不同 GWT Module 要怎麼溝通?」我原本以為這不會是問題,結果回到家實際測了一下才發現根本不是這麼一回事 [死]。一言以蔽之,可以用這個 stackoverflow 來解釋。上頭發問的內容大抵上就是我原本的想法:用 event bus 來解,結果是不可行的,不過那不是這篇文章的重點 XD

為了保險起見實際測試了一下解答的 code,然後就發現根本行不通:

private native static exportMyJavaMethod() /*-{
    $wnd.myJavaMethod = @my.package.module1.MyClass1::myJavaMethod;
}-*/;

那個 myJavaMethod 沒給參數的 field descriptor,GPE 就報 syntax error、development mode 也一樣炸。回頭去確認官方文件,沒想到官方文件的程式碼一樣微妙……

package mypackage;

public MyUtilityClass{
    public static int computeLoanInterest(int amt, float interestRate, int term) { ... }

    public static native void exportStaticMethod() /*-{
        $wnd.computeLoanInterest = $entry(
            @mypackage.MyUtilityClass::computeLoanInterest(IFI)
        );
    }-*/;
}

我不太確定那個 IFI 是啥意思 or 啥縮寫…… ==”

好,不管這些,自己重新寫一個程式測試這個部份:

package foo.client;
//import 略
public class FooEP implements EntryPoint {
    @Override
    public void onModuleLoad() {
        exportJsMethod();
        callFooMethod("...");
    }

    static void javaMethod(String arg1) {
        Window.alert("javaMethod : " + arg1);
    }

    native static void exportJsMethod() /*-{
        $wnd.fooMethod = @foo.client.FooEP::javaMethod(Ljava/lang/String;)();
    }-*/;

    native static void callFooMethod(String message) /*-{
        $wnd.fooMethod(message);
    }-*/;
}

結果在 development mode 執行就炸錯誤,主要錯誤訊息是

com.google.gwt.core.client.JavaScriptException: (TypeError) @foo.client.FooEP::callFooMethod()([]): undefined is not a function

其實在這錯誤訊息之前就有兩個地方散發出怪味道:

  1. $wnd.fooMethod = @foo.client.FooEP::javaMethod(Ljava/lang/String;)();,為什麼沒有實際給 javaMethod() 參數?等等,這個時間點給他什麼都不對啊?
  2. 忽略 1,為什麼會先跳出 alert 視窗顯示 javaMethod : null,然後才炸錯誤?

如果也在 callFooMethod() 裡頭先卡一行 $wnd.alert("WTF?"),會發現顯示 WTF? 的 alert 視窗還是會出現,表示 callFooMethod() 有正確被執行到,炸的是 $wnd.fooMethod()

整個看起來,事情好像就有頭緒了。其實 exportJsMethod()$wnd.fooMethod 根本沒有正確 assign 成 FooEP.javaMethod(),而是 assign 成 FooEP.javaMethod() 的回傳值──根本沒這玩意,所以到 callFooMethod() 的時候當然就炸了。

原先我一直死腦筋用 Java 的角度去看,後來換成 JS 的角度去看其實這樣結果很正常。在 JS 當中一個變數可以代表一個數值、也可以代表一個 function / method,如果程式中要執行該 function / method,那就是加上 (),例如在 Chrome console 輸入 alert 會得到 function alert() { [native code] },而 alert("WTF") 才會真正跳出 alert 視窗。在上頭的 case 當中,我只是要把 $wnd.fooMethod 指定為一段程式碼,根本沒有要執行它的意思,所以加上 () 根本就是多餘且錯誤阿…

所以拿掉後頭的空括號,一切就正常了(之前怎麼沒想過阿阿阿阿 [核爆]),官方文件的那個 IFI,darkk6 猜測是 intfloatint 的縮寫,如今(修正完之後)回頭看也很合理了 [死]。

最後不小心看到另一個 stackoverflow,發現還有一招裏技可以用,就是直接給 * 號,省去打一堆 field descriptor:

    native static void exportJsMethod() /*-{
        $wnd.fooMethod = @foo.client.FooEP::javaMethod(*);
    }-*/;

這有個前提,就是 FooEP 中只能有一個 javaMethod(),否則就會炸錯誤:也就是說,如果你有兩個以上同名的 method,那還是得乖乖寫 field descriptor。另外,在 JSNI 中呼叫 $wnd.fooMethod() 時已經是純粹 JS 了,所以即使你多傳參數、或是少傳參數也不會怎麼樣…… 至少 GPE 跟 browser 都沒有特別反應。

    native static void callFooMethod(String message) /*-{
        $wnd.fooMethod();
        $wnd.fooMethod(message, "又多餘了");
    }-*/;

好了,事實證明 GWT 文件也沒寫得那麼好,除了上次那個很隱晦不想讓人知道的 AutoBean 之外,又遇到了一個謎樣裏技。還是說其實官方文件有,只是我沒找到呢…… [淚目]

沒有留言:

張貼留言