規(guī)則引擎

1、規(guī)則引擎是什么

在很多企業(yè)的 IT 業(yè)務(wù)系統(tǒng)中,會有大量的業(yè)務(wù)規(guī)則配置,而且隨著企業(yè)管理者的決策變化,這些業(yè)務(wù)規(guī)則也會隨之發(fā)生更改。

1.png
2.png

為了適應(yīng)這樣的需求,我們的 IT 業(yè)務(wù)系統(tǒng)應(yīng)該能快速且低成本的更新。一般的作法是將業(yè)務(wù)規(guī)則的配置單獨拿出來,使之與業(yè)務(wù)系統(tǒng)保持低耦合。

3.png
4.png

配合規(guī)則引擎提供的良好的業(yè)務(wù)規(guī)則設(shè)計器,不用編碼就可以快速實現(xiàn)復(fù)雜的業(yè)務(wù)規(guī)則,同樣,即使是完全不懂編程的業(yè)務(wù)人員,也可以輕松上手使用規(guī)則引擎來定義復(fù)雜的業(yè)務(wù)規(guī)則。

規(guī)則引擎是讓業(yè)務(wù)人士驅(qū)動整個企業(yè)過程的最佳實踐

5.png

規(guī)則引擎推理引擎發(fā)展而來,是一種嵌入在應(yīng)用程序中的組件,可以將業(yè)務(wù)決策從應(yīng)用程序中分離出來,并使用預(yù)定義的語義規(guī)范編寫業(yè)務(wù)規(guī)則。

規(guī)則引擎通過接受輸入的數(shù)據(jù),進(jìn)行業(yè)務(wù)規(guī)則的評估,并做出業(yè)務(wù)決策。

使用規(guī)則引擎可以給系統(tǒng)帶來如下優(yōu)勢:

  • 高靈活性:在規(guī)則保存在知識庫中,可以在不重新啟動系統(tǒng)的情況下發(fā)布規(guī)則,以減少測試和發(fā)布的成本。
  • 容易掌控:規(guī)則比過程代碼更易于理解,因此可以有效地來彌補業(yè)務(wù)分析師和開發(fā)人員之間的溝通問題。
  • 降低復(fù)雜度:在程序中編寫大量的判斷條件,很可能是會造成一場噩夢。使用規(guī)則引擎卻能夠通過一致的表示形式,更好的處理日益復(fù)雜的業(yè)務(wù)邏輯。
  • 可重用性:規(guī)則集中管理,可提高業(yè)務(wù)的規(guī)則的可重用性。決策結(jié)果的積累和回溯,可以反向推動規(guī)則的迭代優(yōu)化,幫助組織形成一個不斷演進(jìn)的商業(yè)智能分析知識庫。

常見的規(guī)則引擎大體上分為兩種:

  • 重量級:組件齊全,提供整套解決方案,以Drools為代表。
  • 輕量級:本質(zhì)上是一種基于JVM的腳本語言,只負(fù)責(zé)腳本的編譯、執(zhí)行,規(guī)則的定義、運維等要結(jié)合具體的業(yè)務(wù)自己開發(fā),以Groovy、AviatorScript、QLExpress、MVEL等代表

2、重量級規(guī)則引擎

Drools 是用 Java 語言編寫的開源規(guī)則引擎,是KIE(Knowledge Is Everything)項目的一部分。

Drools 具有以下優(yōu)點:

  • 非常活躍的社區(qū)
  • 生態(tài)不斷的完善中
  • JSR 94 兼容(JSR 94 是 Java Rule Engine API)
  • 免費

2.1 Rete算法

Drools基于Rete算法實現(xiàn)。

Rete算法是一種前向規(guī)則快速匹配算法,是一個用于產(chǎn)生式系統(tǒng)的高效模式匹配算法,其匹配速度與規(guī)則數(shù)目無關(guān)

Rete是拉丁文,對應(yīng)英文是net,也就是網(wǎng)絡(luò)。

產(chǎn)生式規(guī)則是一種常用的知識表示方法,它以"IF-THEN"的形式表現(xiàn)了因果關(guān)系。例如:

 R1: IF 某動物是有蹄類動物 AND 有長脖子 AND 有長腿 AND 身上有暗斑點 THEN 該動物是長頸鹿(問題解決)
 R2:IF 某動物是有蹄類動物 AND 身上有黑色條紋 THEN 該動物是斑馬(問題解決)
 ……
 R8:IF 動物是哺乳動物 AND 反芻動物 THEN 該動物是有蹄類動物
 ……
 R10:IF 某動物有奶 THEN該動物是哺乳動物……

