叁只仓鼠的个人博客

果然人类,无法理解

Java脚本引擎对比

2025-07-08

最近开发中的一个项目需要对接多个客户,面对复杂且多样化的客户需求,我决定在项目中引入JavaScript脚本引擎来让客户根据需求定制自己的配置文件。

在这个前提下,首先明确自己的需求:

  1. 脚本引擎的性能足够好

  2. 能够在脚本中与Java代码交互

  3. 需要尽可能完善地支持JavaScript语法(降低与客户之间的沟通成本)

脚本引擎介绍

看了一下网上的资料,找到了以下几款脚本引擎:

Rhino

Rhino是早期的JavaScript引擎,完全用Java实现,由Mozilla开发,于Java6时被集成到JDK中,后于Java8版本被Nashorn替代。在我自己的测试中,Rhino引擎的速度最慢,但它对ECMAScript语法比Nashorn更好一点。

Nashorn

它是在JDK 8中引入的,用来替代Rhino,它的速度非常快,但后来由于ECMAScript标准更新太快,Nashorn难以维护,于是被JDK弃用了。

在我自己的测试中,Nashorn对ECMAScript的语法支持不如Rhino和GraalJS。例如它不支持使用let定义变量,只能使用var

JEXL

JEXL 是一个旨在简化在用 Java 编写的应用程序和框架中实现动态和脚本功能的库。支持大部分ECMAScript语法。

语法指南:Apache Commons JEXL Syntax ? JEXL

在本文的测试中,JEXL是对JavaScript语法支持最差的。甚至连获取数组的长度array.length都不支持,必须使用size(array)这种JEXL自创的写法。

Aviator

Aviator是一个高性能、轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值。Aviator的设计目标是轻量级和高性能 ,相比于Groovy、JRuby的笨重,Aviator非常小,加上依赖包也才450K,不算依赖包的话只有70K。Aviator的语法是受限的,它不是一门完整的语言,而只是语言的一小部分集合。

因此,该项目不满足我的需求,所以我排除了这个选项。把它写在这里是因为它的性能表现确实十分亮眼,如果只需要数值运算等简单操作的话,非常建议选它。详情可以看这篇文章:Java 表达式引擎选型调研分析 | 京东云技术团队

Graal.JS

GraalJS 是一个基于 GraalVM 构建的快速JavaScript语言实现。它符合ECMAScript标准,提供与Java和其他Graal语言的互操作性、通用工具,并且如果在GraalVM JDK 运行,默认情况下通过 Graal JIT 编译器提供最佳性能,在OpenJDK中使用标准JVM上可用的JIT编译器。

测试环境

Windows 11 24H2 26100.4351

Bellsoft OpenJDK 21.0.2

处理器 13th Gen Intel(R) Core(TM) i9-13900HX 2.20 GHz

机带 RAM 64.0 GB (63.7 GB 可用)

使用的依赖如下:

    // https://mvnrepository.com/artifact/org.mozilla/rhino
    implementation("org.mozilla:rhino-engine:1.8.0")
    // https://mvnrepository.com/artifact/org.openjdk.nashorn/nashorn-core
    implementation("org.openjdk.nashorn:nashorn-core:15.6")
    // https://mvnrepository.com/artifact/org.apache.commons/commons-jexl3
    implementation("org.apache.commons:commons-jexl3:3.5.0")
    // https://mvnrepository.com/artifact/org.graalvm.js/js-scriptengine
    implementation("org.graalvm.js:js-scriptengine:24.2.1")
    // https://mvnrepository.com/artifact/org.graalvm.js/js-community
    implementation("org.graalvm.js:js-community:24.2.1")

性能测试

鉴于我的需求,我将性能测试分为几个部分:

  • 纯JavaScript代码

    • 斐波那契数列第10、20、30位

    • 冒泡排序(1000个数字)、快速排序(10000个数字)

  • 表达式运算

  • 与Java代码交互

为了方便编写测试代码,我使用kotlin进行编程,以下为具体代码。

一个简单的计时器
class SimpleTimer() {
    var start = System.nanoTime()
    var timer = System.nanoTime()

    fun getSet(): Double {
        val now = System.nanoTime()
        val result = (now - timer) / 1000000.0
        this.timer = now
        return result
    }

    fun totalTime(): Double {
        val now = System.nanoTime()
        return (now - start) / 1000000.0
    }
}

一个接口,定义了测试方法
interface EngineTest {
    fun name(): String
    fun init()
    fun eval(code: String, data: Map<String, Any>): Any?
    fun invokeMethod(obj: Any, method: String, vararg args: Any): Any?
    fun invokeFunction(method: String, vararg args: Any): Any?
    fun express(express: String, data: Map<String, Any>): Any?
}

