表达式引擎技术与比较Drools介绍Drools(JBossRules)是一个开源的业务规则引擎,符合行业标准,快速高效。它允许业务分析师或审阅者轻松查看业务规则以验证编码规则是否执行所需的业务规则。除了应用Rete核心算法、开源软件许可证和100%Java实现之外,Drools还提供了许多有用的功能。其中包括JSR94API的实现和可用于编写描述规则的语言的创新规则语义系统。目前,Drools提供了三种语义模块Python模块Java模块Groovy模块Drools的规则都写在drl文件中。对于前面的表达式,Drools中的drl文件描述是:rule"TestingComments"when//thisisasinglelinecommenteval(true)//thisisacommentinthesamelineofapatternthen//thisisacomment在一个语义代码块里面,endWhen表示条件,then是满足条件后可以执行的动作,这里可以调用任何java方法。drools不支持字符串的contians方法,只能用正则表达式代替。IKEExpression简介IKExpression是一个基于java语言开发的开源、可扩展、超轻量级的公式语言解析与执行工具包。IKExpression不依赖于任何第三方java库。它以一个简单的jar形式出现,可以集成到任何Java应用程序中。对于前面的表达式,IKEExpression写为:publicstaticvoidmain(String[]args)throwsThrowable{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这样的重量级脚本语言和IKEExpression这样的轻量级表达式引擎之间。对于前面的表达式,Aviator写成:Mapenv=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为空,则为校验告警策略,直接返回log.info("###strategyData:{}检查成功",strategyData.getName());return;}性能对比Drools是一个高性能的规则引擎,但是设计的使用场景和本次测试的不一样。Drools的目标是成百上千个属性这样的复杂对象,如何快速匹配规则,而不是针对简单对象重复匹配规则,所以本次测试的结果在最下面。IKEExpression依赖解释和执行来完成表达式的执行,所以性能并不理想。与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实现表达式执行逻辑如下:booleanhit=(Boolean)AviatorEvaluator.execute(strategyData.getLogicExpr(),env,true);if(hit){//Alert}自定义函数的实战基于上一节监控中心triggerExec函数的实现。先看源码:publicclassAlertStrategyFunctionextendsAbstractAlertFunction{publicstaticfinalStringTRIGGER_FUNCTION_NAME="triggerExec";@OverridepublicStringgetName(){返回TRIGGER_FUNCTION_NAME;}@OverridepublicAviatorObjectcall(Mapenv,AviatorObjectarg1){AlertStrategyContextstrategyContext=getFromEnv(STRATEGY_CONTEXT_KEY,env,AlertStrategyContext.class);AlertStrategyDatastrategyData=strategyContext.getStrategyData();AlertTriggerServicetriggerService=ApplicationContextHolder.getBean(AlertTriggerService.class);MaptriggerDataMap=strategyData.getTriggerDataMap();AviatorJavaTypetriggerId=(AviatorJavaType)arg1;if(CollectionUtils.isEmpty(triggerDataMap)||!triggerDataMap.containsKey(triggerId.getName())){thrownewRuntimeException("找不到触发器配置");}布尔值res=triggerService。执行者(strategyContext,triggerId.getName());返回AviatorBoolean.valueOf(res);}}按照官方文档,只需要继承AbstractAlertFunction就可以实现一个自定义函数。关键点如下:getName()返回函数对应的调用名,call()方法必须实现,tail参数可选。分别调用对应的函数入参实现自定义函数,使用前需要注册。源码如下:AviatorEvaluator.addFunction(newAlertStrategyFunction());如果在春天项目中使用,只要在bean的初始化方法中调用就可以踩坑guide&tuning使用编译缓存方式compile(script),compileScript(pathandexecute(script,env)等默认编译方式都会没有缓存编译结果每次都会重新编译表达式,生成一些匿名类,然后返回编译结果Expression实例,execute方法会继续调用Expression#execute(env)执行。有这种模式有两个问题:每次Compilation,如果你的脚本没有变化,这个开销很浪费,而且很影响性能。每次Compilation都会产生新的匿名类,这些类会占用JVM方法区(Perm或metaspace),内存会逐渐填满,最终触发fullgc。因此,通常建议开启编译缓存模式。compile、compileScript和execute方法都有相应的重载方法,允许传入一个布尔缓存参数,表示是否启用缓存。建议设置为true:publicfinalclassAviatorEvaluatorInstance{publicExpressioncompile(finalStringexpression,finalbooleancached)publicExpressioncompile(finalStringcacheKey,finalStringexpression,finalbooleancached)publicExpressioncompileScript(finalStringpath,finalbooleancached)throwsIOExceptionpublicObjectexecute(finalStringexpression,finalMapenv,finalbooleancached)}其中cacheKey用于指定缓存键。如果你的脚本很长,默认使用脚本作为key会占用更多的内存,消耗CPU进行字符串比较检测。使用像MD5这样的唯一键值来减少缓存开销。缓存管理AviatorEvaluatorInstance有一系列管理缓存的方法:获取当前缓存大小,缓存编译结果个数如果没有缓存,则返回null。使缓存无效invalidateCache(script)或invalidateCacheByKey(cacheKey)。清空缓存clearExpressionCache()建议优先使用执行优先模式(默认模式)以提高性能。使用编译结果缓存方式,可以复用编译结果,传入不同的变量执行。传入外部变量,首先使用编译结果的Expression#newEnv(..args)方法创建外部env,这样可以实现符号化,减少变量访问开销。不要在生产环境中启用执行跟踪模式。调用Java方法,先使用自定义函数,再使用引入方法,最后使用基于FunctionMissing的反射方式。精彩的性能调优——小日志,大坑,性能优化必不可少——火焰图Flink实现了风控场景下的实时特性。欢迎关注公众号:咕咕鸡技术专栏个人技术博客:https://jifuwei.github.io/参考资料:[1]。Drools、IKEExpression、Aviator和Groovy字符串表达式求值比较[2]。AviatorScript编程指南