Java脚本引擎对比
2025-07-08
最近开发中的一个项目需要对接多个客户,面对复杂且多样化的客户需求,我决定在项目中引入JavaScript脚本引擎来让客户根据需求定制自己的配置文件。
在这个前提下,首先明确自己的需求:
脚本引擎的性能足够好
能够在脚本中与Java代码交互
需要尽可能完善地支持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吧