以上一些產(chǎn)生式規(guī)則,給出"有奶"、“反芻”、“長脖子”、“長腿”、"身上有暗斑點"條件(也稱為事實 facts),就可以求解出問題的答案是“長頸鹿”。

其核心思想是用分離的匹配項構(gòu)造匹配網(wǎng)絡(luò),同時緩存中間結(jié)果,以空間換時間。有三個核心要素:

  • 事實(fact):對象之間及對象屬性之間的多元關(guān)系,可以簡單理解為對象的屬性和屬性值。
  • 規(guī)則(rule):是由條件和結(jié)論構(gòu)成的推理語句,一般表示為if...then...。一個規(guī)則的if部分稱為LHS(left-hand-side),then部分稱為RHS(right hand side)。
  • 模式(patten):就是指IF語句的條件。這里IF條件可能是有幾個更小的條件組成的大條件。模式就是指的不能在繼續(xù)分割下去的最小的原子條件。

2.2 Drools的使用

6.png

Drools規(guī)則引擎基于以下抽象組件實現(xiàn):

  • 規(guī)則(Rules):業(yè)務(wù)規(guī)則或DMN決策。所有規(guī)則必須至少包含觸發(fā)該規(guī)則的條件以及對應(yīng)的操作。
  • 事實(Facts):輸入到規(guī)則引擎的數(shù)據(jù),用于規(guī)則的條件的匹配。
  • 生產(chǎn)內(nèi)存(Production memory):規(guī)則引擎中規(guī)則存儲的地方
  • 工作內(nèi)存(Working memory):規(guī)則引擎中Fact對象存儲的地方。
  • 議程(Agenda):用于存儲被激活的規(guī)則的分類和排序的地方。

Drools的腳本需要以特定的語法編寫成drl文件。例如:

package rules

import com.clf.Order

lock-on-active true

//規(guī)則一:訂單總價在100元以下時,沒有優(yōu)惠
rule order_discount_1
    when
        $order:Order(originalPrice < 100)
    then
        $order.setRealPrice($order.getOriginalPrice());
        System.out.println("訂單折扣規(guī)則匹配,成功匹配到規(guī)則order_discount_1:訂單總價在100元以下時,沒有優(yōu)惠");
        System.out.println("訂單原價:" + $order.getOriginalPrice() + "\t折扣價:" + $order.getRealPrice());
end

//規(guī)則二:訂單總價在 [100,500) 區(qū)間時,享受滿100減30
rule order_discount_2
    when
        $order:Order(originalPrice >= 100 && originalPrice < 500)
    then
        $order.setRealPrice($order.getOriginalPrice() - 30);
        System.out.println("訂單折扣規(guī)則匹配,成功匹配到規(guī)則order_discount_2:訂單總價在 [100,500) 區(qū)間時,享受滿100減30");
        System.out.println("訂單原價:" + $order.getOriginalPrice() + "\t折扣價:" + $order.getRealPrice());
end

規(guī)則以腳本的形式存儲在一個文件中,使規(guī)則的變化不需要修改代碼,重新啟動機器即可在線上環(huán)境中生效。

如果只使用規(guī)則的執(zhí)行,引入Business Rules Engine (BRE)就夠了,編寫Java代碼和規(guī)則文件即可。如果要編排很復(fù)雜的工程,甚至整個業(yè)務(wù)都重度依賴,需要產(chǎn)品、運營同學(xué)一起來指定規(guī)則,則需要用到BRMS整套解決方案了,包括BRE、Drools Workbench、DMN等。

我們說Drools太重了,主要是在說:

  • Drools相關(guān)組件比較多,需要逐個研究才知道是否需要
  • Drools邏輯復(fù)雜,不了解原理,一旦出現(xiàn)問題排查難度高
  • Drools需要編寫規(guī)則文件,學(xué)習(xí)成本高

3、輕量級規(guī)則引擎

3.1 Groovy

3.1.1 簡介