支持JSR223的脚本引擎测试实现类
class JSREngineTest(val engineName: String) : EngineTest {
    lateinit var engine: ScriptEngine

    override fun name() = "JSR-$engineName"

    override fun init() {
        engine = ScriptEngineManager().getEngineByName(engineName)
    }

    override fun eval(code: String, data: Map<String, Any>): Any? {
        for ((key, value) in data) {
            engine.put(key, value)
        }
        return engine.eval(code)
    }

    override fun invokeMethod(obj: Any, method: String, vararg args: Any): Any? {
        val invocable = engine as Invocable
        return invocable.invokeMethod(obj, method, *args)
    }

    override fun invokeFunction(method: String, vararg args: Any): Any? {
        val invocable = engine as Invocable
        return invocable.invokeFunction(method, *args)
    }

    override fun express(express: String, data: Map<String, Any>): Any? {
        for ((key, value) in data) {
            engine.put(key, value)
        }
        return engine.eval(express)
    }
}

JEXL平台特定的测试实现类
class JexlTest() : EngineTest {
    lateinit var engine: JexlEngine
    lateinit var context: JexlContext

    override fun name() = "jexl"

    override fun init() {
        engine = JexlBuilder().create()
        context = MapContext()
    }

    override fun eval(code: String, data: Map<String, Any>): Any? {
        for ((key, value) in data) {
            context.set(key, value)
        }
        return engine.createScript(code).execute(context)
    }

    override fun invokeMethod(obj: Any, method: String, vararg args: Any): Any? {
        return engine.invokeMethod(obj, method, *args)
    }

    override fun invokeFunction(method: String, vararg args: Any): Any? {
        return engine.createScript(method).execute(context, *args)
    }

    override fun express(express: String, data: Map<String, Any>): Any? {
        for ((key, value) in data) {
            context.set(key, value)
        }
        return engine.createExpression(express).evaluate(context)
    }
}

GraalJS平台特定的测试实现类
class GraalTest() : EngineTest {
    override fun name() = "graal.js"
    lateinit var context: Context
    lateinit var bindings: Value

    override fun init() {
        context = Context.newBuilder()
            .allowAllAccess(true)
            .build()
        bindings = context.getBindings("js")
    }

    override fun eval(code: String, data: Map<String, Any>): Any? {
        for ((key, value) in data) {
            bindings.putMember(key, value)
        }
        return context.eval("js", code)
    }

    override fun invokeMethod(obj: Any, method: String, vararg args: Any): Any? {
        val value = context.asValue(obj)
        return value.invokeMember(method, *args)
    }

    override fun invokeFunction(method: String, vararg args: Any): Any? {
        return bindings.getMember(method)
            .execute(*args)
//        return bindings.invokeMember(method, *args)
    }

    override fun express(express: String, data: Map<String, Any>): Any? {
        for ((key, value) in data) {
            bindings.putMember(key, value)
        }
        return context.eval("js", express)
    }
}

main函数
fun main() {
    // 屏蔽 slf4j 和 GraalJS 输出的警告
    System.err.close()
    // 屏蔽 GraalJS 输出的警告
    System.setProperty("polyglot.engine.WarnInterpreterOnly", "false")
    // 开启 GraalJS 的 nashorn 兼容模式
    System.setProperty("polyglot.js.nashorn-compat", "true")
    // [记录成绩]
    // key  : 引擎名称
    // value: 引擎成绩
    // [引擎成绩]:
    // key  : 项目
    // value: 耗时
    val engineScore = linkedMapOf<String, MutableMap<String, Double>>()
    // 可以更换这个列表来改变引擎的先后测试顺序
    val engineList = listOf(
        JSREngineTest("jexl"), JSREngineTest("rhino"),
        JSREngineTest("nashorn"), JSREngineTest("graal.js"),
        JexlTest(), GraalTest(),
    )
    val round = 10
    for (i in 0 until round) {
        println("round: $i")
        for (engine in engineList) {
            val roundScore = linkedMapOf<String, Double>()
            val timer = SimpleTimer()

            if (i == 0) {
                // 第一轮时初始化引擎
                engine.init()
                roundScore["init"] = timer.getSet()
            }

            // 执行测试代码
            test(engine, timer, roundScore)

            // 记录本轮总共耗时
            roundScore["total"] = timer.totalTime()

            // 输出本轮成绩
            val list = roundScore.map { String.format("%s = %-8.2f", it.key, it.value) }
            println(String.format("%12s: %s", engine.name(), list.joinToString(" | ")))

            Thread.sleep(1000)
            if (i == 0) {
                // 第一轮冷启动不计入成绩
                continue
            }

            // 将本轮成绩计入最终得分
            val totalScore = engineScore.computeIfAbsent(engine.name()) { linkedMapOf() }
            for ((key, value) in roundScore) {
                val score = totalScore.getOrDefault(key, 0.0) + value
                totalScore[key] = score
            }
        }
    }
    // 输出最终得分
    println("average score:")
    for ((engineName, map) in engineScore) {
        val list = map.map { String.format("%s = %-8.2f", it.key, it.value / (round - 1)) }
        println(String.format("%12s: %s", engineName, list.joinToString(" | ")))
    }
}

