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


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


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

規(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的使用

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)用模式).

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é)碼:
- Lexer 文法分析
- Parser 語法解析
- 一趟優(yōu)化:常量折疊、常量池化等簡單優(yōu)化。
- 第二趟生成 JVM 字節(jié)碼,并最終動態(tài)生成一個匿名 Class
- 實例化 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í)行過程三個方面提供二次定制的功能擴展。

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ā)工作量非常大。