Java 互操作性

本文件將說明如何啟用與 Java 的互操作性,以及可能的 JavaScript 到 Java 的嵌入情境。

啟用 Java 互操作性 #

從適用於 JDK 21 的 GraalVM 開始,所有必要的成品可以直接從 Maven Central 下載。所有與嵌入程式相關的成品都可以在 Maven 相依性群組 org.graalvm.polyglot 中找到。如需相依性設定的詳細資訊,請參閱入門指南

多語系環境 #

在 Java 中嵌入 JavaScript 的首選方法是透過 Context。為此,會使用 hostAccess 選項建立新的 org.graalvm.polyglot.Context,允許存取權,並使用 hostClassLookup 述詞定義您允許存取的 Java 類別。

Context context = Context.newBuilder("js")
    .allowHostAccess(HostAccess.ALL)
    //allows access to all Java classes
    .allowHostClassLookup(className -> true)
    .build();
context.eval("js", jsSourceCode);

請參閱嵌入語言指南,瞭解如何從 Java 主機應用程式與 JavaScript 等客體語言互動。

ScriptEngine (JSR 223) #

在 GraalVM JDK 上執行的 JavaScript 與 JSR 223 完全相容,並支援 ScriptEngine API。在內部,GraalVM 的 JavaScript ScriptEngine 會包裝一個多語系 Context 實例。

ScriptEngine eng = new ScriptEngineManager()
    .getEngineByName("graal.js");
Object fn = eng.eval("(function() { return this; })");
Invocable inv = (Invocable) eng;
Object result = inv.invokeMethod(fn, "call", fn);

如需如何從 GraalJS 使用它的詳細資訊,請參閱ScriptEngine 指南

從 JavaScript 存取 Java #

GraalVM 提供一組功能,可允許從 JavaScriptJava 的互操作性。雖然 Rhino、Nashorn 和 GraalJS 具有大部分可比較的整體功能集,但它們在確切的語法和部分語意上有所不同。

類別存取 #

若要存取 Java 類別,GraalJS 支援 Java.type(typeName) 函式。

var FileClass = Java.type('java.io.File');

如果允許主機類別查詢 (allowHostClassLookup),則預設情況下會提供 java 全域屬性。存取例如 java.io.File 的現有程式碼,應重寫為使用 Java.type(name) 函式。

// GraalJS (and Nashorn) compliant syntax
var FileClass = Java.type("java.io.File");
// Backwards-compatible syntax
var FileClass = java.io.File;

GraalJS 提供 Packagesjava 和類似的全域屬性以取得相容性。不過,在可能的情況下,最好使用 Java.type 明確存取所需的類別,原因有兩個:

  1. 它會一步解析類別,而不是嘗試將每個屬性解析為類別。
  2. 如果找不到類別或無法存取類別,Java.type 會立即擲回 TypeError,而不是以靜默方式將未解析的名稱視為套件。

可以使用 js.java-package-globals 旗標來停用 Java 套件的全域欄位 (設定為 false 以避免建立欄位;預設值為 true)。

建構 Java 物件 #

可以使用 JavaScript 的 new 關鍵字來建構 Java 物件。

var FileClass = Java.type('java.io.File');
var file = new FileClass("myFile.md");

欄位和方法存取 #

可以像 JavaScript 屬性一樣存取 Java 類別的靜態欄位,或 Java 物件的欄位。

var JavaPI = Java.type('java.lang.Math').PI;

可以像 JavaScript 函式一樣呼叫 Java 方法。

var file = new (Java.type('java.io.File'))("test.md");
var fileName = file.getName();

方法引數的轉換 #

JavaScript 定義為在 double 數字類型上操作。GraalJS 內部可能會出於效能考量而使用其他 Java 資料類型 (例如,int 類型)。

呼叫 Java 方法時,可能需要值轉換。當 Java 方法預期 long 參數,而從 GraalJS 提供 int 時,就會發生這種情況 (類型擴展)。如果此轉換導致遺失轉換,則會擲回 TypeError

//Java
void longArg   (long arg1);
void doubleArg (double arg2);
void intArg    (int arg3);
//JavaScript
javaObject.longArg(1);     //widening, OK
javaObject.doubleArg(1);   //widening, OK
javaObject.intArg(1);      //match, OK

javaObject.longArg(1.1);   //lossy conversion, TypeError!
javaObject.doubleArg(1.1); //match, OK
javaObject.intArg(1.1);    //lossy conversion, TypeError!

請注意,引數值必須符合參數類型。您可以使用自訂目標類型對應來覆寫此行為。

方法選取 #

Java 允許依引數類型多載方法。從 JavaScript 呼叫 Java 時,會選取可用類型中最窄的方法,該方法可以在不遺失的情況下轉換為實際引數。

//Java
void foo(int arg);
void foo(short arg);
void foo(double arg);
void foo(long arg);
//JavaScript
javaObject.foo(1);              // will call foo(short);
javaObject.foo(Math.pow(2,16)); // will call foo(int);
javaObject.foo(1.1);            // will call foo(double);
javaObject.foo(Math.pow(2,32)); // will call foo(long);

