表達(dá)式引擎技術(shù)介紹及比較
()是一個開源的業(yè)務(wù)規(guī)則引擎,符合行業(yè)標(biāo)準(zhǔn),快速高效。 它允許業(yè)務(wù)分析師或第一審閱者輕松查看業(yè)務(wù)規(guī)則,從而驗(yàn)證編碼規(guī)則是否實(shí)現(xiàn)了所需的業(yè)務(wù)規(guī)則。
除了應(yīng)用 Rete 的核心算法、開源軟件和 100% Java 實(shí)現(xiàn)外,它還提供了許多有用的功能。 其中包括JSR94 API的實(shí)現(xiàn)和創(chuàng)新的規(guī)則語義系統(tǒng),可以用來編寫描述規(guī)則的語言。目前提供了三個語義模塊
規(guī)則寫在drl文件中。 對于上面的表達(dá)式,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
復(fù)制代碼
when表示條件,then是滿足條件后可以執(zhí)行的動作機(jī)械效率的定義及表達(dá)式,這里可以調(diào)用任何java技術(shù)。 在不支持字符串的方式下,只能用正則表達(dá)式代替。
介紹
IK是一個基于java語言開發(fā)的開源、可擴(kuò)展、超輕量級的公式語言分析和執(zhí)行工具包。 IK 不依賴于任何第三方 java 庫。 它以一個簡單的 jar 形式出現(xiàn),可以集成到任何 Java 應(yīng)用程序中。
對于上面的表達(dá)式,寫法是:
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"));
}
復(fù)制代碼
可以看出IK是通過自定義函數(shù)$來實(shí)現(xiàn)功能的。
介紹
它通常被認(rèn)為是一種腳本語言,將其理解為腳本語言是一種誤解。 代碼被編譯成 Java 字節(jié)碼,然后可以集成到 Java 應(yīng)用程序或 Web 應(yīng)用程序中。 可以編譯整個應(yīng)用程序。 - 非常靈活。
特別是與Java平臺集成,包括大量的java泛型也可以直接在里面使用。 對于上面的表達(dá)式,寫法是:
Binding binding = new Binding();
binding.setVariable("verifyStatus", 1);
GroovyShell shell = new GroovyShell(binding);
boolean result = (boolean) shell.evaluate("verifyStatus == 1");
Assert.assertTrue(result);
復(fù)制代碼
介紹
是一個用java語言實(shí)現(xiàn)的高性能、輕量級的表達(dá)式求值引擎,主要用于各種表達(dá)式的動態(tài)求值。 已經(jīng)有很多開源的 java 表達(dá)式求值引擎可用,為什么我們需要它們?
JRuby 的設(shè)計目標(biāo)是輕量級和高性能。 相對于JRuby的笨重機(jī)械效率的定義及表達(dá)式,它非常小巧,依賴包只有450K,不包含依賴包也只有70K; 實(shí)際上,
句型有限,不是完整的語言,只是語言集中的一小部分。
其次,實(shí)現(xiàn)思路與其他輕量級評估器有很大不同。 其他的求值器通常以解釋的形式運(yùn)行,而是直接將表達(dá)式編譯成Java字節(jié)碼,交給JVM執(zhí)行。 . 簡單來說,語言的定位是介于這樣一個重量級的腳本語言和這樣一個輕量級的表達(dá)式引擎之間。 對于上面的表達(dá)式,寫法是:
Map 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;
}
復(fù)制代碼
性能比較
是一個高性能的規(guī)則引擎,設(shè)計的使用場景和本次測試的場景不一樣。 目標(biāo)是具有數(shù)百或數(shù)千個屬性的復(fù)雜對象,如何快速匹配規(guī)則,而不是簡單的對象重復(fù)匹配規(guī)則,因此在本次測試中排在最后。 表達(dá)式的執(zhí)行是通過解釋和執(zhí)行的方式完成的,所以表現(xiàn)不盡如人意。 與編譯執(zhí)行相比,性能差異還是很明顯的。
它會將表達(dá)式編譯成字節(jié)碼,然后在執(zhí)行前將其代入一個變量。 整體表現(xiàn)相當(dāng)不錯。
它是一種動態(tài)語言,借助反射的方式動態(tài)地執(zhí)行表達(dá)式求值,并借助于JIT編譯器,在執(zhí)行足夠的次數(shù)后,將其編譯成本地字節(jié)碼,因此性能非常高。 對于需要反復(fù)執(zhí)行的表達(dá)式,比如eSOC,是非常好的選擇。
場景實(shí)戰(zhàn)監(jiān)控告警規(guī)則
監(jiān)控規(guī)則配置效果圖:
最終轉(zhuǎn)化為表達(dá)語言可以表示為:
// 0.t實(shí)體邏輯如下
{
"indicatorCode": "test001",
"operator": ">=",
"threshold": 1.5,
"aggFuc": "sum",
"interval": 5,
"intervalUnit": "minute",
...
}
// 1.規(guī)則命中表達(dá)式
triggerExec(t1) && triggerExec(t2) && (triggerExec(t3) || triggerExec(t4))
// 2.單個 triggerExec 執(zhí)行內(nèi)部
indicatorExec(indicatorCode) >= threshold
復(fù)制代碼
此時,我們只需要調(diào)用實(shí)現(xiàn)表達(dá)式,執(zhí)行邏輯如下:
boolean hit = (Boolean) AviatorEvaluator.execute(strategyData.getLogicExpr(), env, true);
if (hit) {
// 告警
}
復(fù)制代碼
自定義函數(shù)實(shí)踐