Groovy是Apache 旗下的一種基于JVM的面向?qū)ο缶幊陶Z言,既可以用于面向?qū)ο缶幊?,也可以用作純粹的腳本語言。在語言的設(shè)計上它吸納了Python、Ruby 等腳本語言的優(yōu)秀特性,比如動態(tài)類型轉(zhuǎn)換、閉包和元編程支持。

Groovy 為 Java 開發(fā)者提供了現(xiàn)代最流行的編程語言特性,而且學(xué)習(xí)成本很低(幾乎為零)。Groovy和Java代碼的最大區(qū)別在于Groovy更靈活,語法要求更少,因此吸引了許多Java使用者。比起Java,Groovy語法更加的靈活和簡潔,可以用更少的代碼來實現(xiàn)Java實現(xiàn)的同樣功能。

在某種程度上,Groovy可以被視為Java的一種腳本化改良版。Groovy可以無縫集成所有已經(jīng)存在的 Java 對象和類庫,直接編譯成 JVM 字節(jié)碼,這樣可以在任何使用 Java 的地方使用 Groovy 。

Groovy之于Java,類似狂草之于行楷。熟悉Groovy的人開發(fā)起來猶如行云流水,但不熟悉的感覺還是在寫Java。

3.1.2 原理

Groovy 與Java 最終都是以字節(jié)碼的方式在JVM 上面執(zhí)行,兩者的編譯和加載步驟是一樣的,差異是Groovy顯式支持運行時編譯動態(tài)加載。

Groovy支持將.groovy源代碼編譯成.class字節(jié)碼文件(預(yù)編譯模式),同時又支持在運行時加載并編譯.groovy源文件(直接調(diào)用模式).

7.png

Groovy 卻是一門動態(tài)語言,可以在運行時擴展程序,比如動態(tài)調(diào)用(攔截、注入、合成)方法,那么 Groovy 是如何實現(xiàn)這一切的呢?

其實這一切都要歸功于 Groovy 編譯器,Groovy 編譯器在編譯 Groovy 代碼的時候,并不是像 Java 一樣,直接編譯成字節(jié)碼,而是編譯成 “動態(tài)調(diào)用的字節(jié)碼”。

例如下面這一段 Groovy 代碼:


package groovy

println("Hello World!")

當(dāng)我們用Groovy編譯器編譯之后,就會變成:

package groovy;

......

public class HelloGroovy extends Script {
    private static /* synthetic */ ClassInfo $staticClassInfo;
    public static transient /* synthetic */ boolean __$stMC;
    private static /* synthetic */ ClassInfo $staticClassInfo$;
    private static /* synthetic */ SoftReference $callSiteArray;
    ......
    public static void main(String ... args) {
        // 調(diào)用runScript()方法
        CallSite[] arrcallSite = HelloGroovy.$getCallSiteArray();
        arrcallSite[0].call(InvokerHelper.class, HelloGroovy.class, (Object)args);
    }

    public Object run() {
        // 調(diào)用println()方法
        CallSite[] arrcallSite = HelloGroovy.$getCallSiteArray();
        return arrcallSite[1].callCurrent((GroovyObject)this, (Object)"Hello World!");
    }
    ......
    private static /* synthetic */ void $createCallSiteArray_1(String[] arrstring) {
        arrstring[0] = "runScript";
        arrstring[1] = "println";
    }
    ......
}

```java

簡單的一行代碼,經(jīng)過 Groovy 編譯器編譯之后,變得如此復(fù)雜。而這就是 Groovy 編譯器做的,將普通的代碼編譯成可以動態(tài)調(diào)用的代碼。

不難發(fā)現(xiàn),經(jīng)過編譯之后,幾乎所有的方法調(diào)用都變成通過 `CallSite`進(jìn)行了,這個 `CallSite` 就是實現(xiàn)動態(tài)調(diào)用的入口。

我們來看看這個 CallSite 都做了什么。

```java
package org.codehaus.groovy.runtime.callsite;

/**
 * Base class for all call sites
 */
public class AbstractCallSite implements CallSite {
    ......
    // call()方法是運行時方法調(diào)用的時候才觸發(fā)的
    public Object call(Object receiver, Object arg1) throws Throwable {
        CallSite stored = this.array.array[this.index];
        return stored != this ? stored.call(receiver, arg1) : this.call(receiver, ArrayUtil.createArray(arg1));
    }
    ......
    public Object call(Object receiver, Object[] args) throws Throwable {
        return CallSiteArray.defaultCall(this, receiver, args);
    }
}

