ここ最近、業務でJavaのソースコードを解析するJavaアプリケーションを開発していますが、解析対象のソースコードの一部をevalしたくなるケースがありました。
そこでJavaのソースコードをevalし、結果を得る方法を軽く調査しました。
実験に使用したソースコードは tanzaku/eval-java-code を参照ください。
masterにはJDK10のテストコードが入っており、eval_jdk8ブランチにはJDK8でのテストコードが入っています。
Java Compiler API を用いる方法
下記のサイト通りの方法でClassのインスタンスが取れるので、リフレクションでメソッド実行すれば評価することができます。
Javaコンパイラークラスメモ(Hishidama's JavaCompiler Memo)
細かいカスタマイズをしない場合は、以下のようなライブラリを使用するのがお手軽かもしれません。
OpenHFT/Java-Runtime-Compiler: Java Runtime Compiler
Groovyスクリプトとして評価する方法
以下のようにScriptEngine#eval
を呼び出すだけで評価することができます。(groovy-all
をクラスパスに追加する必要があります)
package experiments.eval.evaluator;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class GroovyEvaluator {
private static ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("groovy");
public String eval(final String sourceCode) throws ScriptException {
return scriptEngine.eval(sourceCode).toString();
}
}
ただし、JavaのソースコードがそのままvalidなGroovyスクリプトではない場合があるので注意が必要です。例えばnew String[]{"A"}
のような式はgroovyではinvalidで["A"] as String[]
のようにする必要があります。
※ Javaスタイルの配列初期化はGroovy 3.0で対応されているようです。alpha版ですがgroovy-all 3.0.0-alpha-4ではそのまま評価できるかもしれません。
The Apache Groovy programming language - Groovy 3.0 release notes
JShellにより評価する方法
Java9で追加されたJShell APIを用いてevalすることができます。
JShell (Java SE 9 & JDK 9 )
package experiments.eval.evaluator;
import java.util.Arrays;
import java.util.stream.Stream;
import jdk.jshell.JShell;
import jdk.jshell.SnippetEvent;
public class JShellEvaluator {
private Stream<String> splitStatements(final String sourceCode) {
return Arrays.stream(sourceCode.split(";")).map(stmt -> stmt + ";");
}
public String eval(final String sourceCode) {
try (final JShell jshell = JShell.create()) {
// 複数のstatementを一度に評価できないようだったので、分割して評価する
return splitStatements(sourceCode)
.flatMap(stmt -> jshell.eval(stmt).stream())
.reduce((a,b)->b)
.map(SnippetEvent::value)
.orElse(null);
}
}
}
処理時間の比較
各手法で評価した際の処理時間を計測しました。小さい処理を100回evalするのにかかる時間を測定しています。キャッシュされないよう、100回の処理は微妙に異なるものにしています。詳細はソースコードをご参照ください。
手法 | JDK ver | 処理時間 [sec] |
---|---|---|
Java Compiler API | Oracle JDK 8 | 14.732 |
Java Compiler API | Oracle JDK 10 | 42.743 |
groovy | Oracle JDK 8 | 3.578 |
groovy | Oracle JDK 10 | 4.914 |
JShell API | Oracle JDK 10 | 147.294 |
結論
ソースコードが簡潔で、かつ速度的にも特に見劣りしないようなので、groovyで評価するのがよさそうです。ただし、いずれにしても非常に重い処理であることは間違いないため多用するのは厳しそうです。
コンパイラのバージョンが変われば当然パフォーマンスが変わってくるので、バージョンアップの際には注意が必要ですね。