对于每一项测试,测试循环进行10次,仅在第一次循环时初始化引擎,之后的9次循环中全部复用之前轮次的引擎。

纯JavaScript代码测试

斐波那契数列

首先来看最简单的写法,直接在JavaScript代码中定义斐波那契数列,然后通过invokeFunction执行,并传入参数。

测试代码
val fibCode = """
    function fib(n) {
        if (n <= 1) {
            return n;
        } else {
            return fib(n - 1) + fib(n - 2);
        }
    }
""".trimIndent()

fun test(engine: EngineTest, timer: SimpleTimer, roundScore: MutableMap<String, Double>) {
    engine.eval(fibCode, mapOf())
    for (i in listOf(10, 20, 30)) {
        try {
            engine.invokeFunction("fib", i)
            roundScore["fib(${i})"] = timer.getSet()
        } catch (e: Exception) {
            e.printStackTrace()
            // 如果不支持该语法,则成绩为 -1
            roundScore["fib(${i})"] = -1.0
        }
    }
}

为了避免数据过多影响观看,这里只看第一轮(冷启动)和平均(第2~10轮)成绩。

round: 0
    JSR-jexl: init = 79.69    | fib(10) = -1.00    | fib(20) = -1.00    | fib(30) = -1.00    | total = 98.38   
   JSR-rhino: init = 3.93     | fib(10) = 203.56   | fib(20) = 5.80     | fib(30) = 124.84   | total = 338.13  
 JSR-nashorn: init = 90.60    | fib(10) = 92.35    | fib(20) = 5.36     | fib(30) = 17.47    | total = 205.79  
JSR-graal.js: init = 265.60   | fib(10) = 524.36   | fib(20) = 21.67    | fib(30) = 372.29   | total = 1183.93 
        jexl: init = 0.44     | fib(10) = -1.00    | fib(20) = -1.00    | fib(30) = -1.00    | total = 2.61    
    graal.js: init = 7.21     | fib(10) = 3.43     | fib(20) = 14.73    | fib(30) = 595.87   | total = 621.24  

从初始化速度来看,Rhino引擎的初始化速度最快的,其次是JEXL,再然后是Nashorn和GraalJS。

GraalJS的冷启动运行速度比较慢,在首轮运行中其速度甚至比rhino还要慢。其中有一部分原因是本次测试时启动了GraalJS的Nashorn兼容模式,不过即使不开启这个模式,GraalJS的首轮运行速度仍然比Rhino要慢。

Nashorn的成绩非常诡异,fib(20)的运算时间甚至比fib(10)还要短,可以断定其内部对于这样的场景有特定优化。

JEXL由于不支持转换为Invocable,因此无法进行本轮测试。而Nashorn则在本轮中速度表现最为优异,它的执行速度是最快的。

顺带一提,对于GraalJS和JEXL来说,由于平台特定写法和JSR223写法初始化时使用的是相同的后端,所以JSR223的初始化和平台特定写法的初始化顺序会对后启动的引擎有影响。

例如:先启动JSR-jexl,则jexl的启动速度会很快,因为JEXL后端大部分内容已经被加载了。同样的,先启动jexl,则JSR-jexl的启动速度会很快。因此,在查看初始化速度的数据时,只需要看先初始化的引擎即可。

average score:
    JSR-jexl: fib(10) = -1.00    | fib(20) = -1.00    | fib(30) = -1.00    | total = 0.83    
   JSR-rhino: fib(10) = 3.20     | fib(20) = 4.07     | fib(30) = 221.25   | total = 228.52  
 JSR-nashorn: fib(10) = 0.66     | fib(20) = 0.08     | fib(30) = 7.54     | total = 8.29    