CallSite主要負(fù)責(zé)分發(fā)和緩存不同類型的方法調(diào)用邏輯,包括 callGetPropertySafe(), callGetProperty(), callGroovyObjectGetProperty(), callGroovyObjectGetPropertySafe(), call(), callCurrent(), callStatic(), callConstructor()等等。

對于不同類型的方法調(diào)用需要通過不同的 CallSite 調(diào)用,因為針對不同類型的方法需要有不同的處理邏輯,否則可能會出現(xiàn)循環(huán)調(diào)用,拋出 StackOverflow 異常。例如對于當(dāng)前對象(this)的方法調(diào)用需要通過 callCurrent(),對于static類型方法需要通過 callStatic(),而對于局部變量或者實例變量則是通過 call()。

不過由于每次執(zhí)行的時候,都會新生成一個class文件,這樣就會導(dǎo)致JVM的perm區(qū)或Metaspace持續(xù)增長,進(jìn)而導(dǎo)致FullGC問題,解決辦法就是腳本文件變化了之后才去創(chuàng)建文件,之前從緩存中獲取即可。

3.2 AviatorScript

3.2.1 簡介

AviatorScript是阿里開源的一個高性能、輕量級的Java語言實現(xiàn)的表達(dá)式求值引擎。

AviatorScript 將表達(dá)式直接翻譯成對應(yīng)的 Java 字節(jié)碼執(zhí)行,這樣就保證了它的性能超越絕大部分解釋性的表達(dá)式引擎,測試也證明如此;其次,除了依賴 commons-beanutils 這個庫之外(用于做反射)不依賴任何第三方庫,因此整體非常輕量級,整個 jar 包大小哪怕發(fā)展到現(xiàn)在 5.0 這個大版本,也才 430K。同時, Aviator 內(nèi)置的函數(shù)庫非?!肮?jié)制”,除了必須的字符串處理、數(shù)學(xué)函數(shù)和集合處理之外,類似文件 IO、網(wǎng)絡(luò)等等你都是沒法使用的,這樣能保證運行期的安全,如果你需要這些高階能力,可以通過開放的自定義函數(shù)來接入。

因此總結(jié)它的特點是:

  • 高性能
  • 輕量級
  • 一些比較有特色的特點:
    • 支持運算符重載
    • 原生支持大整數(shù)和 BigDecimal 類型及運算,并且通過運算符重載和一般數(shù)字類型保持一致的運算方式。
    • 原生支持正則表達(dá)式類型及匹配運算符
    • 類 clojure 的 seq 庫及 lambda 支持,可以靈活地處理各種集合
  • 開放能力:包括自定義函數(shù)接入以及各種定制選項

那么,既然業(yè)界已經(jīng)有 Groovy/Kotlin/Jruby 等很成熟的動態(tài)語言,為什么需要 AviatorScript 呢?

優(yōu)先使用社區(qū)廣泛使用的語言,有一個比較好的社區(qū)支持,這都是很好、很正確的考量。那么為什么還想要發(fā)展和去使用 AviatorScript? 我能想到的理由如下:

  • 你不想使用一個全功能的、相對重量級的語言,你只是做一些布爾表達(dá)式判定、數(shù)據(jù)集合處理等等,你不想引入一堆依賴,并且期待有一定的性能保證。AviatorScript 提供了大量的定制選項,甚至各種語法特性都是可以開關(guān)的。
  • 你的表達(dá)式或者 script 是用戶輸入的,你無法保證他們的安全性,你希望控制用戶能使用的 API,提供一個相對安全的運行沙箱。

3.2.2 原理

AviatorScript 編譯和執(zhí)行的入口是 AviatorEvaluatorInstance 類,該類的一個實例就是一個編譯和執(zhí)行的單元,這個單元我們稱為一個 AviatorScript 引擎。

AviatorEvaluatorInstance 接受一個腳本文件,經(jīng)過以下步驟,動態(tài)實時地編譯成 JVM 字節(jié)碼:

  1. Lexer 文法分析
  2. Parser 語法解析
  3. 一趟優(yōu)化:常量折疊、常量池化等簡單優(yōu)化。
  4. 第二趟生成 JVM 字節(jié)碼,并最終動態(tài)生成一個匿名 Class
  5. 實例化 Class,最終的 Expression 對象。

