JavaにおけるJavaScriptサポート機能

Javaには「Java Scripting API」と呼ばれるAPIが用意されている。これは、正式には「JSR 223: Scripting for the Java Platform」という名前の機能で、Java 6から標準で搭載されるようになった。この機能を利用すると、Javaプログラム内でほかのスクリプト言語のコードを実行したり、逆にスクリプト言語からJavaのオブジェクトを使用したりすることが可能になる。

Java Scripting APIが主にターゲットとしているスクリプト言語はJavaScriptで、この機能をサポートするために、Java 6の頃からJDKには標準でJavaScriptエンジンが搭載されてきた。JDK 6および7にはMozilla Foundationによって開発された「Rhino」というJavaScriptエンジンが付属していたが、JDK 8以降は「Nashorn」というJavaScriptエンジンに置き換えられた。NashornはJDKのために開発されたJavaScriptエンジンで、「JSR 292」で追加されたJVM命令のinvokeDynamicを使用している点や、ECMAScript 5.1および6に準拠している点などが、Rhinoに比べて優れていた。

Java Scripting APIを使用してスクリプト言語のコードを実行するには、JVMとは別にその言語の実行エンジンが必要となる。その点JavaScriptであれば、JDKに最初からNashornが付属しているので、追加のソフトウェアを用意しなくても利用できる。また、NashornはJavaプロラグムから利用するだけでなく、独立したJavaScript実行環境としても使用できるようになっている。これはJDKに「jjs」というコマンドツールとして実装されていて、標準でJavaオブジェクトにアクセスするための機能を備えている点が特徴だ。

JavaがJavaScriptをサポートしているメリットとしては、次のような点が挙げられる。

  • 局所的に利用するDSL(Domain Specific Language)やプロトタイピングとしてJavaScriptを使える
  • Javaの強力なクラスライブラリをJavaScriptで利用できる
  • サーバサイドのサービスで、JavaとJavaScriptを組み合わせて開発ができる

JavaScriptは世界中で広く使われている言語であるため使用できる開発者が多く、情報も充実している。そのノウハウをJavaアプリケーションの開発と組み合わせることができるメリットは大きい。また、Node.jsのようなサーバサイドのJavaScriptフレームワークとシームレスに連携できるという強みもある。JavaによるJavaScriptサポートには、意外と軽視できない理由がある。

ところが、詳しい理由は後述するが、現在JDKに付属してるNashornは2018年9月にリリースされたJava 11からはすでに非推奨(Deprecated)に指定されており、2020年9月にリリースされるJava 15では正式にJDKから削除される予定になっている。

  • JavaによるJavaScriptサポートの歴史

    JavaによるJavaScriptサポートの歴史

Nashornの代替手段として最も有力な候補となるのが「GraalJS」(GraalVM JavaScript)と呼ばれるJavaScriptエンジンである。ここではまず、Java Scripting APIの使用方法をおさらいした上で、GraalJSの使い方やNashornとの違いなどについて解説したい。

JavaでJavaScriptコードを実行する

それでは、まずはJavaScript APIとNashornを使って、Javaプログラム内でJavaScriptコードを実行してみよう。次のプログラムは、JavaScriptでsum()関数を定義した上で呼び出している例である。

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class JavaScriptSample {
    public static void main(String[] args) {
        // JavaScriptエンジンを指定
        ScriptEngine scriptEngine =
            new ScriptEngineManager().getEngineByName("nashorn");
        try {
            // JavaScriptコードを実行
            scriptEngine.eval("function sum(a, b) { return a + b }");
            double v = (Double)scriptEngine.eval("sum(21, 22)");
            System.out.println(v);

        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
}

Java Scripting APIで提供されるクラスはjavax.scriptパッケージに定義されている。JavaプログラムでJavaScriptコードを実行するには、まず使用するJavaScriptエンジンを指定してScriptEngineインタフェースのインスタンスを作成する。ScriptEngineManagerクラスが、ScriptEngineのファクトリクラスになっている。Nashornをエンジンとして使用する場合は、getEngineByName()メソッドに単に"nashorn"と指定するだけでよい。

実際のJavaScriptコードの実行は、ScriptEngineのeval()メソッドで行う。eval()に文字列を渡すと、その内容がJavaScriptコードとして解釈されて実行される。上の例では、まず最初のeval()でsum()関数を宣言し、次のeval()で「sum(21, 22)」のコードを呼び出している。ただし、eval()の戻り値はObject型なので、適切な型にキャストする必要がある。

なお、前述のようにJDK 11以降ではNashornはすでに非推奨になっているので、このプログラムではWarningが発生する。

JavaScriptからJavaクラスを使用する

次に、JavaScriptからJavaのクラスを使用してみよう。JDK 8以降のJDKには「jjs」というコマンドが用意されており、これでNashornを使ってJavaScriptを実行することができる。jjsはJDKをインストールした場所のbinディレクトリにある。jjsコマンドを次のようにオプション無しで実行すると、Nashornが対話モードで起動する。このモードでは、JavaScriptのコードを直接書き込んで実行することができる。終了したい場合はquit()関数を呼び出す。

$ jjs
Warning: The jjs tool is planned to be removed from a future JDK release
Warning: The jjs tool is planned to be removed from a future JDK release
jjs> print("Hello, World!")
Hello, World!
jjs> quit()
$

JavaScriptをファイルから読み込んで実行したい場合は、次のようにjjsコマンドのオプションにJavaScriptを記述したファイル名を渡せばよい。

$ jjs sample.js
Warning: The jjs tool is planned to be removed from a future JDK release
now = 2020-07-18T19:30:58.204995
1 day later = 2020-07-19T19:30:58.204995
Truncated To Hours = 2020-07-18T19:00

この例では、sample.jsの中身は次のように記述した。

// Javaクラスのインスタンスを作成
var now = java.time.LocalDateTime.now();
// Javaクラスのメソッドを呼び出す
print("now = " + now);
print("1 day later = " + now.plusDays(1));
print("Truncated To Hours = " + now.truncatedTo(java.time.temporal.ChronoUnit.HOURS));

java.time.LocalDateTimeはJavaのクラスである。この例のように、NashornではJavaScriptのコード内でjavaのクラスのインスタンスを生成したり、メソッドを呼び出したりすることができる。

上の例ではJavaのクラスをパッケージ名付きのフルパスで指定したが、Javaプログラムと同様にパッケージ単位でインポートすることもできる。パッケージのインポートには、次のようにpackageImport()メソッドを使用する。ただし、packageImport()メソッドはNashorn独自の機能なので、使用するには「nashorn:mozilla_compat.js」という互換用のライブラリを使用する必要がある。

// JavaImporterを使用するためのライブラリをロード
load("nashorn:mozilla_compat.js");

// java.timeパッケージをインポート
importPackage("java.time");
var now = LocalDateTime.now();
print("now = " + now);
print("1 day later = " + now.plusDays(1));

// java.time.temporalパッケージをインポート
importPackage("java.time.temporal");
print("Truncated To Hours = " + now.truncatedTo(ChronoUnit.HOURS));

使ってみると何かと便利なJava Scripting APIだが、JDKにNashornが付属しなくなるということで、今後は別のJavaScriptエンジンを使わなければならない。その最も有力な候補となるのが「GraalJS」と呼ばれるエンジンだが、Nashornとは若干機能や使い勝手が異なる部分がある。次回はGraalJSについて紹介する。