若要覆寫此行為,可以使用 javaObject['methodName(paramTypes)'] 語法來選取明確的方法多載。參數類型需要以逗號分隔,且沒有空格,而物件類型需要完整限定 (例如,'get(java.lang.String,java.lang.String[])')。請注意,這與允許額外空格和簡單名稱的 Nashorn 不同。在上述範例中,即使可以使用無損轉換 (foo(1)) 達到 foo(short),也可能總是想要呼叫例如 foo(long)

javaObject['foo(int)'](1);
javaObject['foo(long)'](1);
javaObject['foo(double)'](1);

請注意,引數值仍然必須符合參數類型。您可以使用自訂目標類型對應來覆寫此行為。

當方法多載不明確且無法自動解析,以及您想要覆寫預設選擇時,明確的方法選取也很有用。

//Java
void sort(List<Object> array, Comparator<Object> callback);
void sort(List<Integer> array, IntBinaryOperator callback);
void consumeArray(List<Object> array);
void consumeArray(Object[] array);
//JavaScript
var array = [3, 13, 3, 7];
var compare = (x, y) => (x < y) ? -1 : ((x == y) ? 0 : 1);

// throws TypeError: Multiple applicable overloads found
javaObject.sort(array, compare);
// explicitly select sort(List, Comparator)
javaObject['sort(java.util.List,java.util.Comparator)'](array, compare);

// will call consumeArray(List)
javaObject.consumeArray(array);
// explicitly select consumeArray(Object[])
javaObject['consumeArray(java.lang.Object[])'](array);

請注意,目前沒有明確選取建構函式多載的方法。未來版本的 GraalJS 可能會解除此限制。

套件存取 #

GraalJS 提供 Packages 全域屬性。

> Packages.java.io.File
JavaClass[java.io.File]

陣列存取 #

GraalJS 支援從 JavaScript 程式碼建立 Java 陣列。支援 Rhino 和 Nashorn 建議的兩種模式。

//Rhino pattern
var JArray = Java.type('java.lang.reflect.Array');
var JString = Java.type('java.lang.String');
var sarr = JArray.newInstance(JString, 5);
// Nashorn pattern
var IntArray = Java.type("int[]");
var iarr = new IntArray(5);

建立的陣列是 Java 類型,但可以在 JavaScript 程式碼中使用。

iarr[0] = iarr[iarr.length] * 2;

地圖存取 #

在 GraalJS 中,您可以建立和存取 Java Map,例如,java.util.HashMap

var HashMap = Java.type('java.util.HashMap');
var map = new HashMap();
map.put(1, "a");
map.get(1);

GraalJS 支援以類似 Nashorn 的方式在這些地圖上進行迭代。

for (var key in map) {
    print(key);
    print(map.get(key));
}

清單存取 #

在 GraalJS 中,您可以建立和存取 Java List,例如,java.util.ArrayList

var ArrayList = Java.type('java.util.ArrayList');
var list = new ArrayList();
list.add(42);
list.add("23");
list.add({});

for (var idx in list) {
    print(idx);
    print(list.get(idx));
}

字串存取 #

GraalJS 可以使用 Java 互操作性建立 Java 字串。可以使用 length 屬性查詢字串的長度 (請注意,length 是值屬性,不能作為函式呼叫)。

var javaString = new (Java.type('java.lang.String'))("Java");
javaString.length === 4;

請注意,GraalJS 在內部使用 Java 字串來表示 JavaScript 字串,因此上述程式碼和 JavaScript 字串常值 "Java" 實際上無法區分。

迭代屬性 #

可以使用 JavaScript for..in 迴圈來迭代 Java 類別和 Java 物件的屬性 (欄位和方法)。

var m = Java.type('java.lang.Math')
for (var i in m) { print(i); }
> E
> PI
> abs
> sin
> ...

從 Java 存取 JavaScript 物件 #

JavaScript 物件會以 com.oracle.truffle.api.interop.java.TruffleMap 的實例的形式公開給 Java 程式碼。此類別會實作 Java 的 Map 介面。

JavaImporter #

JavaImporter 功能僅在 Nashorn 相容模式下可用 (使用 js.nashorn-compat 選項)。

Java 類別和 Java 物件的控制台輸出 #

GraalJS 提供 printconsole.log

GraalJS 提供與 Nashorn 相容的 print 內建函式。

console.log 直接由 Node.js 提供。它不會對互操作物件進行特殊處理。請注意,GraalJS 上 console.log 的預設實作只是 print 的別名,而 Node 的實作僅在 Node.js 上執行時可用。

例外狀況 #

可以在 JavaScript 程式碼中捕捉 Java 程式碼中擲回的例外狀況。它們會表示為 Java 物件。

try {
    Java.type('java.lang.Class')
    .forName("nonexistent");
} catch (e) {
    print(e.getMessage());
}

