之前同僚用了一款輕量級的規則引擎腳本AviatorScript,後來也跟着用了起來,真的挺香,能少寫很多代碼。今天就給大家介紹一下這款規則引擎。
簡介
AviatorScript 是一門高性能、輕量級寄宿于 JVM (包括 Android 平台)之上的腳本語言。
它起源于2010年,作者對當時已有的一些産品不是很滿意,是以自己撸了一個,它是Groovy的一個定制化的子集。
相比較一些傳統的規則引擎,比如Drools、Jess、JRules,它更加輕量級,而且性能更好,同時能力開放,擴充很友善。
我們來看(吹)看(吹)AviatorScript的特點:
- 它支援數字、字元串、正規表達式、布爾值等基本類型,并且可以使用所有 Java 運算符進行運算。
- 還有一個内置的東西叫做 bigint 和 decimal,可以處理超大整數和高精度運算。而且我們還可以通過運算符重載讓它們使用普通的算術運算符 +-*/。
- 文法非常齊全,可以用它來寫多行資料、條件語句、循環語句,還能處理詞法作用域和異常處理等等。
- 如果我們喜歡函數式程式設計,還有一個叫做 Sequence 抽象的東西,可以讓你更友善地處理集合。
- 還有一個輕量化的子產品系統,友善我們組織代碼。
- 如果我們需要調用 Java 方法,也沒問題,可以用多種方式友善地調用 Java 方法,還有一個完整的腳本 API可以讓你從 Java 調用腳本。
- 性能也是超出想象的好,如果使用 ASM 模式,它會直接将腳本翻譯成 JVM 位元組碼,解釋模式還可以在 Android 等非标準 Java 平台上運作。
AviatorScript可以用在各種場景,比如規則判斷和規則引擎、公式計算、動态腳本控制,甚至集合資料 ELT 等等。可以說相當全能了。
快速開始
AviatorScript 是一門寄生在 JVM (Hosted on the JVM)上的語言,類似 clojure/scala/kotlin 等等,我們從寫個Hello World開始。
- 建立一個SpringBoot項目,引入依賴,這裡選擇的是最新版本
java複制代碼 <dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>5.3.3</version>
</dependency>
PS:可以看到aviator的groupId有一個googlecode,但是它和Google可沒什麼關系,這是因為早期aviator托管在Google的一個開源項目托管平台Google Code。
- 在項目的resource目錄下建立一個目錄script,在script目錄下建立腳本hello.av
java複制代碼println("Hello, AviatorScript!");
- 編寫一個單元測試,運作腳本
java複制代碼 @Test
void testHello() throws Exception {
//擷取路徑
ClassPathResource resource = new ClassPathResource("script/hello.av");
String scriptPath = resource.getPath();
//編譯
Expression exp = AviatorEvaluator.getInstance().compileScript(scriptPath);
//執行
exp.execute();
}
最後執行一下,就可以看到輸出:
java複制代碼Hello, AviatorScript!
- 我們也可以直接把腳本定義成字元串,用compile()來進行編譯
java複制代碼 @Test
void testHelloStr() throws Exception {
//定義腳本
String script="println(\"Hello, AviatorScript!\");";
//編譯
Expression exp = AviatorEvaluator.getInstance().compile(script);
//執行
exp.execute();
}
AviatorScript有一個Idea插件,支援直接編譯運作Aviator腳本,比較友善。
但不足之處,這個插件已經不怎麼維護了,隻相容到了Idea2021版本。
AviatorScript腳本的運作,分為兩步,編譯和執行。
編譯支援編譯腳本檔案和腳本文本,分别使用compileScript和compile方法。
編譯産生的 Expression 對象,最終都是調用 execute() 方法執行。
這裡有個重要能力,execute 方法可以接受一個變量清單組成的 map,來注入執行的上下文:
java複制代碼 String expression = "a-(b-c) > 100";
Expression compiledExp = AviatorEvaluator.compile(expression);
//上下文
double a=100.3,b=45,c= -199.100;
Map<String, Object> context=new HashMap<>();
context.put("a",a);
context.put("b",b);
context.put("c",c);
//通過注入的上下文執行
Boolean result = (Boolean) compiledExp.execute(context);
System.out.println(result);
我們實作一些規則的判斷就是基于這個能力,把一些參數下上下文傳進去,然後進行邏輯判斷。
基本文法
我們在來看看AviatorScript的基本文法,它的文法相當簡潔,比較接近于數學表達式的形式。
基本類型及運算
AviatorScript 支援常見的類型,如數字、布爾值、字元串等等,同時将大整數、BigDecimal、正規表達式也作為一種基本類型來支援。
數字
AviatorScript 支援數字類型,包括整數和浮點數,以及高精度計算(BigDecimal)。數字類型可以進行各種算術運算。
整數和算術運算
整數類型,對應Java中的long類型,可以表示範圍為 -9223372036854774808 ~ 9223372036854774807 的整數。整數可以使用十進制或十六進制表示。
javascript複制代碼let a = 99;
let b = 0xFF;
let c = -99;
println(a + b); // 270
println(a / b); // 0
println(a - b + c); // -156
println(a + b * c); // -9801
println(a - (b - c)); // 198
println(a / b * b + a % b); // 99
整數可以進行加減乘除和取模運算。需要注意的是,整數相除的結果仍然是整數,遵循整數運算規則。可以使用括号來指定運算的優先級。
浮點數
浮點數類型對應Java中的double類型,表示雙精度 64 位浮點數。浮點數可以使用十進制或科學計數法表示。
javascript複制代碼let a = 1.34159265;
let b = 0.33333;
let c = 1e-2;
println(a + b); // 1.67492265
println(a - b); // 1.00826265
println(a * b); // 0.4471865500145
println(a / b); // 4.0257402772554
println(a + c); // 1.35159265
浮點數可以進行加減乘除運算,結果仍然為浮點數。
高精度計算(Decimal)
高精度計算使用 BigDecimal 類型,可以進行精确的數值計算,适用于貨币運算或者實體公式運算的場景。可以通過在數字後面添加 "M" 字尾來表示 BigDecimal 類型。
javascript複制代碼let a = 1.34M;
let b = 0.333M;
let c = 2e-3M;
println(a + b); // 1.673M
println(a - b); // 1.007M
println(a * b); // 0.44622M
println(a / b); // 4.022022022M
println(a + c); // 1.342M
BigDecimal 類型可以進行加減乘除運算,結果仍然為 BigDecimal 類型。預設的運算精度是 MathContext.DECIMAL128,可以通過修改引擎配置項 Options.MATH_CONTEXT 來改變。
數字類型轉換
數字類型在運算時會自動進行類型轉換:
- 單一類型參與的運算,結果仍然為該類型。
- 多種類型參與的運算,按照 long -> bigint -> decimal -> double 的順序自動提升,結果為提升後的類型。
可以使用 long(x) 函數将數字強制轉換為 long 類型,使用 double(x) 函數将數字強制轉換為 double 類型。
javascript複制代碼let a = 1;
let b = 2;
println("a/b is " + a/b); // 0
println("a/double(b) is " + a/double(b)); // 0.5
a 和 b 都是 long 類型,它們相除的結果仍然是整數。使用 double(b) 将 b 轉換為 double 類型後,相除的結果為浮點數。
字元串
字元串類型由單引号或雙引号括起來的連續字元組成。可以使用 println 函數來列印字元串。
javascript複制代碼let a = "hello world";
println(a); // hello world
字元串的長度可以通過 string.length 函數擷取。
java複制代碼let a = "hello world";
println(string.length(a)); // 11
字元串可以通過 + 運算符進行拼接。
javascript複制代碼let a = "hello world";
let b = "AviatorScript";
println(a + ", " + b + "!" + 5); // hello world, AviatorScript!5
字元串還包括其他函數,如截取字元串 substring,都在 string 這個 namespace 下,具體見函數庫清單。
布爾類型和邏輯運算
布爾類型用于表示真和假,它隻有兩個值 true 和 false 分别表示真值和假值。
比較運算如大于、小于可以産生布爾值:
javascript複制代碼println("3 > 1 is " + (3 > 1)); // 3 > 1 is true
println("3 >= 1 is " + (3 >= 1)); // 3 >= 1 is true
println("3 >= 3 is " + (3 >= 3)); // 3 >= 3 is true
println("3 < 1 is " + (3 < 1)); // 3 < 1 is false
println("3 <= 1 is " + (3 <= 1)); // 3 <= 1 is false
println("3 <= 3 is " + (3 <= 3)); // 3 <= 3 is true
println("3 == 1 is " + (3 == 1)); // 3 == 1 is false
println("3 != 1 is " + (3 != 1)); // 3 != 1 is true
上面示範了所有的邏輯運算符:
- > 大于
- >= 大于等于
- < 小于
- <= 小于等于
- == 等于
- != 不等于
基本文法
AviatorScript也支援條件語句和循環語句。
條件語句
AviatorScript 中的條件語句和其他語言沒有太大差別:
- if
java複制代碼if(true) {
println("in if body");
}
- if-else
java複制代碼if(false){
println("in if body");
} else {
println("in else body");
}
- if-elsif-else
java複制代碼let a = rand(1100);
if(a > 1000) {
println("a is greater than 1000.");
} elsif (a > 100) {
println("a is greater than 100.");
} elsif (a > 10) {
println("a is greater than 10.");
} else {
println("a is less than 10 ");
}
循環語句
AviatorScript提供了兩種循環語句:for和while。
for循環:周遊集合
for ... in 語句通常用于周遊一個集合,例如下面是周遊 0 到 9 的數字
java複制代碼for i in range(0, 10) {
println(i);
}
在這裡,range(start, end) 函數用于建立一個整數集合,包括起始值 start,但不包括結束值 end。在循環疊代過程中,變量 i 綁定到集合中的每個元素,并執行大括号 {...} 中的代碼塊。
range 函數還可以接受第三個參數,表示遞增的步長大小(預設步長為 1)。例如,我們可以列印出0到9之間的偶數:
java複制代碼for i in range(0, 10, 2) {
println(i);
}
for .. in 可以用于任何集合結構,比如數組、 java.util.List 、 java.util.Map 等等。
while循環
while 循環本質上是将條件語句與循環結合在一起。當條件為真時,不斷執行一段代碼塊,直到條件變為假。
例如,下面的示例中,變量 sum 從 1 開始,不斷累加自身,直到超過 1000 才停止,然後進行列印輸出:
java複制代碼let sum = 1;
while sum < 1000 {
sum = sum + sum;
}
println(sum);
循環可以用這三個關鍵字結束——continue/break/return:
- continue用于跳過目前疊代,繼續下一次疊代。
- break用于跳出整個循環。
- return用于中斷整個腳本(或函數)的執行并傳回。
函數
我們再來看看AviatorScript一個非常重要的特性——函數。
函數
函數定義和調用
AviatorScript中使用fn文法來定義函數:
java複制代碼fn add(x, y) {
return x + y;
}
three = add(1, 2);
println(three); // 輸出:3
s = add('hello', ' world');
println(s); // 輸出:hello world
我們這裡通過fn關鍵字來定義了一個函數,函數名為add,它接受兩個參數x和y,并傳回它們的和。
需要注意的是,AviatorScript是動态類型系統,不需要定義參數和傳回值的類型,它會根據實際傳入和傳回的值進行自動類型轉換。是以,我們可以使用字元串來調用add函數。
函數的傳回值可以通過return語句來指定,也可以省略不寫。在函數體内,如果沒有明确的return語句,最後一個表達式的值将被作為傳回值。
自定義函數
再來給大家介紹一個AviatorScript裡非常好的特性,支援自定義函數,這給AviatorScript帶來了非常強的擴充性。
可以通過 java 代碼實作并往引擎中注入自定義函數,在 AviatorScript 中就可以使用,事實上所有的内置函數也是通過同樣的方式實作的:
java複制代碼public class TestAviator {
public static void main(String[] args) {
//通通建立一個AviatorEvaluator的執行個體
AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
//注冊函數
instance.addFunction(new AddFunction());
//執行ab腳本,腳本裡調用自定義函數
Double result= (Double) instance.execute("add(1, 2)");
//輸出結果
System.out.println(result);
}
}
/**
* 實作AbstractFunction接口,就可以自定義函數
*/
class AddFunction extends AbstractFunction {
/**
* 函數調用
* @param env 目前執行的上下文
* @param arg1 第一個參數
* @param arg2 第二個參數
* @return 函數傳回值
*/
@Override
public AviatorObject call(Map<String, Object> env,
AviatorObject arg1, AviatorObject arg2) {
Number left = FunctionUtils.getNumberValue(arg1, env);
Number right = FunctionUtils.getNumberValue(arg2, env);
//将兩個參數進行相加
return new AviatorDouble(left.doubleValue() + right.doubleValue());
}
/**
* 函數的名稱
* @return 函數名
*/
public String getName() {
return "add";
}
}
我們看到:
- 繼承AbstractFunction類,就可以自定義一個函數
- 重寫call方法,就可以定義函數的邏輯,可以通過FunctionUtils擷取腳本傳遞的參數
- 通過getName可以設定函數的名稱
- 通過addFunction添加一個自定義函數類的執行個體,就可以注冊函數
- 最後就可以在Aviator的腳本裡編譯執行我們自定義的函數
好了,關于AviatorScript的文法我們就不過多介紹了,大家可以直接檢視官方文檔[1],可讀性相當不錯。
接下來我們就來看看AviatorScript的實際應用,看看它到底怎麼提升項目的靈活性。
實戰案例
标題帶了規則引擎,在我們的項目裡也主要是拿AviatorScript作為規則引擎使用——我們可以把AviatorScript的腳本維護在配置中心或者資料庫,進行動态地維護,這樣一來,一些規則的修改,就不用大動幹戈地去修改代碼,這樣就更加友善和靈活了。
用戶端版本控制
在日常的開發中,我們很多時候可能面臨這樣的情況,相容用戶端的版本,尤其是Android和iPhone,有些功能是低版本不支援的,或者說有些功能到了高版本就廢棄掉,這時候如果寫死去相容就很麻煩,那麼就可以考慮使用規則腳本的方式。
- 自定義版本比較函數:AviatorScript沒有内置版本比較函數,但是可以利用它的自定義函數特性,自己定義一個版本比較函數
java複制代碼class VersionFunction extends AbstractFunction {
@Override
public String getName() {
return "compareVersion";
}
@Override
public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
// 擷取版本
String version1 = FunctionUtils.getStringValue(arg1, env);
String version2 = FunctionUtils.getStringValue(arg2, env);
int n = version1.length(), m = version2.length();
int i = 0, j = 0;
while (i < n || j < m) {
int x = 0;
for (; i < n && version1.charAt(i) != '.'; ++i) {
x = x * 10 + version1.charAt(i) - '0';
}
++i; // 跳過點号
int y = 0;
for (; j < m && version2.charAt(j) != '.'; ++j) {
y = y * 10 + version2.charAt(j) - '0';
}
++j; // 跳過點号
if (x != y) {
return x > y ? new AviatorBigInt(1) : new AviatorBigInt(-1);
}
}
return new AviatorBigInt(0);
}
}
- 注冊自定義函數:為了友善使用各種自定義函數,我們一般定義一個單例的AviatorEvaluatorInstance,把它注冊成Bean
java複制代碼 @Bean
public AviatorEvaluatorInstance aviatorEvaluatorInstance() {
AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
// 預設開啟緩存
instance.setCachedExpressionByDefault(true);
// 使用LRU緩存,最大值為100個。
instance.useLRUExpressionCache(100);
// 注冊内置函數,版本比較函數。
instance.addFunction(new VersionFunction());
}
- 在代碼裡傳遞上下文:接下來,就可以在業務代碼裡将一些參數放進執行上下文,然然後進行編譯執行,注意編譯的時候最好要開啟緩存,這樣效率會高很多
java複制代碼 /**
*
* @param device 裝置
* @param version 版本
* @param rule 規則腳本
* @return
*/
public boolean filter(String device,String version,String rule){
// 執行參數
Map<String, Object> env = new HashMap<>();
env.put("device", device);
//編譯腳本
Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
//執行腳本
boolean isMatch = (boolean) expression.execute(env);
return isMatch;
}
- 編寫腳本:接下來就可以編寫和維護對應的規則腳本,這些規則腳本通常放在在配置中心或者資料庫,友善進行動态變更
java複制代碼if(device==bil){
return false;
}
## 控制android的版本
if (device=="Android" && compareVersion(version,"1.38.1")<0){
return false;
}
return true;
這樣一來,假如某天,用戶端Bug或者産品原因,需要修改用戶端和用戶端的版本控制,直接修改腳本就好了。
甚至我們可以在env裡放進更多參數,比如uid,可以實作簡單的黑白名單。
我們的自定義函數除了這種簡單的比較版本,我們還可以放一些複雜的邏輯,比如判斷是否新使用者等等。
營銷活動規則
假如現在我們的營運希望進行一場營銷活動,對使用者進行一定的支付優惠,最開始的一版活動規則:
- 滿1000減200,滿500減100
這個好寫,一頓if-else就完事了。
但是沒過幾天,又改了活動規則:
- 首單使用者統一減20
好,啪啪改代碼。
又過去幾天,活動規則又改了:
- 随機優惠不同金額
為了一些多變的營銷規則,大動幹戈,不停修改代碼,耗時費力,那麼不如用規則腳本實作:
- 定義腳本
java複制代碼if (amount>=100){
return 200;
}elsif(amount>=500){
return 100;
}else{
return 0;
}
- 業務代碼調用
java複制代碼 public BigDecimal getDiscount(BigDecimal amount,String rule){
// 執行規則并計算最終價格
Map<String, Object> env = new HashMap<>();
env.put("amount", amount);
Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
return (BigDecimal) expression.execute();
}
接下來,再發生營銷規則變更,就可以少量開發(自定義函數,比如判斷首單使用者),并且可以元件化地維護營銷規則。
訂單風控規則
Aviator我在訂單風控裡應用也很香,風控的規則調整是相當頻繁的,比如一個電商網站,常常要根據交易的争議率、交易表現等等,來收緊和放松風控規則,這就要求我們能對一風控規則進行快速地配置變更。
例如,根據訂單金額、客戶評級、收貨位址等屬性,自動判斷是否有風險并觸發相應的風控操作。
- 規則腳本
java複制代碼if (amount>=1000 || rating <= 2){
return "High";
}elsif(amount >= 500 || rating<=4){
return "Mid";
}else{
return "Low";
}
- 代碼調用:這裡隻是簡單傳回了一個風控等級,其實可以通過Map的方式傳回多個參數。
typescript複制代碼 public String riskLevel(BigDecimal amount,String rating,String rule){
// 執行規則并計算最終價格
Map<String, Object> env = new HashMap<>();
env.put("amount", amount);
env.put("rating", rating);
Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
return (String) expression.execute();
}
上面随手列出了幾個簡單的例子,AviatorScript 還可以用在一些審批流程、事件處理、資料品質管理等等場景……
在一些輕量級的需要規則引擎的場景下,AviatorScript 真的太香了,尤其是它的擴充性,支援通過Java自定義函數,我甚至可以在腳本裡查詢資料庫、查詢Redis、調用外部接口……這樣就可以像搭積木一樣搭建想要的功能。
總結
這一期給大家分享了一款輕量級的規則腳本語言AviatorScript,它的文法豐富,但是很輕量,并且支援非常靈活的擴充,在項目中使用可以有效提高業務的靈活性,降低開發的工作量。
作者:三分惡
連結:https://juejin.cn/post/7262179684494524475
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。