每次調(diào)用 compileScript(path) 都生成一個新的匿名類和對象,因此如果頻繁調(diào)用會占滿 JVM 的 metaspace,可能導(dǎo)致 full gc 或者 OOM,因此還有一個方法 compileScript(path, cached) 可以通過第二個布爾值參數(shù)決定是否緩存該編譯結(jié)果。

編譯產(chǎn)生的 Expression 對象,最終都是調(diào)用 execute() 方法執(zhí)行,得到結(jié)果。但是 execute 方法還可以接受一個變量列表組成的 map,來注入執(zhí)行的上下文,我們來一個例子:


    String expression = "a-(b-c) > 100";
    Expression compiledExp = AviatorEvaluator.compile(expression);
    // Execute with injected variables.
    Boolean result =
        (Boolean) compiledExp.execute(compiledExp.newEnv("a", 100.3, "b", 45, "c", -199.100));
    System.out.println(result);

我們編譯了一段腳本 a-(b-c) > 100 ,這是一個簡單的數(shù)字計算和比較,最終返回一個布爾值。a, b, c 是三個變量(后面我們將詳解變量),它們的值都是未知,沒有在腳本里明確賦值,那么可以通過外部傳參的方式,將這些變量的值注入進(jìn)去,同時求得結(jié)果,比如例子是通過 Expression#newEnv 方法創(chuàng)建了一個 Map<String, Object 的上下文 map,將 a 設(shè)置為 100.3,將 b 設(shè)置為 45,將 c 設(shè)置為 -199.100,最終代入的執(zhí)行過程如下:

a-(b-c) > 100 
=> 100.3 - (45 - -199.100) > 100
=> 100.3 - 244.1 > 100
=> -143.8 > 100
=> false

因此返回的 result 就是 false。

這是一個很典型的動態(tài)表達(dá)式求值的例子,通過復(fù)用 Expression 對象,結(jié)合不同的上下文 map,你可以對一個表達(dá)式反復(fù)求值。

同樣, compile 方法也有一個緩存模式 compile(script, cached) 用于決定是否緩存編譯結(jié)果,避免重復(fù)生成類和對象。

從 5.3 版本開始, AviatorScript 還支持了解釋執(zhí)行模式,這種模式下,將生成 AviatorScript 自身設(shè)計的指令并解釋執(zhí)行,這樣就不依賴 asm,也不會生成字節(jié)碼,在 Android 等非標(biāo)準(zhǔn) Java 平臺上就可以運行。

3.3 QLExpress

3.3.1 簡介

QLExpress由阿里的電商業(yè)務(wù)規(guī)則、表達(dá)式(布爾組合)、特殊數(shù)學(xué)公式計算(高精度)、語法分析、腳本二次定制等強需求而設(shè)計的一門動態(tài)腳本引擎解析工具。 在阿里集團(tuán)有很強的影響力,同時為了自身不斷優(yōu)化、發(fā)揚開源貢獻(xiàn)精神,于2012年開源。

QLExpress腳本引擎被廣泛應(yīng)用在阿里的電商業(yè)務(wù)場景,具有以下的一些特性:

  • 線程安全,引擎運算過程中的產(chǎn)生的臨時變量都是threadlocal類型。
  • 高效執(zhí)行,比較耗時的腳本編譯過程可以緩存在本地機器,運行時的臨時變量創(chuàng)建采用了緩沖池的技術(shù),和Groovy性能相當(dāng)。
  • 弱類型腳本語言,和groovy,javascript語法類似,雖然比強類型腳本語言要慢一些,但是使業(yè)務(wù)的靈活度大大增強。
  • 安全控制,可以通過設(shè)置相關(guān)運行參數(shù),預(yù)防死循環(huán)、高危系統(tǒng)api調(diào)用等情況。
  • 代碼精簡,依賴最小,250k的jar包適合所有java的運行環(huán)境,在android系統(tǒng)的低端pos機也得到廣泛運用。

3.3.2 原理

來看一個簡單的例子。

ExpressRunner runner = new ExpressRunner(false, true); //打印執(zhí)行編譯過程
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
String express = "a + b * c";
Object r = runner.execute(express, context, null, true, true); //打印指令執(zhí)行過程
System.out.println(r);

打印日志如下:

