用Java解析并执行Java源代码

最近,我正在开发一个用于解析Java源代码的Java应用程序,并且在业务中,有时需要对部分源代码进行eval操作。因此,我简单调查了一下如何在Java中进行eval操作并获取结果。

请查阅 tanzaku/eval-java-code 来获取我们在实验中使用的源代码。
在 master 分支中存有 JDK10 的测试代码,而在 eval_jdk8 分支中存有 JDK8 的测试代码。

使用Java编译器API的方法

按照下面的网站上的方法,可以获取到类的实例,然后通过反射来执行方法,就可以进行评估。
Java编译器类备忘录(Hishidama的Java编译器备忘录)

如果不需要进行细微的定制,可以考虑使用以下库来实现简便:
OpenHFT/Java-Runtime-Compiler:Java运行时编译器。

以原生的中文方式翻译重述如下:如何将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源代码在Groovy脚本中可能是有效的,但并非总是有效的。例如,类似于new String[]{“A”}的表达式在Groovy中无效,需要使用[“A”] as String[]来替代。

※ 根据 Groovy 3.0 的发布说明,似乎已经支持 Java 风格的数组初始化。虽然这是 alpha 版本,但在 groovy-all 3.0.0-alpha-4 可能可以直接进行评估。
《Apache Groovy 编程语言 – Groovy 3.0 发布说明》

如何通过JShell进行评估

可以使用Java 9中新增的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次小处理所需的时间。为了避免缓存,这100次处理略有不同。有关详细信息,请参考源代码。

手法JDK ver処理時間 [sec]Java Compiler APIOracle JDK 814.732Java Compiler APIOracle JDK 1042.743groovyOracle JDK 83.578groovyOracle JDK 104.914JShell APIOracle JDK 10147.294

得出结论

由于源代码简洁且速度上看起来并不差,所以在groovy中对其进行评估似乎是个不错的选择。然而,由于它无论如何都会是一个非常重的处理,因此很难频繁使用。
当编译器版本改变时,性能自然也会发生变化,因此在升级版本时需要注意。