一个基于AviatorScript的简单规则引擎实例。将规则划分为规则集、规则、条件三个维度,可以满足业务规则的通用性配置,同时也可以进行扩展。
此项目同时包含完整的后台管理页面(使用Next.js + Material UI开发),也可自行替换。
逻辑类型 | 值 | 描述 |
---|---|---|
AND | && | 如果当前条件单元为真,则继续向后执行,否则跳过后面的条件单元,直接返回假 |
OR | || | 如果当前条件单元为真,则跳过后面的条件单元直接返回真,否则继续向后执行 |
条件类型 | 描述 |
---|---|
EQUAL | 等于(=) |
NOT_EQUAL | 不等于(!=) |
LESS_EQUAL | 小于等于(<=) |
GREATER_EQUAL | 大于等于(>=) |
LESS | 小于(<) |
GREATER | 大于(>) |
条件类型 | 描述 |
---|---|
INCLUDE_IN_LIST | 列表包含指定字符串 |
NOT_INCLUDE_IN_LIST | 列表不包含指定字符串 |
SOME_CONTAINS_IN_LIST | 列表包含指定字符串的某一部分 |
NONE_CONTAINS_IN_LIST | 列表不包含指定字符串的任何部分 |
提示: 列表作为参考值(
referenceValue
)传入时,多个元素之间以逗号(,
)进行分隔。例如:
1,2,3
,字符1,字符2,字符3
。
条件类型 | 描述 |
---|---|
STRING_CONTAINS | 包含指定字符 |
STRING_NOT_CONTAINS | 不包含指定字符 |
STRING_STARTSWITH | 以指定字符开始 |
STRING_NOT_STARTSWITH | 不以指定字符开始 |
STRING_ENDSWITH | 以指定字符结束 |
STRING_NOT_ENDSWITH | 不以指定字符结束 |
条件类型 | 描述 |
---|---|
INTERVAL_NUMBER | 数值区间 |
INTERVAL_STRING_LENGTH | 字符长度区间 |
提示: 区间作为参考值(
referenceValue
)传入时,和数学中的区间表示形式和含义相同。例如:
[1,10]
是闭区间,表示:x >= 1 && x <= 10
;(1,10)
是开区间,表示:x > 1 && x < 10
;[1,10)
是左闭右开区间,表示:x >= 1 && x < 10
;(1,10]
是左开右闭区间,表示:x > 1 && x <= 10
。
条件类型 | 描述 |
---|---|
REGEX | 正则 |
提示: 正则表达式作为参考值(
referenceValue
)传入时,语法和Java完全一致。但形式上有略微差异,AviatorScript中的正则表达式需要以/
括起来,并且对于需要转义的字符不需要连续的反斜杠\\
,只要一个\
即可。此处做了兼容,如果参考值配置的正则表达式首尾不包含
/
时,将自动添加。
逻辑类型 | 值 | 描述 |
---|---|---|
AND | && | 关联类型:在优先级相邻的两个规则中,优先级较高的规则和优先级较低的规则同时匹配时,才返回对应的返回值。否则返回null或规则集设置的默认返回值(如果已设置)。 |
XOR | 互斥类型:在优先级相邻的两个规则中,优先级较高的规则匹配时,直接返回对应的返回值,不再验证后面的规则;否则,验证优先级较低的规则是否满足,如果满足则返回对应的返回值。不满足则返回null或规则集设置的默认返回值(如果已设置)。 |
条件是最小的执行单元,例如:x >= 99
。
多个条件可以按照关系运算类型、逻辑运算类型组合成一个规则。例如:x >= 99 || y < 45
。
当然单个条件本身也是一个规则,可以认为x >= 99
等同于x >= 99 && true
。
条件优先级决定了条件之间的组合次序和执行顺序。同一个规则下所有条件按优先级进行排序,优先级越高,组合次序和执行顺序越靠前。(同一规则下,不能存在优先级相同的条件,保证执行顺序唯一。)
例如,定义一个规则下的条件表达式及优先级如下:
条件表达式 | 条件逻辑运算符 | 条件优先级 |
---|---|---|
x == 1 | || | 8 |
y > 2 | && | 4 |
z <= 3 | && | 2 |
v != 4 | || | 1 |
最后生成的规则表达式为:
((x ==1 || y>2) && z <= 3) && v != 4
同时注意,在每两个条件的组合两侧添加了括号,保证了括号内条件的优先级。
每个条件都附带逻辑运算类型,当两个条件之间进行组合时,高优先级条件的逻辑运算作用于低优先级条件,而低优先级条件的逻辑运算作用于条件组合整体。
以如下表达式为例:
((x == 1 || y > 2) && z <= 3) && v != 4
x == 1
逻辑运算类型为||
,作用于条件y > 2
。y > 2
逻辑运算类型为&&
,由于和左侧条件x == 1
进行组合,所以作用于右侧条件z <= 3
。z <= 3
逻辑运算类型为&&
,由于和左侧条件组合(x == 1 || y> 2)
进行组合,所以作用于右侧条件v != 4
。v != 4
优先级最低,在生成规则表达式时将会自动忽略其逻辑运算类型。v != 4
逻辑运算类型为||
,则整个表达式等价于(((x == 1 || y > 2) && z <= 3) && v != 4) || false
;假设条件v != 4
逻辑运算类型为&&
,则整个表达式等价于(((x == 1 || y > 2) && z <= 3) && v != 4) && true
。每个条件都附带条件关系类型,用于和参考值进行比较。
例如条件x >= 99
,x
是条件的变量名,作为参数传入的唯一标识符,>=
是条件的关系运算符(对应的关系运算类型为,ConditionRelationType.EQUAL
),99
是条件的参考值,用于和以x
为变量名的参数值进行对应的关系运算。
多个条件经过组合之后就是一个规则,一个规则下附带一个条件列表,在生成规则表达式时,条件列表中的所有条件将按优先级从高到低进行组合。
规则优先级的含义和条件优先级相似。规则优先级决定了多个规则之间的组合顺序和结构。
每条规则都附带逻辑运算类型,以两个规则的组合为例,规则逻辑运算的作用机制说明如下:
AND
),则高优先级规则的逻辑运算作用于低优先级规则,低优先级规则的逻辑运算不影响规则组合,但低优先级规则设置的返回值集合即为规则组合的返回值集合(高优先级规则设置的返回值集合被覆盖);XOR
),则高优先级规则和低优先级规则的逻辑运算互不影响,返回值集合也不会被任何一方覆盖;以如下表达式为例:
(a == 1|| b == 2) && (c == 3 || d == 4)
从表面上看,这是一个规则表达式,将其拆分为4个条件(最小执行单元)后分别为a == 1
、b ==2
、c ==3
、d ==4
,同一规则下的多个条件组合时按照优先级从高到低进行组合,组合顺序是唯一的。但结合表达式来看,此处的表达式出现了两个组合顺序,即:(a == 1|| b == 2)
和(c == 3 || d == 4)
,因此不能作为一条规则进行组合。
我们将其作为两个规则(R1,R2)
进行组合,这两条规则下的条件列表分别为a == 1
、b ==2
和c ==3
、d ==4
,对应的两个规则表达式为R1:a == 1|| b == 2
,R2:c == 3 || d == 4
。
其中R1优先级设置为100,逻辑运算类型设置为关联类型(AND
),返回值集合和R2相同即可(R1在两个规则的组合之间优先级最高,返回值集合被R2覆盖);
R2优先级设置为98,逻辑运算类型为AND和XOR均可(R2在两个规则的组合之间优先级最低,逻辑运算不作用于其他任何规则),返回值集合设置为:{'myvariable':true}
。
将R1、R2添加到一个规则集下,最终生成表达式为(a == 1|| b == 2) && (c == 3 || d == 4)
,对应生成的aviator执行脚本为:
let rmap = seq.map('myvariable', false);
if((a == 1 || b == 2) && (c == 3 || d == 4)){
seq.put(rmap, 'myvariable', true);
}
return rmap;
规则集是规则引擎执行的对象,一个规则集下包含一个或多个规则,默认添加的规则集处于RulesetMode.BUILDING
模式,表示当前的规则集表达式未生成,规则集处于不可用状态。当规则集下存在规则,并且每个规则下存在条件时,规则集自动切换为RulesetMode.BUILT
模式,并且生成规则集表达式。
规则集支持设置默认返回值。在设置规则时,可以设置当前规则匹配时的返回值,对于规则集下所有规则均不匹配的情况,可以在规则集设置默认返回值defaultReturnValues
,数据格式和在规则下设置返回值集合相同(e.g. {'returnVariable1':returnValue1,'returnVariable2':returnValue2}
)。
如下为一个产品费率计算规则:
年龄(AGE) | 额度(AMT) | 费用(PREM) | 费率(RATE)‰ |
---|---|---|---|
0~3岁 | 1000000 | 67.9 | 0.0679 |
4~11岁 | 1000000 | 198.2 | 0.1982 |
12岁 | 1000000 | 18 | 0.018 |
需要按上述规则,通过年龄、额度来动态计算0~12岁的儿童产品的费用和费率。
规则集是规则引擎执行的对象,因此首先创建一个规则集,在规则集添加三条规则,在每个规则中配置对应的条件,具体的过程如下:
指定规则集编码为RULEST_RATE_CALC
,参数如下:
{
"code": "RULEST_RATE_CALC",
"name": "ruleset for rate calculate",
"defaultReturnValues": "{PREM:0.00,RATE:0.00}"
}
其中code
是规则集编码,用于标识一个规则集,必须唯一且不可重复。name
是这个规则集的名称。defaultReturnValues
是这个规则集定义的默认返回值集合,如果规则集下面的规则都不匹配,那么规则引擎执行当前规则集的返回结果就是其默认返回值集合。
返回值集合的格式是一个map集合的json字符串。
返回值集合的含义就是规则引擎接口返回的对应的map集合。如果上述规则集下所有规则都不匹配,那么规则引擎最终返回的结果就是:
{
"PREM": 0.00,
"RATE": 0.00
}
通过返回值集合中定义的返回值变量名PREM
和RATE
,可以从map集合中获取对应的返回值。
在当前例子中,需要添加三组规则,参数如下:
{
"rulesetId": 2,
"name": "rule for age(0-3)",
"returnValues": "{PREM:67.9,RATE:0.0679}",
"logicType": "XOR",
"priority": 100
}
{
"rulesetId": 2,
"name": "rule for age(4-11)",
"returnValues": "{PREM:198.2,RATE:0.1982}",
"logicType": "XOR",
"priority": 99
}
{
"rulesetId": 2,
"name": "rule for age(12)",
"returnValues": "{PREM:18,RATE:0.018}",
"logicType": "XOR",
"priority": 98
}
其中rulesetId
是当前规则所属规则集的主键id,name
是规则的名称,returnValues
是为当前规则匹配时定义的返回值集合,logicType
是规则逻辑运算类型,priority
是规则的优先级。
rule for age(0-3)
[
{
"ruleId": 2,
"name": "children age 0~3",
"variableName": "AGE",
"referenceValue": "[0,3]",
"relationType": "INTERVAL_NUMBER",
"logicType": "AND",
"priority": 10
},
{
"ruleId": 2,
"name": "amount total",
"variableName": "AMT",
"referenceValue": "1000000",
"relationType": "EQUAL",
"logicType": "AND",
"priority": 8
}
]
rule for age(4-11)
[
{
"ruleId": 4,
"name": "children age 4~11",
"variableName": "AGE",
"referenceValue": "[4,11]",
"relationType": "INTERVAL_NUMBER",
"logicType": "AND",
"priority": 10
},
{
"ruleId": 4,
"name": "amount total",
"variableName": "AMT",
"referenceValue": "1000000",
"relationType": "EQUAL",
"logicType": "AND",
"priority": 8
}
]
rule for age(12)
[
{
"ruleId": 5,
"name": "children age",
"variableName": "AGE",
"referenceValue": "12",
"relationType": "EQUAL",
"logicType": "AND",
"priority": 10
},
{
"ruleId": 5,
"name": "amount total",
"variableName": "AMT",
"referenceValue": "1000000",
"relationType": "EQUAL",
"logicType": "AND",
"priority": 9
}
]
ruleId
是当前条件所属规则的主键id,name
为条件名称,variableName
和referenceValue
分别为变量名和参考值,变量名的含义是在调用规则引擎接口时,用于接收外部参数的变量名称,参考值的作用是和变量名所代表的参数值进行比较。relationType
用于定义变量名和参考值之间的关系运算类型 。logicType
表示条件之间的逻辑运算类型。priority
表示条件优先级。
添加规则集、规则、条件之后,自动生成规则集表达式如下:
let rmap = seq.map();
if((AGE >= 0 && AGE <= 3) && AMT == 1000000){
seq.put(rmap, 'PREM', 67.9);
seq.put(rmap, 'RATE', 0.0679);
}
elsif((AGE >= 4 && AGE <= 11) && AMT == 1000000){
seq.put(rmap, 'PREM', 198.2);
seq.put(rmap, 'RATE', 0.1982);
}
elsif(AGE == 12 && AMT == 1000000){
seq.put(rmap, 'PREM', 18);
seq.put(rmap, 'RATE', 0.018);
}
return rmap;
上面生成的规则集表达式即为Aviator脚本表达式,传入对应的参数就可以获取执行结果。
另外,对于所有浮点数类型的入参和出参,在AviatorScript实际执行上述表达式时,会将所有浮点数都解析为decimal
,保证高精度运算的要求。(在默认配置的引擎实例中已开启此配置项Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL
)。
假设现在要计算年龄为9岁的儿童,额度为1000000时的费用和费率,规则引擎请求参数为:
{
"rulesetCode": "RULEST_RATE_CALC",
"paraMap": {
"AGE": 9,
"AMT": 1000000
}
}
规则引擎返回结果:
{
"status": 200,
"success": true,
"message": null,
"body": {
"RATE": 0.1982,
"PREM": 198.2
}
}
在应用服务中,调用接口方式如下:
@Autowired
private RuleCoreService ruleCoreService;
Map<String, Object> paraMap = new HashMap<>();
paraMap.put("AGE", 9);
paraMap.put("AMT", 1000000);
String rulesetCode = "RULEST_RATE_CALC";
Map<String, Object> resultMap = ruleCoreService.executeRuleset(rulesetCode, paraMap);
System.out.println("费用:" + resultMap.get("PREM"));// 198.2
System.out.println("费率:" + resultMap.get("RATE"));// 0.1982
或者通过外部请求接口进行调用。
默认配置的Aviator执行器实例(AviatorEvaluatorInstance
)设置的是编译缓存模式,避免在每次执行规则集时都对表达式重新编译,影响性能。
同时也使用了LRU缓存(Aviator 5.0+),用于指定缓存的规则集表达式数量,默认是500,可以在配置文件中对属性(aviator.expression-cache-capacity
)进行配置。
建议将本项目部署为独立的服务。数据库及相关服务配置请修改配置文件 。
在服务部署前,请在配置的数据库中创建数据表 。
目前规则维护可直接通过界面进行配置,同时也提供了内部管理接口。
默认的管理后台访问地址为:http://localhost:8000/static