、MVEL
、OGNL
、SpEL
、QLExpress
、JEXL
、JUEL
几种常见表达式引擎进行选型调研。先简单介绍一下这几种表达式引擎。
2.1 AviatorScript
2.2 MVEL (MVFLEX Expression Language)
2.3 OGNL (Object-Graph Navigation Language)
2.4 SpEL (Spring Expression Language)
2.5 QLExpress
2.6 JEXL (Java Expression Language)
2.7 JUEL (Java Unified Expression Language)
2.8 Janino
2.9 其他
3.1 社区支持情况
,下面列出了GitHub star,watch,fork,last commit等数据,可以作为参考,由于数据随着时间推移会产生变化,以下仅针对2023.10.29的数据进行分析。
3.2 引入大小和依赖
+- org.mvel:mvel2:jar:2.5.0.Final:compile
+- com.googlecode.aviator:aviator:jar:5.3.3:compile
+- com.alibaba:QLExpress:jar:3.3.1:compile
| +- commons-beanutils:commons-beanutils:jar:1.8.2:compile
| | - (commons-logging:commons-logging:jar:1.1.1:compile - omitted for conflict with 1.2)
| - commons-lang:commons-lang:jar:2.4:compile
+- org.codehaus.janino:janino:jar:3.1.10:compile
| - org.codehaus.janino:commons-compiler:jar:3.1.10:compile
+- ognl:ognl:jar:3.4.2:compile
| - org.javassist:javassist:jar:3.29.2-GA:compile
+- org.apache.commons:commons-jexl3:jar:3.3:compile
| - commons-logging:commons-logging:jar:1.2:compile
+- org.springframework:spring-expression:jar:5.3.29:compile
| - org.springframework:spring-core:jar:5.3.29:compile
| - org.springframework:spring-jcl:jar:5.3.29:compile
+- de.odysseus.juel:juel-api:jar:2.2.7:compile
+- de.odysseus.juel:juel-impl:jar:2.2.7:compile
+- de.odysseus.juel:juel-spi:jar:2.2.7:compile
除了SpEl外,QLExpress,OGNL,JEXL也都有其他依赖。
如果考虑 commons-beanutils, commons-lang, commons-logging 三个依赖,QLExpress 引入的大小在 10MB左右。
如果考虑 javassist 依赖,OGNL 引入的大小是4MB多。
如果考虑 commons-logging 依赖,JEXL 引入的大小是2.5MB左右。
综合来看,JUEL,AviatorScript,MVEL,JEXL 在引入大小和依赖方面要好于其他。
3.3 性能
较好的性能意味着系统能够快速地响应用户的请求,减少等待时间,提升体验。
性能方面主要通过 JMH 在字面量表达式、含有变量的表达式以及含有方法调用的表达式等使用场景对几个表达式引擎进行测试。
JMH(Java Microbenchmark Harness),是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。
由于不同表达式引擎语法或特性稍有差别,下面测试中对于差异项会进行说明。
性能测试代码地址:
GitHub
-https://github.com/howiefh/expression-engine-benchmark
3.3.1 字面量表达式
:1000 + 100.0 * 99 – (600 – 3 * 15) / (((68 – 9) – 3) * 2 – 100) + 10000 % 7 * 71
:6.7 – 100 > 39.6 ? 5 == 5 ? 4 + 5 : 6 – 1 : !(100 % 3 – 39.0
说明:
由于QlExpress执行第2个表达式时报错,需要增加圆括号,实际执行的是6.7 – 100 > 39.6 ? (5 == 5 ? 4 + 5 : 6 – 1) : (!(100 % 3 – 39.0
结果分析:
可以明显看到 JEXL,JUEL,QlExpress这三个表达式引擎性能明显不如其他引擎。
SpEl 在执行第1个算数操作时表现出色,但是在执行第2个嵌套三元操作时明显不如AviatorScript,MVEL,OGNL引擎。
此轮测试中 AviatorScript,OGNL,MVEL表现出色。AviatorScript,OGNL 执行两个表达式表现都比较出色,其中AviatorScript略好于OGNL。MVEL 在执行第1个算数操作时表现最出色,但是在执行第2个嵌套三元操作时慢于AviatorScript,OGNL引擎。
3.3.2 含有变量的表达式
:pi * d + b – (1000 – d * b / pi) / (pi + 99 – i * d) – i * pi * d / b
:piDecimal * dDecimal + bDecimal – (1000 – dDecimal * bDecimal / piDecimal) / (piDecimal + 99 – iDecimal * dDecimal) – iDecimal * piDecimal * dDecimal / bDecimal
:i * pi + (d * b – 199) / (1 – d * pi) – (2 + 100 – i / pi) % 99 == i * pi + (d * b – 199) / (1 – d * pi) – (2 + 100 – i / pi) % 99
:(clientVersion == ‘1.9.0’ || clientVersion == ‘1.9.1’ || clientVersion == ‘1.9.2’) && deviceType == ‘Xiaomi’ && weight >= 4 && osVersion == ‘Android 9.0’ && osType == ‘Android’ && clientIp != null && requestTime 1 && customer.age > 18
说明:
-
由于不同的表达式引擎在执行第2个表达式时底层实现除法时有所差别,MVEL,AviatorScript,JEXL 执行decimal.divide(otherDecimal, java.math.MathContext.DECIMAL128),其他实际执行的是decimal.divide(otherDecimal, scale, roundingMode),只是参数略有不同,分析时分组进行。
-
由于QlExpress执行第3个表达式时报错,不支持非整型mod操作,需要增加类型转换,实际执行的是i * pi + (d * b – 199) / (1 – d * pi) – (int)(2 + 100 – i / pi) % 99 == i * pi + (d * b – 199) / (1 – d * pi) – (int)(2 + 100 – i / pi) % 99
-
由于AviatorScript执行第4个表达式时报错,null的字面量是nil,实际执行的是(clientVersion == ‘1.9.0’ || clientVersion == ‘1.9.1’ || clientVersion == ‘1.9.2’) && deviceType == ‘Xiaomi’ && weight >= 4 && osVersion == ‘Android 9.0’ && osType == ‘Android’ && clientIp != nil && requestTime 1 && customer.age > 18
结果分析:
第1个基本类型包装类的算术计算 SpEl 最优。其次是AviatorScript,MVEL,OGNL。而JEXL,JUEL,QlExpress则不如其他引擎。
第2个BigDecimal类型的算术计算。由于底层实现不同,分为两组。第1组 MVEL、AviatorScript和JEXL,AviatorScript 优于 MVEL 优于 JEXL。第2组 JUEL,QlExpress,OGNL和SpEl,性能由优到差依次是 OGNL,SpEl,JUEL,QlExpress。并且第1组由于精度更高,性能明显都差于第2组。
第3个含有基本类型包装类算数计算的布尔表达式。SpEl 最优,AviatorScript 次之,接下来依次是 OGNL, MVEL,JUEL,JEXL,QlExpress。
第4个含有字符串比较的布尔表达式。AviatorScript,MVEL,JEXL,OGNL 性能优于 JUEL,QlExpress,SpEl。
3.3.3 含有方法调用的表达式
:new java.util.Date()
:s.substring(b.d)
:s.substring(b.d).substring(a, b.c.e)
说明:
-
由于 JUEL 执行new java.util.Date()时报错,不支持new实例,本轮实际执行的是自定义函数fn:date()
-
由于 AviatorScript 执行s.substring时报错,需使用其提供的内部函数,本轮实际执行的是其内部函数string.substring
结果分析:
此轮测试中 SpEl 的表现最优,甚至比Janino还要快。MVEL,AviatorScript次之,在执行构造方法时MVEL要好于AviatorScript。JEXL 表现也比较出色。QlExpress,JUEL,OGNL这三个表达式引擎则不如其他引擎。
3.3.4 总结
综合以上测试结果,AviatorScript,SpEl,MVEL,OGNL性能表现相对较好。
AviatorScript 性能相对较好,表现均衡,但其语法相较其他引擎跟Java的差异略大。
SpEl 除了在个别场景下性能较差,大部分场景表现非常出色,尤其是在字面量和含有变量的算数计算及方法调用场景下。
MVEL 性能表现相对均衡,含有变量的算术计算略差于AviatorScript,其在字面量算术计算,方法调用场景下表现都非常出色。
OGNL 性能表现也相对均衡,但方法调用场景下表现不佳。
3.4 安全
引入表达式引擎,应该重视系统的安全性和可靠性,比如要防止在不可信环境中被注入恶意脚本,越权执行某些系统命令或使应用停止服务等。安全性方面主要通过漏洞披露、安全指南和配置比较几种表达式引擎。
3.4.1 漏洞
首先在https://cve.mitre.org/cve/search_cve_list.html通过关键字搜索的方式粗略了解一下不同表达式引擎被公开的漏洞。这种方式可能不是非常的准确,由于不同表达式引擎的使用场景、使用方式、关注度的不同可能导致被公开的漏洞存在差异。比如我们所熟悉的 OGNL、SpEl 的关键字出现在漏洞中的频率明显高于其他表达式引擎。OGNL 在MyBatis和Struts中被使用,SpEl则在Spring中被广泛使用,这两个表达式引擎会被大部分项目间接使用,直接将用户输入作为表达式的一部分执行,很容易导致出现漏洞。
我们可以从这些公布的漏洞中了解不同表达式引擎可能存在的安全隐患及其修复情况,在使用过程中尽可能避免出现类似问题。
此外,不推荐将表达式执行直接开放到不可信的环境,如果确实需要,应该详细了解选择的表达式引擎,是否提供了必要的设置选项可以避免某些安全隐患。
3.4.2 安全设置
AviatorScript,QLExpress,JEXL均从不同程度提供了一些安全选项设置。
AviatorScript
-
设置白名单
// 在new语句和静态方法调用中允许使用的类白名单 默认 null 表示无限制
AviatorEvaluator.setOption(Options.ALLOWED_CLASS_SET, Sets.newHashSet(List.class));
// 在new语句和静态方法调用中允许使用的类白名单 包含子类 默认 null 表示无限制
AviatorEvaluator.setOption(Options.ASSIGNABLE_ALLOWED_CLASS_SET, Sets.newHashSet(List.class));
-
防止死循环
// 循环最大次数 默认 0 表示无限制
AviatorEvaluator.setOption(Options.MAX_LOOP_COUNT, 10000);
-
特性开关
// 关闭某些特性
AviatorEvaluator.getInstance().disableFeature(Feature.Module);
AviatorEvaluator.getInstance().disableFeature(Feature.NewInstance);
// 只开启需要的特性
AviatorEvaluator.setOption(Options.FEATURE_SET, Feature.asSet(Feature.If));
QLExpress
-
开启沙箱模式
QLExpressRunStrategy.setSandBoxMode(true);
在沙箱模式中,不可以:
◦
import Java 类
◦
显式引用 Java 类,比如String a = ‘mmm’
◦
取 Java 类中的字段:a = new Integer(11); a.value
◦
调用 Java 类中的方法:Math.abs(12)
可以:
◦使用 QLExpress 的自定义操作符/宏/函数,以此实现与应用的受控交互
◦使用. 操作符获取 Map 的 key 对应的 value,比如 a 在应用传入的表达式中是一个 Map,那么可以通过 a.b 获取
◦所有不涉及应用 Java 类的操作
-
设置白名单
// 设置编译期白名单
QLExpressRunStrategy.setCompileWhiteCheckerList(Arrays.asList(
// 精确设置
CheckerFactory.must(Date.class),
// 子类设置
CheckerFactory.assignable(List.class)
));
// 设置运行时白名单// 必须将该选项设置为 true
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
// 有白名单设置时, 则黑名单失效
QLExpressRunStrategy.addSecureMethod(RiskBean.class, "secureMethod");
-
设置黑名单
// 必须将该选项设置为 true
QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
// 这里不区分静态方法与成员方法, 写法一致
// 不支持重载, riskMethod 的所有重载方法都会被禁止
QLExpressRunStrategy.addSecurityRiskMethod(RiskBean.class, "riskMethod");
QLExpess 目前默认添加的黑名单有:
◦java.lang.System.exit
◦java.lang.Runtime.exec
◦java.lang.ProcessBuilder.start
◦java.lang.reflect.Method.invoke
◦java.lang.reflect.Class.forName
◦java.lang.reflect.ClassLoader.loadClass
◦java.lang.reflect.ClassLoader.findClass
-
防止死循环
//可通过timeoutMillis参数设置脚本的运行超时时间:1000ms
Object r = runner.execute(express, context, null, true, false, 1000);
JEXL
-
使用沙箱
// 使用中应该通过JexlSandbox的重载构造方法进行配置
new JexlBuilder().sandbox(new JexlSandbox()).create();
-
设置白名单权限
new JexlBuilder().permissions(JexlPermissions.RESTRICTED.compose("com.jd.*")).create();
-
特性开关
// 关闭循环、new 实例,import等特性
new JexlBuilder().features(new JexlFeatures().loops(false).newInstance(false).importPragma(false)).create();
3.5 使用案例
从业界使用情况可以了解不同表达式引擎的可行性、生态和整合性,以及最佳实践,进而借鉴。从下表可以看到AviatorScript,MVEL,QLExpress在国内业务线均有使用案例,有些企业也有文章输出,我们可以借鉴使用。
3.6 语法
易于理解和使用的语法可以提高开发效率,并降低学习成本。接下来从类型、操作符、控制语句、集合、方法定义几方面比较一下不同表达式引擎的语法设计。
类型方面,AviatorScript 设计了特有的类型,使用时需要注意其类型转换的优先级long->bigint->decimal->double。AviatorScript、MVEL、OGNL、JEXL都支持BigInteger、BigDecimal字面量,这意味着进行精确计算时可以使用字面量,将更方便,如10.24B就表示一个BigDecimal字面量(AviatorScript中BigDecimal字面量后缀是M)。此外AviatorScript、QLExpress还支持高精度计算的设置项。
操作符方面,QLExpress支持替换、自定义操作符及添加操作符别名,这可能有助于简化复杂表达式或使表达式更加直观,不过添加预置函数应该可以达到差不多的效果。AviatorScript也支持自定义部分操作符,不过支持数量相当有限。AviatorScript、SpEl、JEXL支持正则匹配操作符。
控制语句方面,除OGNL、SpEl、JUEL不支持控制语句外,其他都支持,不过需要注意 AviatorScript 的 else if 语法有些特殊写作 elsif,foreach语句跟Java也有所不同。
集合方面,除JUEL外其他都提供了快捷定义的方式,只不过语法不同。
函数定义方面,SpEl、JUEL均不支持,OGNL支持伪lambda定义,其他都支持定义函数。QLExpress不支持定义lambda。
综合来看,和Java语法都或多或少存在一些差异。AviatorScript设计了自己特有的一些语法,使用的话需要熟悉一下。QLExpress支持自定义操作符,可以使表达式看起来更直观。MVEL、JEXL的语法可能更接近Java,让人更容易接受一些。OGNL、SpEl、JUEL的语法更简单一些,不支持控制语句和函数定义,当然也可以通过预置一些函数变通解决一些较复杂的问题。
四、选型建议
社区方面,SpEl无疑是最活跃的。AviatorScript,QLExpress,MVEL在国内很受欢迎,QLExpress 有阿里背书。
代码大小和依赖方面,AviatorScript,MVEL 依赖少,并且代码大小也偏小。
性能方面,如果你使用表达式引擎执行字面量算术计算或方法调用偏多可以选用SpEl,MVEL。如果希望整体性能表现较好可以选用 AviatorScript。
安全方面,如果想自定义安全选项,可以考虑 AviatorScript,QLExpress和JEXL。
使用案例方面,AviatorScript,MVEL,QLExpress在国内都有实际使用案例可循。
语法方面,可能存在一些主观因素,仅供参考,个人觉得MVEL、JEXL的语法设计使用起来会更容易一些。
通过对以上几个方面的评估和分析,希望可以帮助团队基于自身情况及偏好选择最适合自己项目的Java表达式引擎。
参考资料
[1] QLExpress:https://github.com/alibaba/QLExpress
[2] AviatorScript:https://github.com/killme2008/aviatorscript
[3] MVEL:https://github.com/mvel/mvel
[4] OGNL:https://github.com/orphan-oss/ognl
[5] SpEl:https://github.com/spring-projects/spring-framework
[6] Janino:https://github.com/janino-compiler/janino
[7] JUEL:https://github.com/beckchr/juel
[8] JEXL:https://github.com/apache/commons-jexl
[9] Fel:https://github.com/dbcxy/fast-el
[10] ik-expression:https://code.google.com/archive/p/ik-expression/
[11] JSEL:https://code.google.com/archive/p/lite/wikis/JSEL.wiki
[1] JMH:https://www.cnblogs.com/wupeixuan/p/13091381.html
-end-
本文分享自微信公众号 – 京东云开发者(JDT_Developers)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。