DEBUG com.ql.util.express.parse.ExpressParse - 執(zhí)行的表達(dá)式:a + b * c
DEBUG com.ql.util.express.parse.ExpressParse - 單詞分解結(jié)果:{a},{+},,{*},{c}
DEBUG com.ql.util.express.parse.ExpressParse - 預(yù)處理后結(jié)果:{a},{+},,{*},{c}
DEBUG com.ql.util.express.parse.ExpressParse - 單詞分析結(jié)果:a:ID,+:+,b:ID,*:*,c:ID
DEBUG com.ql.util.express.parse.ExpressParse - 最后的語法樹:
1:   STAT_BLOCK:STAT_BLOCK                                                          STAT_BLOCK
2:      STAT_SEMICOLON:STAT_SEMICOLON   STAT_SEMICOLON
3:         +:+  +
4:            a:ID  ID
4:            *:*   *
5:               b:ID   ID
5:               c:ID   ID

DEBUG com.ql.util.express.ExpressRunner - 
1:LoadAttr:a
2:LoadAttr:b
3:LoadAttr:c
4:OP : * OPNUMBER[2]
5:OP : + OPNUMBER[2]

DEBUG com.ql.util.express.instruction.detail.Instruction - LoadAttr:a:1
DEBUG com.ql.util.express.instruction.detail.Instruction - LoadAttr:b:2
DEBUG com.ql.util.express.instruction.detail.Instruction - LoadAttr:c:3
DEBUG com.ql.util.express.instruction.detail.Instruction - *(b:2,c:3)
DEBUG com.ql.util.express.instruction.detail.Instruction - +(a:1,6)
7

由這個簡單的例子,我們看到了整個QL的執(zhí)行過程:

單詞分解-->單詞類型分析-->語法分析-->生成運行期指令集合-->執(zhí)行生成的指令集合。

其中前4個過程涉及語法的匹配運算等非常耗時,所以我們看到了 execute 方法的 isCache 是否使用Cache中的指令集參數(shù),它可以緩存前四個過程。即把 expressString 本地緩存乘一段指令,第二次重復(fù)執(zhí)行的時候直接執(zhí)行指令,極大的提高了性能。

QLExpressRunner如下圖所示,從語法樹分析、上下文、執(zhí)行過程三個方面提供二次定制的功能擴展。

8.jpeg

3.4 MVEL

3.4.1 簡介

MVEL為 MVFLEX Expression Language(MVFLEX表達(dá)式語言)的縮寫,是一種基于Java語法,可嵌入的表達(dá)式語言。

MVEL簡單說就是一種表達(dá)式解析器。我們可以自己寫一些表達(dá)式,交給MVEL進(jìn)行解析計算,得到這個表達(dá)式計算的值。MVEL可以用來解析復(fù)雜的JavaBean表達(dá)式,還可以方便地調(diào)用java的類,函數(shù)等
。Java Runtime(運行時)允許MVEL表達(dá)式通過解釋執(zhí)行或者預(yù)編譯執(zhí)行

目前最新的版本是2.0,具有以下特性:

  • 動態(tài)JIT優(yōu)化器。當(dāng)負(fù)載超過一個確保代碼產(chǎn)生的閾值時,選擇性地產(chǎn)生字節(jié)代碼,這大大減少了內(nèi)存的使用量。
  • 新的靜態(tài)類型檢查和屬性支持,允許集成類型安全表達(dá)。
  • 錯誤報告的改善。包括行和列的錯誤信息。
  • 新的腳本語言特征。MVEL2.0 包含函數(shù)定義,如:閉包,lambda定義,標(biāo)準(zhǔn)循環(huán)構(gòu)造(for, while, do-while, do-until…),空值安全導(dǎo)航操作,內(nèi)聯(lián)with-context運營 ,易變的(isdef)的測試運營等等。
  • 改進(jìn)的集成功能。迎合主流的需求,MVEL2.0支持基礎(chǔ)類型的個性化屬性處理器,集成到JIT中。
  • 更快的模板引擎,支持線性模板定義,宏定義和個性化標(biāo)記定義。
  • 新的交互式shell(MVELSH)。

Drools當(dāng)中就集成了MVEL,用于動態(tài)代碼的生成。

3.4.2 原理

