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"
when
    // this is a single line comment
    eval( true ) // this is a comment in the same line of a pattern
then
    // 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 的写法为:

publicstaticvoidmain(String[] args)throws Throwable{
    E2Sayobj=newE2Say();
    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的写法为:

Bindingbinding=newBinding();
binding.setVariable("verifyStatus", 1);
GroovyShellshell=newGroovyShell(binding);
booleanresult= (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));

booleanhit= (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 函数如何实现

先看源码:

publicclassAlertStrategyFunctionextendsAbstractAlertFunction {

    publicstaticfinalStringTRIGGER_FUNCTION_NAME="triggerExec";

    @Overridepublic String getName() {
        return TRIGGER_FUNCTION_NAME;
    }

    @Overridepublic AviatorObject call(Map<String, Object> env, AviatorObject arg1) {
        AlertStrategyContextstrategyContext= getFromEnv(STRATEGY_CONTEXT_KEY, env, AlertStrategyContext.class);
        AlertStrategyDatastrategyData= strategyContext.getStrategyData();
        AlertTriggerServicetriggerService= ApplicationContextHolder.getBean(AlertTriggerService.class);

        Map<String, AlertTriggerData> triggerDataMap = strategyData.getTriggerDataMap();
        AviatorJavaTypetriggerId= (AviatorJavaType) arg1;
        if (CollectionUtils.isEmpty(triggerDataMap) || !triggerDataMap.containsKey(triggerId.getName())) {
            thrownewRuntimeException("can't find trigger config");
        }

        Booleanres= triggerService.executor(strategyContext, triggerId.getName());
        return AviatorBoolean.valueOf(res);
    }
}
复制代码

按照官方文档,只需继承 AbstractAlertFunction ,即可实现自定义函数,重点如下:

  • getName() 返回 函数对应的调用名称,必须实现

  • call() 方法可以重载,尾部参数可选,对应函数入参多个参数分别调用使用

实现自定义函数后,使用前需要注册,源码如下:

AviatorEvaluator.addFunction(newAlertStrategyFunction());
复制代码

如果在 Spring 项目中使用,只需在 bean 的初始化方法中调用即可。

踩坑指南 & 调优

使用编译缓存模式

默认的编译方法如 compile(script) 、 compileScript(path 以及 execute(script, env) 都不会缓存编译的结果,每次都将重新编译表达式,生成一些匿名类,然后返回编译结果 Expression 实例, execute 方法会继续调用 Expression#execute(env) 执行。

这种模式下有两个问题:

  1. 每次都重新编译,如果你的脚本没有变化,这个开销是浪费的,非常影响性能。

  1. 编译每次都产生新的匿名类,这些类会占用 JVM 方法区(Perm 或者 metaspace),内存逐步占满,并最终触发 full gc。

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

publicfinalclassAviatorEvaluatorInstance {
  public Expression compile(final String expression, finalboolean cached)public Expression compile(final String cacheKey, final String expression, finalboolean cached)public Expression compileScript(final String path, finalboolean cached)throws IOException
  public Object execute(final String expression, final Map<String, Object> env,
      finalboolean cached)      
}
复制代码

其中的 cacheKey 是用来指定缓存的 key,如果你的脚本特别长,默认使用脚本作为 key 会占用较多的内存并耗费 CPU 做字符串比较检测,可以使用 MD5 之类唯一的键值来降低缓存开销。

缓存管理

AviatorEvaluatorInstance 有一系列用于管理缓存的方法:

  • 获取当前缓存大小,缓存的编译结果数量 getExpressionCacheSize()

  • 获取脚本对应的编译缓存结果 getCachedExpression(script) 或者根据 cacheKey 获取 getCachedExpressionByKey(cacheKey) ,如果没有缓存过,返回 null。

  • 失效缓存 invalidateCache(script) 或者 invalidateCacheByKey(cacheKey) 。

  • 清空缓存 clearExpressionCache()

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值