如何在上一節(jié)的基礎(chǔ)上實(shí)現(xiàn)監(jiān)控中心的內(nèi)部功能
先看源碼:
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 env, AviatorObject arg1) {
AlertStrategyContext strategyContext = getFromEnv(STRATEGY_CONTEXT_KEY, env, AlertStrategyContext.class);
AlertStrategyData strategyData = strategyContext.getStrategyData();
AlertTriggerService triggerService = ApplicationContextHolder.getBean(AlertTriggerService.class);
Map 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);
}
}
復(fù)制代碼
根據(jù)官方文檔,只需要繼承n就可以實(shí)現(xiàn)自定義功能,關(guān)鍵點(diǎn)如下:
實(shí)現(xiàn)自定義功能后,需要先注冊后才能使用。 源代碼如下:
AviatorEvaluator.addFunction(new AlertStrategyFunction());
復(fù)制代碼
如果在項(xiàng)目中使用,只需要在bean的初始化方法中調(diào)用即可。
踩坑手冊&使用編譯緩存模式調(diào)優(yōu)
(,(path,(,env)等默認(rèn)編譯方法不會緩存編譯結(jié)果,每次都會重新編譯表達(dá)式,生成一些匿名類,然后返回編譯結(jié)果實(shí)例,trick會繼續(xù)調(diào)用#(環(huán)境)實(shí)施。
這些模式有兩個問題:
每次都重新編譯,如果你的腳本沒有改變,這種開銷是浪費(fèi)的,尤其會影響性能。 每次編譯都會形成一個新的匿名類,這個類會占用JVM模式區(qū)(Perm或),顯存會逐漸填滿,最終會觸發(fā)。
為此,一般建議開啟編譯緩存模式,方法有對應(yīng)的重載方法,允許傳入一個參數(shù)來表示是否開啟緩存。 建議設(shè)置為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 env,
final boolean cached)
}
復(fù)制代碼
其中就是用來指定緩存的key。 如果你的腳本很長,默認(rèn)使用腳本作為key會占用更多的顯存和消耗CPU進(jìn)行字符串比較測量。 您可以使用唯一通配符(例如 MD5)來增加緩存成本。 .
緩存管理
ance有一系列管理緩存的方式:
過去的性能建議很精彩