Promise #

GraalJS 提供 JavaScript Promise 物件和 Java 之間的互操作性支援。Java 物件可以公開為 JavaScript 程式碼,作為可 then 物件,允許 JavaScript 程式碼 await Java 物件。此外,JavaScript Promise 物件是一般的 JavaScript 物件,可以使用本文件中所述的機制從 Java 存取。這允許在解析或拒絕 JavaScript promise 時從 JavaScript 回呼 Java 程式碼。

建立可以從 Java 解析的 JavaScript Promise 物件 #

JavaScript 應用程式可以建立 Promise 物件,將 Promise 實例的解析委派給 Java。這可以透過將 Java 物件用作 JavaScript Promise「執行器」函式來從 JavaScript 實現。例如,實作下列函式介面的 Java 物件可用於建立新的 Promise 物件。

@FunctionalInterface
public interface PromiseExecutor {
    void onPromiseCreation(Value onResolve, Value onReject);
}

可以使用任何實作 PromiseExecutor 的 Java 物件來建立 JavaScript Promise

// `javaExecutable` is a Java object implementing the `PromiseExecutor` interface
var myPromise = new Promise(javaExecutable).then(...);

不僅可以使用函式介面建立 JavaScript Promise 物件,還可以使用 GraalJS 可以執行的任何其他 Java 物件 (例如,任何實作 Polyglot ProxyExecutable 介面的 Java 物件)。GraalJS 單元測試中提供了更詳細的使用範例。

await 與 Java 物件搭配使用 #

JavaScript 應用程式可以使用 await 表達式搭配 Java 物件。當 Java 和 JavaScript 必須與非同步事件互動時,這會很有用。若要將 Java 物件作為thenable 物件公開給 GraalJS,Java 物件應實作一個名為 then() 的方法,其簽名如下:

void then(Value onResolve, Value onReject);

await 與實作 then() 的 Java 物件一起使用時,GraalJS 會將該物件視為 JavaScript PromiseonResolveonReject 引數是可執行的 Value 物件,Java 程式碼應使用它們來恢復或中止與相應 Java 物件關聯的 JavaScript await 表達式。更詳細的範例用法可在 GraalJS 單元測試中找到。

從 Java 使用 JavaScript Promise #

在 JavaScript 中建立的 Promise 物件可以像其他 JavaScript 物件一樣公開給 Java 程式碼。Java 程式碼可以像正常的 Value 物件一樣存取這些物件,並且可以使用 Promise 的預設 then()catch() 函式註冊新的 promise 解析函式。例如,以下 Java 程式碼註冊了一個 Java 回呼函式,以便在 JavaScript promise 解析時執行

Value jsPromise = context.eval(ID, "Promise.resolve(42);");
Consumer<Object> javaThen = (value)
    -> System.out.println("Resolved from JavaScript: " + value);
jsPromise.invokeMember("then", javaThen);

更詳細的範例用法可在 GraalJS 單元測試中找到。

多執行緒 #

GraalJS 在與 Java 結合使用時支援多執行緒。有關 GraalJS 多執行緒模型的更多詳細資訊,請參閱多執行緒文件。

擴展 Java 類別 #

GraalJS 支援使用 Java.extend 函式來擴展 Java 類別和介面。請注意,必須在 polyglot 上下文中啟用主機存取才能使用此功能。

Java.extend #

Java.extend(types...) 會傳回一個產生的轉接器 Java 類別物件,該物件會擴展指定的 Java 類別和/或介面。例如

var Ext = Java.extend(Java.type("some.AbstractClass"),
                      Java.type("some.Interface1"),
                      Java.type("some.Interface2"));
var impl = new Ext({
  superclassMethod: function() {/*...*/},
  interface1Method: function() {/*...*/},
  interface2Method: function() {/*...*/},
  toString() {return "MyClass";}
});
impl.superclassMethod();

可以使用 Java.super(adapterInstance) 呼叫父類別方法。請參閱組合範例

var sw = new (Java.type("java.io.StringWriter"));
var FilterWriterAdapter = Java.extend(Java.type("java.io.FilterWriter"));
var fw = new FilterWriterAdapter(sw, {
    write: function(s, off, len) {
        s = s.toUpperCase();
        if (off === undefined) {
            fw_super.write(s, 0, s.length)
        } else {
            fw_super.write(s, off, len)
        }
    }
});
var fw_super = Java.super(fw);
fw.write("abcdefg");
fw.write("h".charAt(0));
fw.write("**ijk**", 2, 3);
fw.write("***lmno**", 3, 4);
print(sw); // ABCDEFGHIJKLMNO

請注意,在 nashorn-compat 模式下,您也可以使用介面或抽象類別的類型物件上的 new 運算子來擴展介面和抽象類別

// --experimental-options --js.nashorn-compat
var JFunction = Java.type('java.util.function.Function');
 var sqFn = new JFunction({
   apply: function(x) { return x * x; }
});
sqFn.apply(6); // 36

與我們聯繫