JSR-graal.js: fib(10) = 2.15     | fib(20) = 3.44     | fib(30) = 184.68   | total = 190.27  
        jexl: fib(10) = -1.00    | fib(20) = -1.00    | fib(30) = -1.00    | total = 1.87    
    graal.js: fib(10) = 0.97     | fib(20) = 1.39     | fib(30) = 151.84   | total = 154.21  

接下来看到平均成绩,可以发现GraalJS在JIT完成后,其速度明显超过了rhino引擎。使用平台特定写法的GraalJS与JSR223写法的性能区别不大,平台特定写法的性能略微好一点。

Nashorn和JEXL的成绩在本轮测试环境中没有参考价值。

接下来我们换一种JEXL也能支持的语法
val fibCode = """
    function fib(n) {
        if (n <= 1) {
            return n;
        } else {
            return fib(n - 1) + fib(n - 2);
        }
    }
    fib(count);
""".trimIndent()

fun test(engine: EngineTest, timer: SimpleTimer, roundScore: MutableMap<String, Double>) {
    for (i in listOf(10, 20, 30)) {
        try {
            engine.eval(fibCode, mapOf("count" to i))
            roundScore["fib(${i})"] = timer.getSet()
        } catch (e: Exception) {
            e.printStackTrace()
            // 如果不支持该语法,则成绩为 -1
            roundScore["fib(${i})"] = -1.0
        }
    }
}
round: 0
        jexl: init = 78.52    | fib(10) = 25.23    | fib(20) = 67.15    | fib(30) = 1004.37  | total = 1175.39 
    graal.js: init = 703.50   | fib(10) = 102.81   | fib(20) = 21.27    | fib(30) = 419.29   | total = 1246.88 
    JSR-jexl: init = 8.34     | fib(10) = 5.26     | fib(20) = 48.71    | fib(30) = 1516.56  | total = 1578.88 
   JSR-rhino: init = 3.78     | fib(10) = 155.79   | fib(20) = 10.33    | fib(30) = 273.40   | total = 443.31  
 JSR-nashorn: init = 80.15    | fib(10) = 82.78    | fib(20) = 6.16     | fib(30) = 10.91    | total = 180.00  
JSR-graal.js: init = 10.02    | fib(10) = 61.15    | fib(20) = 12.39    | fib(30) = 613.11   | total = 696.67  

通过首轮成绩对比我们可以发现,冷启动运行时,JEXL在递归轮数较小的优势比较大,在递归轮数提高之后逐渐被其他引擎超过。

此轮测试中除了JEXL本身的成绩以外,我还发现了一个有趣的现象:通过更换JSR和平台特定写法的初始化顺序,GraalJS的启动速度有明显的不同。

具体来说,如果先初始化JSR-graal.js,则后初始化平台特定GraalJS的速度会快很多,两者加起来约260毫秒左右。如果先初始化平台特定的GraalJS,则其启动需要700毫秒。

而JEXL则无明显变化,无论哪边先初始化,二者的时间相加始终为80毫秒左右。

average score:
        jexl: fib(10) = 0.94     | fib(20) = 8.61     | fib(30) = 839.53   | total = 849.09  
    graal.js: fib(10) = 1.37     | fib(20) = 2.01     | fib(30) = 142.73   | total = 146.12  
    JSR-jexl: fib(10) = 0.77     | fib(20) = 11.35    | fib(30) = 1239.53  | total = 1251.67 
   JSR-rhino: fib(10) = 3.33     | fib(20) = 6.21     | fib(30) = 226.99   | total = 236.54  
 JSR-nashorn: fib(10) = 0.40     | fib(20) = 0.23     | fib(30) = 8.96     | total = 9.60    
JSR-graal.js: fib(10) = 1.20     | fib(20) = 1.74     | fib(30) = 141.25   | total = 144.19  

在10轮的平均成绩中可以看到,递归轮次较小时JEXL依旧保持着领先,随着轮次增大慢慢被其他引擎超越。

GraalJS的JIT优势依然很大,在fib(30)中速度比Rhino快了大约一倍,是JEXL的8倍多。

Nashorn的成绩依旧没有参考价值。

冒泡排序

测试代码
// 冒泡排序
val sortCode = """
    function sort() {
        for (var i = 0; i < array.length; i++) {
            for (var j = i + 1; j < array.length; j++) {
                if (array[i] > array[j]) {
                    var temp = array[i];
                    array[i] = array[j];
                    array[j] = temp;
                }
            }
        }
        return array;
    }
    sort();
""".trimIndent()

val random = Random(System.currentTimeMillis())

