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

简介: Drools(JBoss Rules )是一个开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。

表达式引擎技术及比较
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;

}
复制代码
性能对比
0.png

Drools是一个高性能的规则引擎,但是设计的使用场景和在本次测试中的场景并不太一样,Drools的目标是一个复杂对象比如有上百上千的属性,怎么快速匹配规则,而不是简单对象重复匹配规则,因此在这次测试中结果垫底。
IKExpression是依靠解释执行来完成表达式的执行,因此性能上来说也差强人意,和Aviator,Groovy编译执行相比,还是性能差距还是明显。
Aviator会把表达式编译成字节码,然后代入变量再执行,整体上性能做得很好。
Groovy是动态语言,依靠反射方式动态执行表达式的求值,并且依靠JIT编译器,在执行次数够多以后,编译成本地字节码,因此性能非常的高。对应于eSOC这样需要反复执行的表达式,Groovy是一种非常好的选择。
场景实战
监控告警规则
监控规则配置效果图:
0.1.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) 执行。
这种模式下有两个问题:

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

因此,通常更推荐启用编译缓存模式, compile 、 compileScript 以及 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() 

相关文章
|
4天前
|
安全 Java 程序员
《从头开始学java,一天一个知识点》之:控制流程:if-else条件语句实战
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列为你打造Java「速效救心丸」!每天1分钟,地铁通勤、午休间隙即可完成学习。直击高频考点和实际开发中的「坑位」,拒绝冗长概念,每篇都有可运行的代码示例。明日预告:《for与while循环的使用场景》。 ---
42 19
|
6天前
|
存储 架构师 安全
深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(图解+史上最全)
锁状态bits1bit是否是偏向锁2bit锁标志位无锁状态对象的hashCode001偏向锁线程ID101轻量级锁指向栈中锁记录的指针000重量级锁指向互斥量的指针010尼恩提示,讲完 如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等优化手段 , 可以得到 120分了。如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等‌。JVM锁的膨胀、锁的内存结构变化相关的面试题,是非常常见的面试题。也是核心面试题。
深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(图解+史上最全)
|
5天前
|
SQL IDE 算法
《从头开始学java,一天一个知识点》之:运算符与表达式:算术、比较和逻辑运算
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列为你打造Java「速效救心丸」,每天1分钟,地铁通勤、午休间隙即可完成学习。直击高频考点和实际开发中的「坑位」,拒绝冗长概念,每篇都有可运行的代码示例。明日预告:《控制流程:if-else条件语句实战》。
26 6
|
1天前
|
SQL Rust Java
怎么理解Java中的lambda表达式
Lambda表达式是JDK8引入的新语法,用于简化匿名内部类的代码写法。其格式为`(参数列表) -&gt; { 方法体 }`,适用于函数式接口(仅含一个抽象方法的接口)。通过Lambda表达式,代码更简洁灵活,提升Java的表达能力。
|
6天前
|
存储 Java 编译器
课时11:综合实战:简单Java类
本次分享的主题是综合实战:简单 Java 类。主要分为两个部分: 1.简单 Java 类的含义 2.简单 Java 类的开发
|
6天前
|
Oracle Java 关系型数据库
课时37:综合实战:数据表与简单Java类映射转换
今天我分享的是数据表与简单 Java 类映射转换,主要分为以下四部分。 1. 映射关系基础 2. 映射步骤方法 3. 项目对象配置 4. 数据获取与调试
|
2月前
|
存储 缓存 Java
Java中的分布式缓存与Memcached集成实战
通过在Java项目中集成Memcached,可以显著提升系统的性能和响应速度。合理的缓存策略、分布式架构设计和异常处理机制是实现高效缓存的关键。希望本文提供的实战示例和优化建议能够帮助开发者更好地应用Memcached,实现高性能的分布式缓存解决方案。
48 9
|
3月前
|
Java
Java基础却常被忽略:全面讲解this的实战技巧!
本次分享来自于一道Java基础的面试试题,对this的各种妙用进行了深度讲解,并分析了一些关于this的常见面试陷阱,主要包括以下几方面内容: 1.什么是this 2.this的场景化使用案例 3.关于this的误区 4.总结与练习
|
3月前
|
Java 程序员
Java基础却常被忽略:全面讲解this的实战技巧!
小米,29岁程序员,分享Java中`this`关键字的用法。`this`代表当前对象引用,用于区分成员变量与局部变量、构造方法间调用、支持链式调用及作为参数传递。文章还探讨了`this`在静态方法和匿名内部类中的使用误区,并提供了练习题。
58 1
|
Java Spring 容器
java轻量级IOC框架Guice
Guice是由Google大牛Bob lee开发的一款绝对轻量级的java IoC容器。其优势在于: 速度快,号称比spring快100倍。 无外部配置(如需要使用外部可以可以选用Guice的扩展包),完全基于annotation特性,支持重构,代码静态检查。
1831 0

热门文章

最新文章