AviatorScript 是一門高性能、輕量級寄宿于 JVM (包括 Android 平臺)之上的腳本語言。
它起源于2010年,作者對當時已有的一些產品不是很滿意,所以自己擼了一個,它是Groovy的一個定制化的子集。
圖片
相比較一些傳統的規則引擎,比如Drools、Jess、JRules,它更加輕量級,而且性能更好,同時能力開放,擴展很方便。
我們來看(吹)看(吹)AviatorScript的特點:
AviatorScript可以用在各種場景,比如規則判斷和規則引擎、公式計算、動態腳本控制,甚至集合數據 ELT 等等。可以說相當全能了。
AviatorScript 是一門寄生在 JVM (Hosted on the JVM)上的語言,類似 clojure/scala/kotlin 等等,我們從寫個Hello World開始。
<dependency> <groupId>com.googlecode.aviator</groupId> <artifactId>aviator</artifactId> <version>5.3.3</version> </dependency>
PS:可以看到aviator的groupId有一個googlecode,但是它和Google可沒什么關系,這是因為早期aviator托管在Google的一個開源項目托管平臺Google Code。
println("Hello, AviatorScript!");
@Test void testHello() throws Exception { //獲取路徑 ClassPathResource resource = new ClassPathResource("script/hello.av"); String scriptPath = resource.getPath(); //編譯 Expression exp = AviatorEvaluator.getInstance().compileScript(scriptPath); //執行 exp.execute(); }
最后執行一下,就可以看到輸出:
Hello, AviatorScript!
@Test void testHelloStr() throws Exception { //定義腳本 String script="println(/"Hello, AviatorScript!/");"; //編譯 Expression exp = AviatorEvaluator.getInstance().compile(script); //執行 exp.execute(); }
AviatorScript有一個Idea插件,支持直接編譯運行Aviator腳本,比較方便。
Aviator插件
但不足之處,這個插件已經不怎么維護了,只兼容到了Idea2021版本。
Idea插件
AviatorScript腳本的運行,分為兩步,編譯和執行。
編譯執行
編譯支持編譯腳本文件和腳本文本,分別使用compileScript和compile方法。
編譯產生的 Expression 對象,最終都是調用 execute() 方法執行。
這里有個重要能力,execute 方法可以接受一個變量列表組成的 map,來注入執行的上下文:
String expression = "a-(b-c) > 100"; Expression compiledExp = AviatorEvaluator.compile(expression); //上下文 double a=100.3,b=45,c= -199.100; Map<String, Object> cnotallow=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 的整數。整數可以使用十進制或十六進制表示。
let a = 99;let b = 0xFF;let c = -99;println(a + b); // 270println(a / b); // 0println(a - b + c); // -156println(a + b * c); // -9801println(a - (b - c)); // 198println(a / b * b + a % b); // 99
整數可以進行加減乘除和取模運算。需要注意的是,整數相除的結果仍然是整數,遵循整數運算規則。可以使用括號來指定運算的優先級。
浮點數類型對應Java中的double類型,表示雙精度 64 位浮點數。浮點數可以使用十進制或科學計數法表示。
let a = 1.34159265;let b = 0.33333;let c = 1e-2;println(a + b); // 1.67492265println(a - b); // 1.00826265println(a * b); // 0.4471865500145println(a / b); // 4.0257402772554println(a + c); // 1.35159265
浮點數可以進行加減乘除運算,結果仍然為浮點數。
高精度計算使用 BigDecimal 類型,可以進行精確的數值計算,適用于貨幣運算或者物理公式運算的場景。可以通過在數字后面添加 "M" 后綴來表示 BigDecimal 類型。
let a = 1.34M;let b = 0.333M;let c = 2e-3M;println(a + b); // 1.673Mprintln(a - b); // 1.007Mprintln(a * b); // 0.44622Mprintln(a / b); // 4.022022022Mprintln(a + c); // 1.342M
BigDecimal 類型可以進行加減乘除運算,結果仍然為 BigDecimal 類型。默認的運算精度是 MathContext.DECIMAL128,可以通過修改引擎配置項 Options.MATH_CONTEXT 來改變。
數字類型在運算時會自動進行類型轉換:
可以使用 long(x) 函數將數字強制轉換為 long 類型,使用 double(x) 函數將數字強制轉換為 double 類型。
let a = 1;let b = 2;println("a/b is " + a/b); // 0println("a/double(b) is " + a/double(b)); // 0.5
a 和 b 都是 long 類型,它們相除的結果仍然是整數。使用 double(b) 將 b 轉換為 double 類型后,相除的結果為浮點數。
字符串類型由單引號或雙引號括起來的連續字符組成。可以使用 println 函數來打印字符串。
let a = "hello world";println(a); // hello world
字符串的長度可以通過 string.length 函數獲取。
let a = "hello world";println(string.length(a)); // 11
字符串可以通過 + 運算符進行拼接。
let a = "hello world";let b = "AviatorScript";println(a + ", " + b + "!" + 5); // hello world, AviatorScript!5
字符串還包括其他函數,如截取字符串 substring,都在 string 這個 namespace 下,具體見函數庫列表。
布爾類型用于表示真和假,它只有兩個值 true 和 false 分別表示真值和假值。
比較運算如大于、小于可以產生布爾值:
println("3 > 1 is " + (3 > 1)); // 3 > 1 is trueprintln("3 >= 1 is " + (3 >= 1)); // 3 >= 1 is trueprintln("3 >= 3 is " + (3 >= 3)); // 3 >= 3 is trueprintln("3 < 1 is " + (3 < 1)); // 3 < 1 is falseprintln("3 <= 1 is " + (3 <= 1)); // 3 <= 1 is falseprintln("3 <= 3 is " + (3 <= 3)); // 3 <= 3 is trueprintln("3 == 1 is " + (3 == 1)); // 3 == 1 is falseprintln("3 != 1 is " + (3 != 1)); // 3 != 1 is true
上面演示了所有的邏輯運算符:
AviatorScript也支持條件語句和循環語句。
AviatorScript 中的條件語句和其他語言沒有太大區別:
if(true) { println("in if body");}
if(false){ println("in if body");} else { println("in else body");}
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 ... in 語句通常用于遍歷一個集合,例如下面是遍歷 0 到 9 的數字
for i in range(0, 10) { println(i);}
在這里,range(start, end) 函數用于創建一個整數集合,包括起始值 start,但不包括結束值 end。在循環迭代過程中,變量 i 綁定到集合中的每個元素,并執行大括號 {...} 中的代碼塊。
range 函數還可以接受第三個參數,表示遞增的步長大小(默認步長為 1)。例如,我們可以打印出0到9之間的偶數:
for i in range(0, 10, 2) { println(i);}
for .. in 可以用于任何集合結構,比如數組、 java.util.List 、 java.util.Map 等等。
while 循環本質上是將條件語句與循環結合在一起。當條件為真時,不斷執行一段代碼塊,直到條件變為假。
例如,下面的示例中,變量 sum 從 1 開始,不斷累加自身,直到超過 1000 才停止,然后進行打印輸出:
let sum = 1;while sum < 1000 { sum = sum + sum;}println(sum);
循環可以用這三個關鍵字結束——continue/break/return:
我們再來看看AviatorScript一個非常重要的特性——函數。
AviatorScript中使用fn語法來定義函數:
fn add(x, y) { return x + y;}three = add(1, 2);println(three); // 輸出:3s = add('hello', ' world');println(s); // 輸出:hello world
我們這里通過fn關鍵字來定義了一個函數,函數名為add,它接受兩個參數x和y,并返回它們的和。
需要注意的是,AviatorScript是動態類型系統,不需要定義參數和返回值的類型,它會根據實際傳入和返回的值進行自動類型轉換。因此,我們可以使用字符串來調用add函數。
函數的返回值可以通過return語句來指定,也可以省略不寫。在函數體內,如果沒有明確的return語句,最后一個表達式的值將被作為返回值。
再來給大家介紹一個AviatorScript里非常好的特性,支持自定義函數,這給AviatorScript帶來了非常強的擴展性。
可以通過 java 代碼實現并往引擎中注入自定義函數,在 AviatorScript 中就可以使用,事實上所有的內置函數也是通過同樣的方式實現的:
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"; }}
我們看到:
好了,關于AviatorScript的語法我們就不過多介紹了,大家可以直接查看官方文檔[1],可讀性相當不錯。
接下來我們就來看看AviatorScript的實際應用,看看它到底怎么提升項目的靈活性。
標題帶了規則引擎,在我們的項目里也主要是拿AviatorScript作為規則引擎使用——我們可以把AviatorScript的腳本維護在配置中心或者數據庫,進行動態地維護,這樣一來,一些規則的修改,就不用大動干戈地去修改代碼,這樣就更加方便和靈活了。
圖片
Aviator應用
在日常的開發中,我們很多時候可能面臨這樣的情況,兼容客戶端的版本,尤其是Android和iPhone,有些功能是低版本不支持的,或者說有些功能到了高版本就廢棄掉,這時候如果硬編碼去兼容就很麻煩,那么就可以考慮使用規則腳本的方式。
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); } }
@Bean public AviatorEvaluatorInstance aviatorEvaluatorInstance() { AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance(); // 默認開啟緩存 instance.setCachedExpressionByDefault(true); // 使用LRU緩存,最大值為100個。 instance.useLRUExpressionCache(100); // 注冊內置函數,版本比較函數。 instance.addFunction(new VersionFunction()); }
/** * * @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; }
if(device==bil){ return false;}## 控制android的版本if (device=="Android" && compareVersion(version,"1.38.1")<0){ return false;}return true;
這樣一來,假如某天,客戶端Bug或者產品原因,需要修改客戶端和客戶端的版本控制,直接修改腳本就好了。
甚至我們可以在env里放進更多參數,比如uid,可以實現簡單的黑白名單。
我們的自定義函數除了這種簡單的比較版本,我們還可以放一些復雜的邏輯,比如判斷是否新用戶等等。
假如現在我們的運營希望進行一場營銷活動,對用戶進行一定的支付優惠,最開始的一版活動規則:
這個好寫,一頓if-else就完事了。
但是沒過幾天,又改了活動規則:
好,啪啪改代碼。
又過去幾天,活動規則又改了:
為了一些多變的營銷規則,大動干戈,不停修改代碼,耗時費力,那么不如用規則腳本實現:
if (amount>=100){ return 200;}elsif(amount>=500){ return 100;}else{ return 0;}
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我在訂單風控里應用也很香,風控的規則調整是相當頻繁的,比如一個電商網站,常常要根據交易的爭議率、交易表現等等,來收緊和放松風控規則,這就要求我們能對一風控規則進行快速地配置變更。
例如,根據訂單金額、客戶評級、收貨地址等屬性,自動判斷是否有風險并觸發相應的風控操作。
if (amount>=1000 || rating <= 2){ return "High";}elsif(amount >= 500 || rating<=4){ return "Mid";}else{ return "Low";}
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,它的語法豐富,但是很輕量,并且支持非常靈活的擴展,在項目中使用可以有效提高業務的靈活性,降低開發的工作量。
本文鏈接:http://www.www897cc.com/showinfo-26-5131-0.html這款輕量級規則引擎,真香!
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 彈性研發團隊的探索