fun test(engine: EngineTest, timer: SimpleTimer, roundScore: MutableMap<String, Double>) {
      val testName = "Bubble Sort"
      val array = IntArray(1000)
      for (n in 0 until array.size) {
          array[n] = random.nextInt(10240)
      }
      try {
          if (engine.name().contains("jexl")) {
              engine.eval(sortCode.replace("array.length", "size(array)"), mapOf("array" to array))
          } else {
              engine.eval(sortCode, mapOf("array" to array))
          }
          roundScore[testName] = timer.getSet()
      } catch (e: Exception) {
          e.printStackTrace()
          // 如果不支持该语法,则成绩为 -1
          roundScore[testName] = -1.0
      }
}
round: 0
        jexl: init = 70.00    | Bubble Sort = 437.25   | total = 507.26  
    graal.js: init = 712.48   | Bubble Sort = 370.12   | total = 1082.60 
    JSR-jexl: init = 9.02     | Bubble Sort = 537.55   | total = 546.57  
   JSR-rhino: init = 5.01     | Bubble Sort = 604.27   | total = 609.28  
 JSR-nashorn: init = 89.96    | Bubble Sort = 191.70   | total = 281.66  
JSR-graal.js: init = 8.21     | Bubble Sort = 260.52   | total = 268.74  

在冒泡排序中,我使用Javakotlin代码生成了1000个int数值,然后传递给脚本引擎,用JavaScript代码对其进行冒泡排序。

从这个结果来看,Nashorn引擎没法再继续作弊,不过其冷启动的排序时间(191.70ms)依旧比GraalJS更快(260.52ms),是所有引擎中速度最快的。

上一轮中表现不佳的JEXL在这里的速度排名倒数第二,平台特定写法的速度(437.25ms)约为Rhino速度(606.27)的2/3。

average score:
        jexl: Bubble Sort = 223.93   | total = 223.94  
    graal.js: Bubble Sort = 173.49   | total = 173.50  
    JSR-jexl: Bubble Sort = 427.60   | total = 427.61  
   JSR-rhino: Bubble Sort = 415.98   | total = 415.98  
 JSR-nashorn: Bubble Sort = 15.56    | total = 15.58   
JSR-graal.js: Bubble Sort = 179.90   | total = 179.90  

当所有引擎暖机完成之后,来到平均成绩,可以看到Nashorn引擎似乎又开始作弊了,天知道它到底怎么跑出来15.56ms的成绩。

暖机之后的GraalJS速度(179.90ms)比冷启动时(260.52ms)快一倍,这比较符合我们的预期。但令人意外的是,JEXL的10轮平均成绩竟然也比冷启动时快一倍——可它明明没有JIT技术。

个中细节尚不清楚,等之后有时间再看看吧。

快速排序

测试代码
// 快速排序
val sortCode = """
    function partition(arr, left, right) {
      // 选择最右侧元素作为基准值
      var pivotValue = arr[right];
      var storeIndex = left; // 存储小于基准的元素位置
    
      // 遍历当前分区(left到right-1)
      var i = left;
      while (i < right) {
        if (arr[i] < pivotValue) {
          // 将小于基准的元素交换到存储位置
          var temp = arr[i];
          arr[i] = arr[storeIndex];
          arr[storeIndex] = temp;
          storeIndex++;
        }
        i++;
      }
    
      // 将基准值放到正确位置
      var temp = arr[storeIndex];
      arr[storeIndex] = arr[right];
      arr[right] = temp;
      return storeIndex; // 返回基准值的最终位置
    }
    function sort(arr, left, right) {
      // 递归终止条件:子数组长度<=1
      if (left >= right) return;
    
      // 分区操作
      var pivotIndex = partition(arr, left, right);
    
      // 递归排序左子数组和右子数组
      sort(arr, left, pivotIndex - 1);
      sort(arr, pivotIndex + 1, right);
    }
    sort(array, 0, array.length - 1);
""".trimIndent()

val random = Random(System.currentTimeMillis())

fun test(engine: EngineTest, timer: SimpleTimer, roundScore: MutableMap<String, Double>) {
    val testName = "Quick Sort"
    val array = IntArray(10000)
    for (n in 0 until array.size) {
        array[n] = random.nextInt(102400)
    }
    try {
        if (engine.name().contains("jexl")) {
            engine.eval(sortCode.replace("array.length", "size(array)"), mapOf("array" to array))
        } else {
            engine.eval(sortCode, mapOf("array" to array))
        }
        roundScore[testName] = timer.getSet()
    } catch (e: Exception) {
        e.printStackTrace()
        // 如果不支持该语法,则成绩为 -1
        roundScore[testName] = -1.0
    }
}
round: 0
        jexl: init = 66.98    | Quick Sort = 269.28   | total = 336.27  
    graal.js: init = 649.92   | Quick Sort = 279.95   | total = 929.88  
    JSR-jexl: init = 10.17    | Quick Sort = 209.15   | total = 219.33  
   JSR-rhino: init = 3.63     | Quick Sort = 309.09   | total = 312.72  
 JSR-nashorn: init = 87.25    | Quick Sort = 203.45   | total = 290.71  
