Google Aviator——轻量级 Java 表达式引擎实战

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;
}

性能对比


image.png
image.png


Drools是一个高性能的规则引擎,但是设计的使用场景和在本次测试中的场景并不太一样,Drools的目标是一个复杂对象比如有上百上千的属性,怎么快速匹配规则,而不是简单对象重复匹配规则,因此在这次测试中结果垫底。 IKExpression是依靠解释执行来完成表达式的执行,因此性能上来说也差强人意,和Aviator,Groovy编译执行相比,还是性能差距还是明显。

Aviator会把表达式编译成字节码,然后代入变量再执行,整体上性能做得很好。

Groovy是动态语言,依靠反射方式动态执行表达式的求值,并且依靠JIT编译器,在执行次数够多以后,编译成本地字节码,因此性能非常的高。对应于eSOC这样需要反复执行的表达式,Groovy是一种非常好的选择。

场景实战

监控告警规则

监控规则配置效果图:

image.png


最终转化成表达式语言可以表示为:

// 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) 执行。

这种模式下有两个问题:

  1. 每次都重新编译,如果你的脚本没有变化,这个开销是浪费的,非常影响性能。
  2. 编译每次都产生新的匿名类,这些类会占用 JVM 方法区(Perm 或者 metaspace),内存逐步占满,并最终触发 full gc。

因此,通常更推荐启用编译缓存模式, compile compileScript 以及 execute 方法都有相应的重载方法,允许传入一个 boolean cached 参数,表示是否启用缓存,建议设置为 true:

public final class AviatorEvaluatorInstance {