Java 相互運用性

このドキュメントでは、Java との相互運用を有効にする方法と、JavaScript から Java への組み込みのシナリオについて説明します。

Java 相互運用性の有効化 #

GraalVM for JDK 21 以降では、必要なすべての成果物を Maven Central から直接ダウンロードできます。埋め込みに関連するすべての成果物は、Maven 依存関係グループ org.graalvm.polyglot にあります。依存関係の設定の詳細については、入門ガイドをご覧ください。

ポリグロットコンテキスト #

Java に JavaScript を埋め込む推奨方法は、Context を使用することです。そのため、アクセスを許可する hostAccess オプションと、アクセスを許可する Java クラスを定義する hostClassLookup 述語を使用して、新しい org.graalvm.polyglot.Context が構築されます。

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 には、JavaScript から Java への相互運用を可能にする一連の機能が用意されています。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、および同様のグローバルプロパティを提供します。ただし、次の 2 つの理由から、可能な場合は常に Java.type で必要なクラスに明示的にアクセスすることをお勧めします。

  1. 各プロパティをクラスとして解決しようとするのではなく、1 つのステップでクラスを解決します。
  2. Java.type は、クラスが見つからない場合、またはアクセスできない場合に、未解決の名前をパッケージとして暗黙的に扱うのではなく、すぐに TypeError をスローします。

js.java-package-globals フラグを使用して、Java パッケージのグローバルフィールドを無効にできます (フィールドの作成を避けるために false に設定します。デフォルトは true です)。

Java オブジェクトの構築 #

Java オブジェクトは、JavaScript の new キーワードを使用して構築できます。

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

フィールドおよびメソッドアクセス #

Java クラスの静的フィールド、または Java オブジェクトのフィールドには、JavaScript のプロパティのようにアクセスできます。

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

Java メソッドは、JavaScript 関数のように呼び出すことができます。

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(short) に到達できる場合でも、常に foo(long) を呼び出すことが望ましい場合があります (foo(1))

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.util.HashMap などの Java マップを作成およびアクセスできます。

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.util.ArrayList などの Java リストを作成およびアクセスできます。

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 は JavaScript 文字列を表すために内部的に Java 文字列を使用しているため、上記のコードと JavaScript 文字列リテラル "Java" は実際には区別できないことに注意してください。

プロパティの反復処理 #

Java クラスと Java オブジェクトのプロパティ (フィールドとメソッド) は、JavaScript の for..in ループで反復処理できます。

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

Java から JavaScript オブジェクトへのアクセス #

JavaScript オブジェクトは、Java コードに対して com.oracle.truffle.api.interop.java.TruffleMap のインスタンスとして公開されます。このクラスは 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 で実行する場合にのみ使用できることに注意してください。

例外 #

Java コードでスローされた例外は、JavaScript コードでキャッチできます。それらは Java オブジェクトとして表現されます。

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

プロミス #

GraalJS は、JavaScript Promise オブジェクトと Java 間の相互運用性のサポートを提供します。Java オブジェクトは、JavaScript コードに対して thenable オブジェクトとして公開できるため、JavaScript コードは Java オブジェクトを await できます。さらに、JavaScript Promise オブジェクトは通常の JavaScript オブジェクトであり、このドキュメントで説明されているメカニズムを使用して Java からアクセスできます。これにより、JavaScript プロミスが解決または拒否されたときに、Java コードを JavaScript からコールバックできます。

Java から解決できる JavaScript Promise オブジェクトの作成 #

JavaScript アプリケーションは、Promise インスタンスの解決を Java に委任する Promise オブジェクトを作成できます。これは、JavaScript から Java オブジェクトを JavaScript Promise「executor」 関数として使用することで実現できます。たとえば、次の関数型インターフェイスを実装する 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 の 単体テスト で確認できます。

Java オブジェクトでの await の使用 #

JavaScript アプリケーションは、Java オブジェクトに対して await 式を使用できます。これは、Java と JavaScript が非同期イベントとやり取りする必要がある場合に役立ちます。Java オブジェクトを thenable オブジェクトとして GraalJS に公開するには、Java オブジェクトは次のシグネチャを持つ then() という名前のメソッドを実装する必要があります。

void then(Value onResolve, Value onReject);

then() を実装する Java オブジェクトで await を使用すると、GraalJS はそのオブジェクトを JavaScript の Promise として扱います。onResolve および onReject 引数は、Java コードが対応する Java オブジェクトに関連付けられた JavaScript の await 式を再開または中止するために使用する必要がある実行可能な Value オブジェクトです。詳細な使用例は、GraalJS の 単体テスト で確認できます。

Java からの JavaScript Promise の使用 #

JavaScript で作成された Promise オブジェクトは、他の JavaScript オブジェクトと同様に Java コードに公開できます。Java コードは、通常の Value オブジェクトのようにこれらのオブジェクトにアクセスでき、Promise のデフォルトの then() および catch() 関数を使用して新しい Promise 解決関数を登録できます。例として、次の Java コードは、JavaScript Promise が解決されたときに実行される Java コールバックを登録します。

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 クラスおよびインターフェースを拡張するためのサポートを提供します。この機能を利用するには、ポリグロットコンテキストでホストアクセスを有効にする必要があることに注意してください。

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

お問い合わせ