JSR-graal.js: init = 10.11    | Quick Sort = 109.07   | total = 119.18  

average score:
        jexl: Quick Sort = 67.89    | total = 67.90   
    graal.js: Quick Sort = 44.94    | total = 44.95   
    JSR-jexl: Quick Sort = 123.75   | total = 123.76  
   JSR-rhino: Quick Sort = 87.44    | total = 87.45   
 JSR-nashorn: Quick Sort = 8.97     | total = 8.98    
JSR-graal.js: Quick Sort = 41.80    | total = 41.81   

在应用了快速排序之后,各个引擎的速度都得到了提升(规模提升了十倍的同时时间减少了大约四倍)

观察10轮平均成绩,可以看到速度最快的依旧是Nashorn,好吧,我现在有点好奇它到底加了什么黑科技了。

平台特定写法的JEXL速度依旧比Rhino快,速度约为Rhino的2/3,GraalJS在JIT暖机后,速度依然领先。

GraalJS由于栈溢出而无法执行
RangeError: Maximum call stack size exceeded
	at <js> sort(Unnamed:25-35:512-746)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at <js> sort(Unnamed:34:712-743)
	at org.graalvm.polyglot.Context.eval(Context.java:446)
	at cn.hamster3.test.GraalTest.eval(EngineTest.kt:297)
	at cn.hamster3.test.EngineTestKt.test(EngineTest.kt:179)
	at cn.hamster3.test.EngineTestKt.main(EngineTest.kt:350)
	at cn.hamster3.test.EngineTestKt.main(EngineTest.kt)

似乎GraalVM默认只有10个方法栈大小,超出就会报错,找遍了网上的资源也没找到怎么修复这个问题
后来自己调试的时候发现,开启 nashorn 兼容模式之后就好了

表达式运算

测试代码
val express = listOf(
    "(1.2 + 3.4 - 5.6) * 7.8 / 9.0",
    "(1.2 + 3.4 - 5.6) * 7.8 / 9.0 > 0.9 + 8.7 - 6.5 * 4.3 / 2.1",
    "Math.max(width, height) * (screen_width + screen_height) / (limit - 10) + Math.min(width, height) * (screen_width - screen_height) * 2",
    """
        ((width * height) / (screen_width + screen_height)) * 
         (limit - (width + height) / 2) + 
         ((screen_width * screen_height) / (width * height)) - 
         (limit / (width + height + screen_width + screen_height)) * 
         ((width * screen_height) + (height * screen_width)) / 
         (limit + (screen_width - width) * (screen_height - height))
    """.trimIndent(),
    """
        (((screen_width - width) * 2) / 3 + height) / 
        ((screen_height / limit) + (width * height / (screen_width + width))) *
        (Math.sqrt(limit * limit + (screen_width - width) * (screen_height - height)) / 
         ((screen_width / screen_height) * height + width))
    """.trimIndent(),
    """
        (((screen_width * width) + (screen_height * height)) / 
         (width * height + screen_width * screen_height)) *
        ((limit * (screen_width + width + screen_height + height)) / 
         ((screen_width - width) * (screen_height - height) + 
          (screen_width * screen_height - width * height))) +
        ((screen_width * screen_height) - (width * height)) / 
        (limit * (screen_width / width + screen_height / height))
    """.trimIndent(),
    """
        (((screen_width * width) / (screen_width + width * 2)) *
         ((screen_height + height) / (screen_height - height / 2)) +
         (limit * (screen_width - width) * (screen_height - height)) / 
         (width * height * (screen_width / width + screen_height / height))) /
        (((screen_width * screen_height) - (width * height)) / 
         (limit + (screen_width + width) * (screen_height + height)) +
         ((screen_width / screen_height) * (width / height)) - 
         ((screen_width - width) * (screen_height - height) / 
          (limit + width + height)))
    """.trimIndent()
)

val random = Random(System.currentTimeMillis())

