LoginSignup
6
2

More than 3 years have passed since last update.

JavaからJavaソースをevalする

Last updated at Posted at 2019-06-04

ここ最近、業務で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で評価するのがよさそうです。ただし、いずれにしても非常に重い処理であることは間違いないため多用するのは厳しそうです。
コンパイラのバージョンが変われば当然パフォーマンスが変わってくるので、バージョンアップの際には注意が必要ですね。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2