風控規則引擎(一):Java 動態指令碼

双鬼带单發表於2024-03-14

風控規則引擎(一):Java 動態指令碼

日常場景

  1. 共享單車會根據微信分或者芝麻分來判斷是否交押金
  2. 汽車租賃公司也會根據微信分或者芝麻分來判斷是否交押金
  3. 在一些外賣 APP 都會提供根據你的信用等級來發放貸款產品
  4. 金融 APP 中會根據很複雜規則來判斷使用者是否有借款資格,以及貸款金額。

在簡單的場景中,我們可以透過直接編寫一些程式碼來解決需求,比如:

// 判斷是否需要支付押金
return 芝麻分 > 650

這種方式程式碼簡單,如果規則簡單且不經常變化可以透過這種方式,在業務改變的時候,重新編寫程式碼即可。

在金融場景中,往往會根據不同的產品,不同的時間,對接的銀行等等多個維度來配置規則,單純的直接編寫程式碼無法滿足業務需求,而且編寫程式碼的方式對於運營人員來說無論實時性、視覺化都很欠缺。

在這種情況往往會引入視覺化的規則引擎,允許運營人員可以透過視覺化配置的方式來實現一套規則配置,具有實時生效、視覺化的效果。減少開發和運營的雙重負擔。

這篇主要介紹一下如何實現一個視覺化的表示式的定義和執行。

表示式的定義

在上面說到的使用場景中,可以瞭解中至少需要支援布林表示式。比如

  1. 芝麻分 > 650
  2. 居住地 不在 國外
  3. 年齡在 18 到 60 之間
  4. 名下無其他逾期借款

...

在上面的例子中,可以將一個表示式分為 3 個部分

  1. 規則引數 (ruleParam)
  2. 對應的操作 (operator)
  3. 對應操作的閾值 (args)

則可以將上面的布林表示式表示為

  1. 芝麻分 > 650
{
  "ruleParam": "芝麻分",
  "operator": "大於",
  "args": ["650"]
}
  1. 居住地 不在 國外
{
  "ruleParam": "居住地",
  "operator": "位於",
  "args": ["國內"]
}
  1. 年齡在 18 到 60 之間
{
  "ruleParam": "年齡",
  "operator": "區間",
  "args": ["18", "60"]
}
  1. 名下無其他逾期借款
{
  "ruleParam": "在途逾期數量",
  "operator": "等於",
  "args": ["0"]
}

表示式執行

上面的透過將表示式使用 json 格式定義出來,下面就是如何在執行中動態的解析這個 json 格式並執行。

有了 json 格式,可以透過以下方式來執行對應的表示式

  1. 因為表示式的結構已經定義好了,可以透過手寫程式碼來判斷所有的情況實現解釋執行, 這種方案簡單,但增加操作需要修改對應的解釋的邏輯, 且效能低
/*
{
  "ruleParam": "在途逾期數量",
  "operator": "等於",
  "args": ["0"]
}
*/
switch(operator) {
  case "等於":
    // 等於操作
    break;
  case "大於":
    // 等於操作
    break;
    ...
}
  1. 在第一次得到 json 字串的時候,直接將其根據不同的情況生成對應的 java 程式碼,並動態編譯成 Java Class,方便下一次執行,該方案依然需要處理各種情況,但因為在第一次編譯成了 java 程式碼,效能和直接編寫 java 程式碼一樣

  2. 使用第三方庫實現表示式的執行

使用第三方庫實現動態表示式的執行

在 Java 中有很多表示式引擎,常見的有

  1. jexl3
  2. mvel
  3. spring-expression
  4. QLExpress
  5. groovy
  6. aviator
  7. ognl
  8. fel
  9. 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

總結

這是寫的規則引擎的第一篇,主要講一下

  1. 如何講一個布林表示式轉換為 json 格式的定義方便做視覺化儲存和後端校驗
  2. 如何去執行一個 json 格式的表示式定義

在這裡也提供了一些不同的表示式引擎和效能測試,如果感興趣的可以去嘗試一下。

下一篇主要講一下在引擎裡面規則引數、運算子是如何設計的,也講一下視覺化圓形的設計

相關文章