Java表达式引擎选型调研分析
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 社区支持情况
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
3.3 性能
3.3.1 字面量表达式
3.3.2 含有变量的表达式
-
由于不同的表达式引擎在执行第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 <= now&& customer.grade > 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);
在沙箱模式中,不可以:
可以:
◦使用 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
本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
SQL 也能搞复杂时序查询?使用 SQL 在 GreptimeDB 上做 Range 查询
查询并聚合一个「给定长度的时间范围的数据」,是时序数据中常见的一种查询模式。例如 PromQL 中的 Range selector,就原生地支持了这种时序查询。但对于通用的数据库查询语言 SQL ,这类时序查询很难通过原生的 SQL 完成,所以我们在 GreptimeDB 中引入了扩展的 SQL Range 查询语法,将时序查询能力与 SQL 高度灵活的表达能力相结合,实现了 SQL 对时序数据查询的原生支持。 ✨ 让数据动起来! 使用 SQL 在 GreptimeDB 上实现动态 Range 查询 https://greptime.cn/playground 🔧 点击链接加入我们的 Playground,立即体验👆 示例 我们用一个例子来介绍 Range 查询。下面这张表 temperature 记录了不同城市在不同时间的气温: 我们想查询北京从 2023 年 5 月 2 日(时间戳为 1682985600000 )以前的: 每日的日平均气温; 每日的周平均气温; 如果某天的数据点缺失,则以前后两天平均气温均值作为这天的平均气温。 我们首先从 PromQL 的视角来看如何去写这句...
- 下一篇
高精度时序分析工具PP-TSv2!一站式解决电力负荷预测、设备异常检测等多场景任务
时间序列数据在各行业和领域中无处不在,如物联网传感器的测量结果、每小时的销售额业绩、金融领域的股票价格等等,都是时间序列数据的例子。在复杂时序预测场景下,长时序、多变量、非平稳等特性严重影响模型的精度。如何高效地选择任务场景下最合适的模型,并获得最佳的结果呢?PaddleX推出了多任务场景自适应寻优的高精度时序分析工具——PP-TSv2,覆盖了时序预测和异常检测两大常见任务,支持了更多的时序任务场景。在没有额外数据的条件下,实现无痛涨点。 该工具有以下几个特点,如果正合您的需求,欢迎使用! 场景丰富:支持时序预测和时序异常检测两大通用任务。时序预测能够准确识别数据中的趋势和周期性变化,通过全面的数据洞察,提前预测未来趋势,从而做出更加明智的决策。时序异常检测能在第一时间发现数据中的异常波动,通过实时预警,迅速应对潜在问题,确保设备的稳定运行。在用电负荷预测、预测性维护、能耗分析、交通流量预估等场景中有重要应用价值。 精准度高:为处理复杂、多变的时序数据而设计,可在多种场景中实现自适应寻优,获得高精度的结果。时序预测任务,在电力场景预测误差降低了20%以上;时序异常检测,在设备异常监控场...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池