表达式引擎技术及比较
Drools 简介
Drools(JBoss Rules )是一个开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。
除了应用了 Rete 核心算法,开源软件 License 和 100% 的 Java 实现之外,Drools 还提供了很多有用的特性。其中包括实现了 JSR94 API 和创新的规则语义系统,这个语义系统可用来编写描述规则的语言。目前,Drools 提供了三种语义模块
- Python 模块
- Java 模块
- Groovy 模块
Drools 的规则是写在 drl 文件中。 对于前面的表达式,在 Drools 的 drl 文件描述为:
1 | rule "Testing Comments" |
When 表示条件,then 是满足条件以后,可以执行的动作,在这里可以调用任何 java 方法等。在 drools 不支持字符串的 contians 方法,只能采用正则表达式来代替。
IKExpression 简介
IK Expression 是一个开源的、可扩展的, 基于 java 语言开发的一个超轻量级的公式化语言解析执行工具包。IK Expression 不依赖于任何第三方的 java 库。它做为一个简单的 jar,可以集成于任意的 Java 应用中。
对于前面的表达式,IKExpression 的写法为:
1 | public static void main(String[] args) throws Throwable{ |
可以看到 IK 是通过自定义函数 $indexOf 来实现功能的。
Groovy 简介
Groovy 经常被认为是脚本语言,但是把 Groovy 理解为脚本语言是一种误解,Groovy 代码被编译成 Java 字节码,然后能集成到 Java 应用程序中或者 web 应用程序,整个应用程序都可以是 Groovy 编写的——Groovy 是非常灵活的。
Groovy 与 Java 平台非常融合,包括大量的 java 类库也可以直接在 groovy 中使用。对于前面的表达式,Groovy 的写法为:
1 | Binding binding = new Binding(); |
Aviator 简介
Aviator 是一个高性能、轻量级的 java 语言实现的表达式求值引擎,主要用于各种表达式的动态求值。现在已经有很多开源可用的 java 表达式求值引擎,为什么还需要 Avaitor 呢?
Aviator 的设计目标是轻量级和高性能,相比于 Groovy、JRuby 的笨重,Aviator 非常小,加上依赖包也才 450K,不算依赖包的话只有 70K;当然,
Aviator 的语法是受限的,它不是一门完整的语言,而只是语言的一小部分集合。
其次,Aviator 的实现思路与其他轻量级的求值器很不相同,其他求值器一般都是通过解释的方式运行,而 Aviator 则是直接将表达式编译成 Java 字节码,交给 JVM 去执行。简单来说,Aviator 的定位是介于 Groovy 这样的重量级脚本语言和 IKExpression 这样的轻量级表达式引擎之间。对于前面的表达式,Aviator 的写法为:
1 | Map<String, Object> env = Maps.newHashMap(); |
性能对比
Drools 是一个高性能的规则引擎,但是设计的使用场景和在本次测试中的场景并不太一样,Drools 的目标是一个复杂对象比如有上百上千的属性,怎么快速匹配规则,而不是简单对象重复匹配规则,因此在这次测试中结果垫底。
IKExpression 是依靠解释执行来完成表达式的执行,因此性能上来说也差强人意,和 Aviator,Groovy 编译执行相比,还是性能差距还是明显。
Aviator 会把表达式编译成字节码,然后代入变量再执行,整体上性能做得很好。
Groovy 是动态语言,依靠反射方式动态执行表达式的求值,并且依靠 JIT 编译器,在执行次数够多以后,编译成本地字节码,因此性能非常的高。对应于 eSOC 这样需要反复执行的表达式,Groovy 是一种非常好的选择。
场景实战
监控告警规则
监控规则配置效果图:
最终转化成表达式语言可以表示为:
1 | // 0.t实体逻辑如下 |
此时我们只需调用 Aviator 实现表达式执行逻辑如下:
1 | boolean hit = (Boolean) AviatorEvaluator.execute(strategyData.getLogicExpr(), env, true); |
自定义函数实战
基于上节监控中心内 triggerExec
函数如何实现
先看源码:
1 | public class AlertStrategyFunction extends AbstractAlertFunction { |
按照官方文档,只需继承 AbstractAlertFunction
,即可实现自定义函数,重点如下:
- getName() 返回 函数对应的调用名称,必须实现
- call() 方法可以重载,尾部参数可选,对应函数入参多个参数分别调用使用
实现自定义函数后,使用前需要注册,源码如下:
1 | AviatorEvaluator.addFunction(new AlertStrategyFunction()); |
如果在 Spring 项目中使用,只需在 bean 的初始化方法中调用即可。
踩坑指南 & 调优
使用编译缓存模式
默认的编译方法如 compile(script)
、 compileScript(path
以及 execute(script, env)
都不会缓存编译的结果,每次都将重新编译表达式,生成一些匿名类,然后返回编译结果 Expression
实例, execute
方法会继续调用 Expression#execute(env)
执行。
这种模式下有两个问题:
- 每次都重新编译,如果你的脚本没有变化,这个开销是浪费的,非常影响性能。
- 编译每次都产生新的匿名类,这些类会占用 JVM 方法区(Perm 或者 metaspace),内存逐步占满,并最终触发 full gc。
因此,通常更推荐启用编译缓存模式, compile
、 compileScript
以及 execute
方法都有相应的重载方法,允许传入一个 boolean cached
参数,表示是否启用缓存,建议设置为 true:
1 | public final class AviatorEvaluatorInstance { |
其中的 cacheKey
是用来指定缓存的 key,如果你的脚本特别长,默认使用脚本作为 key 会占用较多的内存并耗费 CPU 做字符串比较检测,可以使用 MD5 之类唯一的键值来降低缓存开销。
缓存管理
AviatorEvaluatorInstance
有一系列用于管理缓存的方法:
- 获取当前缓存大小,缓存的编译结果数量
getExpressionCacheSize()
- 获取脚本对应的编译缓存结果
getCachedExpression(script)
或者根据 cacheKey 获取getCachedExpressionByKey(cacheKey)
,如果没有缓存过,返回 null。 - 失效缓存
invalidateCache(script)
或者invalidateCacheByKey(cacheKey)
。 - 清空缓存
clearExpressionCache()
性能建议
- 优先使用执行优先模式(默认模式)。
- 使用编译结果缓存模式,复用编译结果,传入不同变量执行。
- 外部变量传入,优先使用编译结果的
Expression#newEnv(..args)
方法创建外部 env,将会启用符号化,降低变量访问开销。 - 生产环境切勿打开执行跟踪模式。
- 调用 Java 方法,优先使用自定义函数,其次是导入方法,最后是基于 FunctionMissing 的反射模式。
往期精彩
欢迎关注公众号:咕咕鸡技术专栏
个人技术博客:https://jifuwei.github.io/ >
参考:
[1].Drools, IKExpression, Aviator 和 Groovy 字符串表达式求值比较
[2].AviatorScript 编程指南