Google Aviator——轻量级 Java 表达式引擎实战
表达式引擎技术及比较
Drools 简介
Drools(JBoss Rules )是一个开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。
除了应用了 Rete 核心算法,开源软件 License 和 100% 的Java实现之外,Drools还提供了很多有用的特性。其中包括实现了JSR94 API和创新的规则语义系统,这个语义系统可用来编写描述规则的语言。目前,Drools提供了三种语义模块
- Python模块
- Java模块
- Groovy模块
Drools的规则是写在drl文件中。 对于前面的表达式,在Drools的drl文件描述为:
rule "Testing Comments"
// this is a single line comment
eval( true ) // this is a comment in the same line of a pattern
// this is a comment inside a semantic code block
end
When表示条件,then是满足条件以后,可以执行的动作,在这里可以调用任何java方法等。在drools不支持字符串的contians方法,只能采用正则表达式来代替。
IKExpression 简介
IK Expression 是一个开源的、可扩展的, 基于java 语言开发的一个超轻量级的公式化语言解析执行工具包。IK Expression 不依赖于任何第三方的 java 库。它做为一个简单的jar,可以集成于任意的Java 应用中。
对于前面的表达式,IKExpression 的写法为:
public static void main(String[] args) throws Throwable{
E2Say obj = new E2Say();
FunctionLoader.addFunction("indexOf",
obj,
E2Say.class.getMethod("indexOf",
String.class,
String.class));
System.out.println(ExpressionEvaluator.evaluate("$indexOf(\"abcd\",\"ab\")==0?1:0"));
}
可以看到 IK 是通过自定义函数 $indexOf 来实现功能的。
Groovy简介
Groovy经常被认为是脚本语言,但是把 Groovy 理解为脚本语言是一种误解,Groovy 代码被编译成 Java 字节码,然后能集成到 Java 应用程序中或者 web 应用程序,整个应用程序都可以是 Groovy 编写的——Groovy 是非常灵活的。
Groovy 与 Java 平台非常融合,包括大量的java类库也可以直接在groovy中使用。对于前面的表达式,Groovy的写法为:
Binding binding = new Binding();
binding.setVariable("verifyStatus", 1);
GroovyShell shell = new GroovyShell(binding);
boolean result = (boolean) shell.evaluate("verifyStatus == 1");
Assert.assertTrue(result);
Aviator简介
Aviator是一个高性能、轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值。现在已经有很多开源可用的java表达式求值引擎,为什么还需要Avaitor呢?
Aviator的设计目标是轻量级和高性能,相比于Groovy、JRuby的笨重,Aviator非常小,加上依赖包也才450K,不算依赖包的话只有70K;当然,
Aviator的语法是受限的,它不是一门完整的语言,而只是语言的一小部分集合。
其次,Aviator的实现思路与其他轻量级的求值器很不相同,其他求值器一般都是通过解释的方式运行,而Aviator则是直接将表达式编译成Java字节码,交给JVM去执行。简单来说,Aviator的定位是介于Groovy这样的重量级脚本语言和IKExpression这样的轻量级表达式引擎之间。对于前面的表达式,Aviator的写法为:
Map<String, Object> env = Maps.newHashMap();
env.put(STRATEGY_CONTEXT_KEY, context);
// triggerExec(t1) && triggerExec(t2) && triggerExec(t3)
log.info("### guid: {} logicExpr: [ {} ], strategyData: {}",
strategyData.getGuid(), strategyData.getLogicExpr(), JSON.toJSONString(strategyData));
boolean hit = (Boolean) AviatorEvaluator.execute(strategyData.getLogicExpr(), env, true);
if (Objects.isNull(strategyData.getGuid())) {
//若guid为空,为check告警策略,直接返回
log.info("### strategyData: {} check success", strategyData.getName());
return;
}
性能对比
Drools是一个高性能的规则引擎,但是设计的使用场景和在本次测试中的场景并不太一样,Drools的目标是一个复杂对象比如有上百上千的属性,怎么快速匹配规则,而不是简单对象重复匹配规则,因此在这次测试中结果垫底。 IKExpression是依靠解释执行来完成表达式的执行,因此性能上来说也差强人意,和Aviator,Groovy编译执行相比,还是性能差距还是明显。
Aviator会把表达式编译成字节码,然后代入变量再执行,整体上性能做得很好。
Groovy是动态语言,依靠反射方式动态执行表达式的求值,并且依靠JIT编译器,在执行次数够多以后,编译成本地字节码,因此性能非常的高。对应于eSOC这样需要反复执行的表达式,Groovy是一种非常好的选择。
场景实战
监控告警规则
监控规则配置效果图:
最终转化成表达式语言可以表示为:
// 0.t实体逻辑如下
"indicatorCode": "test001",
"operator": ">=",
"threshold": 1.5,
"aggFuc": "sum",
"interval": 5,
"intervalUnit": "minute",
// 1.规则命中表达式
triggerExec(t1) && triggerExec(t2) && (triggerExec(t3) || triggerExec(t4))
// 2.单个 triggerExec 执行内部
indicatorExec(indicatorCode) >= threshold
此时我们只需调用 Aviator 实现表达式执行逻辑如下:
boolean hit = (Boolean) AviatorEvaluator.execute(strategyData.getLogicExpr(), env, true);
if (hit) {
// 告警
}
自定义函数实战
基于上节监控中心内
triggerExec
函数如何实现
先看源码:
public class AlertStrategyFunction extends AbstractAlertFunction {
public static final String TRIGGER_FUNCTION_NAME = "triggerExec";
@Override
public String getName() {
return TRIGGER_FUNCTION_NAME;
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
AlertStrategyContext strategyContext = getFromEnv(STRATEGY_CONTEXT_KEY, env, AlertStrategyContext.class);
AlertStrategyData strategyData = strategyContext.getStrategyData();
AlertTriggerService triggerService = ApplicationContextHolder.getBean(AlertTriggerService.class);
Map<String, AlertTriggerData> triggerDataMap = strategyData.getTriggerDataMap();
AviatorJavaType triggerId = (AviatorJavaType) arg1;
if (CollectionUtils.isEmpty(triggerDataMap) || !triggerDataMap.containsKey(triggerId.getName())) {
throw new RuntimeException("can't find trigger config");
Boolean res = triggerService.executor(strategyContext, triggerId.getName());
return AviatorBoolean.valueOf(res);
}
按照官方文档,只需继承
AbstractAlertFunction
,即可实现自定义函数,重点如下:
- getName() 返回 函数对应的调用名称,必须实现
- call() 方法可以重载,尾部参数可选,对应函数入参多个参数分别调用使用
实现自定义函数后,使用前需要注册,源码如下:
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:
public final class AviatorEvaluatorInstance {