fun test(engine: EngineTest, timer: SimpleTimer, roundScore: MutableMap<String, Double>) {
    for ((index, string) in express.withIndex()) {
        val testName = "express_$index"
        try {
            engine.express(
                string, mapOf(
                    "screen_width" to random.nextInt(1280),
                    "screen_height" to random.nextInt(720),
                    "width" to random.nextInt(400),
                    "height" to random.nextInt(300),
                    "limit" to random.nextInt(1024)
                )
            )
            roundScore[testName] = timer.getSet()
        } catch (e: Exception) {
            e.printStackTrace()
            // 如果不支持该语法,则成绩为 -1
            roundScore[testName] = -1.0
        }
    }
}
round: 0
        jexl: init = 69.71    | express_0 = 16.44    | express_1 = 0.44     | express_2 = -1.00    | express_3 = 2.10     | express_4 = -1.00    | express_5 = 1.70     | express_6 = 1.03     | total = 91.42   
    graal.js: init = 648.61   | express_0 = 81.94    | express_1 = 3.10     | express_2 = 15.02    | express_3 = 1.40     | express_4 = 3.10     | express_5 = 1.18     | express_6 = 1.37     | total = 755.73  
    JSR-jexl: init = 11.29    | express_0 = 3.61     | express_1 = 0.53     | express_2 = -1.00    | express_3 = 1.79     | express_4 = -1.00    | express_5 = 1.46     | express_6 = 0.84     | total = 19.53   
   JSR-rhino: init = 3.38     | express_0 = 85.10    | express_1 = 63.62    | express_2 = 13.70    | express_3 = 9.62     | express_4 = 8.71     | express_5 = 9.58     | express_6 = 11.80    | total = 205.50  
 JSR-nashorn: init = 83.57    | express_0 = 67.36    | express_1 = 1.80     | express_2 = 9.56     | express_3 = 4.46     | express_4 = 5.76     | express_5 = 3.86     | express_6 = 4.84     | total = 181.21  
JSR-graal.js: init = 7.84     | express_0 = 58.64    | express_1 = 0.74     | express_2 = 0.90     | express_3 = 0.90     | express_4 = 0.92     | express_5 = 0.95     | express_6 = 1.76     | total = 72.66   

average score:
        jexl: express_0 = 0.40     | express_1 = 0.29     | express_2 = -1.00    | express_3 = 0.76     | express_4 = -1.00    | express_5 = 0.87     | express_6 = 0.38     | total = 2.71    
    graal.js: express_0 = 0.59     | express_1 = 0.24     | express_2 = 0.26     | express_3 = 0.29     | express_4 = 0.32     | express_5 = 0.25     | express_6 = 0.45     | total = 2.40    
    JSR-jexl: express_0 = 0.15     | express_1 = 0.08     | express_2 = -1.00    | express_3 = 0.95     | express_4 = -1.00    | express_5 = 0.95     | express_6 = 0.47     | total = 2.60    
   JSR-rhino: express_0 = 1.28     | express_1 = 1.16     | express_2 = 2.40     | express_3 = 3.12     | express_4 = 2.61     | express_5 = 2.91     | express_6 = 5.12     | total = 18.60   
 JSR-nashorn: express_0 = 0.37     | express_1 = 0.12     | express_2 = 0.17     | express_3 = 0.12     | express_4 = 0.09     | express_5 = 0.07     | express_6 = 0.23     | total = 1.17    
JSR-graal.js: express_0 = 0.66     | express_1 = 0.28     | express_2 = 0.24     | express_3 = 0.19     | express_4 = 0.20     | express_5 = 0.18     | express_6 = 0.18     | total = 1.93    

跑完这个测试,我对Nashorn的认知产生了严重的怀疑,为啥它的数字总是这么好看,这和我的经验明显有冲突。

我曾经在不同的项目中引入过Nashorn引擎,有过一次客户向我报告说我的代码导致他的服务器卡顿,检查后发现是Nashorn引擎计算一段表达式花费了太多时间。我将那段代码替换成JEXL引擎,在项目初始化时使用express = jexl.createExpression()创建表达式,需要使用时直接执行express.evaluate(),修改后的代码运行效率非常高。

虽然这段经历中修改的代码与本次测试中使用的测试代码有些不同,但它至少说明Nashorn引擎在做表达式运算时速度不应该有这么快。

好吧,也许是我的个人偏见。

总之,在这段测试中可以得出结论,如果排除Nashorn不看的话,JEXL在表达式运算中的速度非常快,但仍旧比GraalJS慢3~4倍。至于Rhino?我只能说祝他好运。

以及,JEXL对JavaScript支持不足的缺点在这里再一次表现了出来:第三段脚本执行失败了。(所以看它的total成绩没有意义,需要单独对比每一项express运算的成绩)

与Java代码交互

