表达式引擎技能及比较

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 的写法为:

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

功能比照

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

Drools是一个高功能的规矩引擎,可是设计的运用场景和在本次测试中的场景并不太相同,Drools的目标是一个复杂对象比如有上百上千的属性,怎样快速匹配规矩,而不是简略对象重复匹配规矩,因而在这次测试中成果垫底。 IKExpression是依托解说履行来完成表达式的履行,因而功能上来说也差强人意,和Aviator,Groovy编译履行比较,仍是功能距离仍是显着。

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

Groovy是动态言语,依托反射办法动态履行表达式的求值,并且依托JIT编译器,在履行次数够多今后,编译成本地字节码,因而功能十分的高。对应于eSOC这样需求重复履行的表达式,Groovy是一种十分好的选择。

场景实战

监控告警规矩

监控规矩装备效果图:

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

终究转化成表达式言语能够表明为:

// 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。

因而,一般更推荐启用编译缓存形式, compilecompileScript以及 execute办法都有相应的重载办法,允许传入一个 boolean cached参数,表明是否启用缓存,主张设置为 true:

public final class AviatorEvaluatorInstance {
  public Expression compile(final String expression, final boolean cached)
  public Expression compile(final String cacheKey, final String expression, final boolean cached)
  public Expression compileScript(final String path, final boolean cached) throws IOException
  public Object execute(final String expression, final Map<String, Object> env,
      final boolean cached)      
}

其间的 cacheKey是用来指定缓存的 key,如果你的脚本特别长,默许运用脚本作为 key 会占用较多的内存并耗费 CPU 做字符串比较检测,能够运用 MD5 之类仅有的键值来下降缓存开支。

缓存办理

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

  • 获取当前缓存巨细,缓存的编译成果数量 getExpressionCacheSize()
  • 获取脚本对应的编译缓存成果 getCachedExpression(script)或许根据 cacheKey 获取 getCachedExpressionByKey(cacheKey),如果没有缓存过,返回 null。
  • 失效缓存 invalidateCache(script)或许 invalidateCacheByKey(cacheKey)
  • 清空缓存 clearExpressionCache()

功能主张

  • 优先运用履行优先形式(默许形式)。
  • 运用编译成果缓存形式,复用编译成果,传入不同变量履行。
  • 外部变量传入,优先运用编译成果的 Expression#newEnv(..args)办法创立外部 env,将会启用符号化,下降变量拜访开支。
  • 生产环境切勿打开履行跟踪形式。
  • 调用 Java 办法,优先运用自定义函数,其次是导入办法,最后是根据 FunctionMissing 的反射形式。

往期精彩

  • 功能调优——小小的log大大的坑
  • 功能优化必备——火焰图
  • Flink 在风控场景实时特征落地实战

欢迎重视大众号:咕咕鸡技能专栏 个人技能博客:jifuwei.github.io/

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

参考:

  • [1].Drools, IKExpression, Aviator和Groovy字符串表达式求值比较
  • [2].AviatorScript 编程攻略