关于java:Google-Aviator轻量级-Java-表达式引擎实战

41次阅读

共计 5603 个字符,预计需要花费 15 分钟才能阅读完成。

表达式引擎技术及比拟

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 办法,只能采纳正则表达式来代替。
<!–more–>

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

这种模式下有两个问题:

  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 的反射模式。

往期精彩

集体技术博客:https://jifuwei.github.io/
公众号:是咕咕鸡

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

参考:
[1].Drools, IKExpression, Aviator 和 Groovy 字符串表达式求值比拟
[2].AviatorScript 编程指南

正文完
 0