测试代码
val invoke1 = """
    map.put("num", number);
    
    var data = map.getOrDefault("data", 0);
    map.put("dat", data);
""".trimIndent()

class UINode {
    var x: Int = 0
    var y: Int = 0
    var width: Int = 400
    var height: Int = 300
}

val invoke2 = """
    node.x = screen_width / 2 - node.getWidth() / 2;
    node.y = screen_height / 2 - node.getHeight() / 2;
""".trimIndent()

fun test(engine: EngineTest, timer: SimpleTimer, roundScore: MutableMap<String, Double>) {
    try {
        engine.eval(
            invoke1, mapOf(
                "number" to Random.nextInt(),
                "data" to Random.nextInt(),
                "map" to mutableMapOf<String, Any>()
            )
        )
        roundScore["invoke1"] = timer.getSet()
    } catch (e: Exception) {
        e.printStackTrace()
        roundScore["invoke1"] = -1.0
    }
    try {
        engine.eval(
            invoke2, mapOf(
                "node" to UINode(),
                "screen_width" to 1280,
                "screen_height" to 720
            )
        )
        roundScore["invoke2"] = timer.getSet()
    } catch (e: Exception) {
        e.printStackTrace()
        roundScore["invoke2"] = -1.0
    }
}
round: 0
        jexl: init = 72.09    | invoke1 = 39.88    | invoke2 = -1.00    | total = 121.28  
    graal.js: init = 647.09   | invoke1 = 136.72   | invoke2 = 8.31     | total = 792.12  
    JSR-jexl: init = 8.92     | invoke1 = 2.49     | invoke2 = -1.00    | total = 13.09   
   JSR-rhino: init = 3.80     | invoke1 = 152.57   | invoke2 = 6.69     | total = 163.07  
 JSR-nashorn: init = 93.84    | invoke1 = 92.05    | invoke2 = 18.16    | total = 204.06  
JSR-graal.js: init = 7.50     | invoke1 = 63.55    | invoke2 = 1.96     | total = 73.01   

average score:
        jexl: invoke1 = 0.84     | invoke2 = -1.00    | total = 2.14    
    graal.js: invoke1 = 0.84     | invoke2 = 0.49     | total = 1.34    
    JSR-jexl: invoke1 = 0.96     | invoke2 = -1.00    | total = 1.74    
   JSR-rhino: invoke1 = 3.28     | invoke2 = 4.27     | total = 7.55    
 JSR-nashorn: invoke1 = 0.90     | invoke2 = 0.22     | total = 1.12    
JSR-graal.js: invoke1 = 0.73     | invoke2 = 0.51     | total = 1.24    

JEXL再一次出现了无法运行的现象,说实话我很不理解,这么简单的语法为啥它都能不支持的。

Nashorn的成绩再一次领先所有脚本引擎,也许是因为它真就如此强大吧。

在与Java代码交互中,Rhino的成绩明显落后其他的脚本引擎,看来它被Nashorn取代也不是没有原因的。

总结

本来以为这篇文章很快就能写完,结果没想到一不小心从中午干到晚上,花费了整整9个小时。

  • Rhino

    • 现在几乎已经没什么人使用了

    • 对ECMAScript语法支持还算好

    • 性能很差,除非还在维护老旧项目,否则不建议继续使用

  • Nashorn

    • 对ECMAScript语法支持较差

    • Java8内置的脚本引擎,如果没有特殊需求,可以直接用这个

    • 虽然它在测试中的成绩非常好,但根据我本人的实际经验来看,它的运行效率其实并不高。在对性能有严格要求的环境下,可以尝试使用JEXL替代并优化。

  • JEXL

    • 对ECMAScript语法支持最差

    • 支持JSR223,但只支持一点点

    • 性能忽高忽低,当JavaScript代码的执行量较少时,性能比Rhino好很多。当数据规模上升后,性能非常差劲

    • 如果要用的话尽量还是用官方推荐的方式使用,这样性能也会比直接使用ScriptEngine接口要高很多

  • GraalJS

    • 对ECMAScript语法支持最全面

    • 支持JSR223,但如果要通过这种方式使用,最好通过System.setProperty("polyglot.js.nashorn-compat", "true")开启nashorn兼容模式,否则会遇到很多奇奇怪怪的问题(比如JavaScript方法栈超过10个会报错、默认无法与Java代码互相访问等)

    • 内置Graal JIT支持,在GraalVM上运行时性能最好,在OpenJDK上也能用

    • 如果你的目标环境还在使用Java8,建议放弃使用GraalJS,换成Nashorn或JEXL吧