風控規則引擎(一):Java 動態指令碼
日常場景
- 共享單車會根據微信分或者芝麻分來判斷是否交押金
- 汽車租賃公司也會根據微信分或者芝麻分來判斷是否交押金
- 在一些外賣 APP 都會提供根據你的信用等級來發放貸款產品
- 金融 APP 中會根據很複雜規則來判斷使用者是否有借款資格,以及貸款金額。
在簡單的場景中,我們可以透過直接編寫一些程式碼來解決需求,比如:
// 判斷是否需要支付押金
return 芝麻分 > 650
這種方式程式碼簡單,如果規則簡單且不經常變化可以透過這種方式,在業務改變的時候,重新編寫程式碼即可。
在金融場景中,往往會根據不同的產品,不同的時間,對接的銀行等等多個維度來配置規則,單純的直接編寫程式碼無法滿足業務需求,而且編寫程式碼的方式對於運營人員來說無論實時性、視覺化都很欠缺。
在這種情況往往會引入視覺化的規則引擎,允許運營人員可以透過視覺化配置的方式來實現一套規則配置,具有實時生效、視覺化的效果。減少開發和運營的雙重負擔。
這篇主要介紹一下如何實現一個視覺化的表示式的定義和執行。
表示式的定義
在上面說到的使用場景中,可以瞭解中至少需要支援布林表示式。比如
- 芝麻分 > 650
- 居住地 不在 國外
- 年齡在 18 到 60 之間
- 名下無其他逾期借款
...
在上面的例子中,可以將一個表示式分為 3 個部分
- 規則引數 (ruleParam)
- 對應的操作 (operator)
- 對應操作的閾值 (args)
則可以將上面的布林表示式表示為
- 芝麻分 > 650
{
"ruleParam": "芝麻分",
"operator": "大於",
"args": ["650"]
}
- 居住地 不在 國外
{
"ruleParam": "居住地",
"operator": "位於",
"args": ["國內"]
}
- 年齡在 18 到 60 之間
{
"ruleParam": "年齡",
"operator": "區間",
"args": ["18", "60"]
}
- 名下無其他逾期借款
{
"ruleParam": "在途逾期數量",
"operator": "等於",
"args": ["0"]
}
表示式執行
上面的透過將表示式使用 json 格式定義出來,下面就是如何在執行中動態的解析這個 json 格式並執行。
有了 json 格式,可以透過以下方式來執行對應的表示式
- 因為表示式的結構已經定義好了,可以透過手寫程式碼來判斷所有的情況實現解釋執行, 這種方案簡單,但增加操作需要修改對應的解釋的邏輯, 且效能低
/*
{
"ruleParam": "在途逾期數量",
"operator": "等於",
"args": ["0"]
}
*/
switch(operator) {
case "等於":
// 等於操作
break;
case "大於":
// 等於操作
break;
...
}
-
在第一次得到 json 字串的時候,直接將其根據不同的情況生成對應的 java 程式碼,並動態編譯成 Java Class,方便下一次執行,該方案依然需要處理各種情況,但因為在第一次編譯成了 java 程式碼,效能和直接編寫 java 程式碼一樣
-
使用第三方庫實現表示式的執行
使用第三方庫實現動態表示式的執行
在 Java 中有很多表示式引擎,常見的有
- jexl3
- mvel
- spring-expression
- QLExpress
- groovy
- aviator
- ognl
- fel
- jsel
這裡簡單介紹一下 jexl3 和 aviator 的使用
jexl3 在 apache commons-jexl3 中,該表示式引擎比較符合人的書寫習慣,其會判斷操作的型別,並將引數轉換成對應的型別比如 3 > 4 和 "3" > 4 這兩個的執行結果是一樣的
aviator 是一個高效能的 Java 的表示式型別,其要求確定引數的型別,比如上面的 "3" > 4 在 aviator 是無法執行的。
jexl3 更適合讓運營手動編寫的情況,能容忍一些錯誤情況;aviator 適合開發來使用,使用確定的型別引數來提供效能
jexl3 使用
加入依賴
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl3</artifactId>
<version>3.2.1</version>
</dependency>
// 建立一個帶有快取 jexl 表示式引擎,
JexlEngine JEXL = new JexlBuilder().cache(1000).strict(true).create();
// 根據表示式字串來建立一個關於年齡的規則
JexlExpression ageExpression = JEXL.createExpression("age > 18 && age < 60");
// 獲取需要的引數,java 程式碼太長了,簡寫一下
Map<String, Object> parameters parameters = {"age": 30}
// 執行一下
JexlContext jexlContext = new MapContext(parameters);
boolean result = (boolean) executeExpression.evaluate(jexlContext);
以上就會 jexl3 的簡單使用
aviator
引入依賴
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>5.3.1</version>
</dependency>
Expression ageExpression = executeExpression = AviatorEvaluator.compile("age > 18 && age < 60");
// 獲取需要的引數,java 程式碼太長了,簡寫一下
Map<String, Object> parameters parameters = {"age": 30}
boolean result = (boolean) ageExpression.execute(parameters);
注意 aviator 是強型別的,需要注意傳入 age 的型別,如果 age 是字串型別需要進行型別轉換
效能測試
不同表示式引擎的效能測試
Benchmark Mode Cnt Score Error Units
Empty thrpt 3 1265642062.921 ± 142133136.281 ops/s
Java thrpt 3 22225354.763 ± 12062844.831 ops/s
JavaClass thrpt 3 21878714.150 ± 2544279.558 ops/s
JavaDynamicClass thrpt 3 18911730.698 ± 30559558.758 ops/s
GroovyClass thrpt 3 10036761.622 ± 184778.709 ops/s
Aviator thrpt 3 2871064.474 ± 1292098.445 ops/s
Mvel thrpt 3 2400852.254 ± 12868.642 ops/s
JSEL thrpt 3 1570590.250 ± 24787.535 ops/s
Jexl thrpt 3 1121486.972 ± 76890.380 ops/s
OGNL thrpt 3 776457.762 ± 110618.929 ops/s
QLExpress thrpt 3 385962.847 ± 3031.776 ops/s
SpEL thrpt 3 245545.439 ± 11896.161 ops/s
Fel thrpt 3 21520.546 ± 16429.340 ops/s
GroovyScript thrpt 3 91.827 ± 106.860 ops/s
總結
這是寫的規則引擎的第一篇,主要講一下
- 如何講一個布林表示式轉換為 json 格式的定義方便做視覺化儲存和後端校驗
- 如何去執行一個 json 格式的表示式定義
在這裡也提供了一些不同的表示式引擎和效能測試,如果感興趣的可以去嘗試一下。
下一篇主要講一下在引擎裡面規則引數、運算子是如何設計的,也講一下視覺化圓形的設計