MVEL在執(zhí)行語言時主要有解釋模式(Interpreted Mode)和Java Runtime(運行時)(Compiled Mode )兩種。

解釋模式(Interpreted Mode)是一個無狀態(tài)的,動態(tài)解釋執(zhí)行,不需要負(fù)載表達(dá)式就可以執(zhí)行相應(yīng)的腳本。

//解釋模式
Foo foo = new Foo();
foo.setName("test");

Map context = new HashMap();
context.put("foo",foo);

String expression = "foo.name == 'test'";
VariableResolverFactory functionFactory = new MapVariableResolverFactory(context);

Boolean result = (Boolean) MVEL.eval(expression,functionFactory);

編譯模式(Compiled Mode)需要在緩存中產(chǎn)生一個完全規(guī)范化表達(dá)式之后再執(zhí)行。

//編譯模式
Foo foo = new Foo();
foo.setName("test");

Map context = new HashMap();
String expression = "foo.name == 'test'";

VariableResolverFactory functionFactory = new MapVariableResolverFactory(context);
context.put("foo",foo);

Serializable compileExpression = MVEL.compileExpression(expression);

Boolean result = (Boolean) MVEL.executeExpression(compileExpression, context, functionFactory);

默認(rèn)情況下,MVEL的優(yōu)化器有:

  • 反射優(yōu)化器
  • ASM字節(jié)碼優(yōu)化器
  • 動態(tài)優(yōu)化器

優(yōu)化器Optimizers通常只使用于編譯模式,而不考慮在eval解釋模式下。

由于MVEL是動態(tài)運行時的動態(tài)語言,所以需要通過反射的對象讓腳本訪問字段和方法。但這嚴(yán)重影響性能,MVEL配備優(yōu)化,為了最大限度地減少或消除反射調(diào)用的開銷。

反射優(yōu)化器在一些api中也被稱為SAFE_REFLECTIVE優(yōu)化器,表示它是絕對安全的,不會對類加載造成影響,保證兼容所有的語言結(jié)構(gòu)。

ASM優(yōu)化器可能會在某些情況下,由于各種原因不能被編譯成字節(jié)碼,對于某些操作會依靠這個優(yōu)化器。
優(yōu)化器的配置可以通過 OptimizerFactory 進(jìn)行配置:

public static String SAFE_REFLECTIVE = "reflective";
OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE);

ASM字節(jié)碼優(yōu)化器在MVEL是默認(rèn)啟用。它使用一個內(nèi)聯(lián)版本的ASM3.0字節(jié)碼操作庫產(chǎn)生編譯反射訪問器存根用于反射調(diào)用的地方。

優(yōu)化器效率對比:

MVEL.eval [無預(yù)熱] Costs: 668
MVEL.eval [預(yù)熱后] Costs: 509
MVEL.compileExpression [無預(yù)熱] Costs: 67
MVEL.compileExpression [預(yù)熱后] Costs: 33
MVEL.compileExpression + dynamic [無預(yù)熱] Costs: 31
MVEL.compileExpression + dynamic  [預(yù)熱后] Costs: 29
MVEL.compileExpression + reflective  [無預(yù)熱] Costs: 38
MVEL.compileExpression + reflective [預(yù)熱后] Costs: 33
MVEL.compileExpression + ASM [無預(yù)熱] Costs: 33
MVEL.compileExpression + ASM [預(yù)熱后] Costs: 29

3.5 總結(jié)

除了以上四個,實際上還有很多類似的腳本語言,各有優(yōu)缺點,可以結(jié)合自己的業(yè)務(wù)特點選擇。

基于以上腳本語言,可以實現(xiàn)規(guī)則的熱加載,不用重新啟動就可改變代碼的執(zhí)行邏輯。例如可以將腳本片段用前端組件進(jìn)行組合,后臺拼裝為執(zhí)行片段存儲到數(shù)據(jù)庫以及緩存中,執(zhí)行時實時查詢出來進(jìn)行加載和實例化并執(zhí)行。

在實現(xiàn)了以上功能后,再自行實現(xiàn)規(guī)則的組合和決策流的編排等功能,即可形成一個比較完整的規(guī)則引擎。

這樣做的優(yōu)點是整個規(guī)則引擎是自行實現(xiàn),可擴展和靈活性比較強。不過缺點也比較明顯,就是前后端基礎(chǔ)的開發(fā)工作量非常大。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容