关于规则引擎:规则引擎Drools在贷后催收业务中的应用

5次阅读

共计 14645 个字符,预计需要花费 37 分钟才能阅读完成。

作者:vivo 互联网服务器团队 - Feng Xiang

在日常业务开发工作中咱们常常会遇到一些依据业务规定做决策的场景。为了让开发人员从大量的规定代码的开发保护中释放出来,把规定的保护和生成交由业务人员,为了达到这种目标通常咱们会应用规定引擎来帮忙咱们实现。

本篇文章次要介绍了规定引擎的概念以及 Kie 和 Drools 的关系,重点解说了 Drools 中规定文件编写以及匹配算法 Rete 原理。文章的最初为大家展现了规定引擎在催收零碎中是如何应用的,次要解决的问题等。

一、业务背景

1.1 催收业务介绍

生产贷作为 vivo 钱包中的重要业务板块当呈现逾期的案件须要解决时,咱们会将案件统计收集后导入到催收零碎中,在催收零碎中定义了一系列的规定来帮忙业务方依据客户的逾期水平、危险合规评估、操作老本及收益回报最大准则制订催收策略。例如“分案规定”会依据规定将不同类型的案件调配到不同的队列,再通过队列调配给各个催收岗位和催收员,最终由催收员去进行催收。上面我会联合具体场景进行具体介绍。

1.2 规定引擎介绍

1.2.1 问题的引入

案例:根据上述分案规定咱们列举了如下的规定集:

代码实现:将以上规定集用代码实现

if(overdueDays>a && overdueDays<b && overdueAmt <W){taskQuene = "A 队列";}
else if(overdueDays>c && overdueDays<d && overdueAmt <W){taskQuene = "B 队列";}
else if(overdueDays>e && overdueDays<f && overdueAmt <W){taskQuene = "C 队列";}
else if(overdueDays>h && overdueDays<g && overdueAmt <W){taskQuene = "D 队列";}
……

业务变动:

  1. 条件字段和后果字段可能会增长而且变动频繁。
  2. 下面列举的规定集只是一类规定,实际上在咱们零碎中还有很多其余品种的规定集。
  3. 规定最好由业务人员保护,能够随时批改,不须要开发人员染指,更不心愿重启利用。

问题产生:

能够看出如果规定很多或者比较复杂的场景须要在代码中写很多这样 if else 的代码,而且不容易保护一旦新增条件或者规定有变更则须要改变很多代码。

此时咱们须要引入规定引擎来帮忙咱们将规定从代码中拆散进来,让开发人员从规定的代码逻辑中解放出来,把规定的保护和设置交由业务人员去治理。

1.2.2 什么是规定引擎

规定引擎由推理引擎倒退而来,是一种嵌入在应用程序中的组件,实现了将业务决策从利用程序代码中分离出来,并应用预约义的语义模块编写业务决策。

通过接收数据输出解释业务规定,最终依据业务规定做出业务决策。罕用的规定引擎有:Drools,easyRules 等等。本篇咱们次要来介绍 Drools。

二、Drools

2.1 整体介绍

2.1.1 KIE 介绍

在介绍 Drools 之前咱们不得不提到一个概念 KIE,KIE(Knowledge Is Everything)是一个综合性我的项目,将一些相干技术整合到一起,同时也是各个技术的外围,这外面就蕴含了明天要讲到的 Drools。

技术组成:

  1. Drools 是一个业务规定管理系统,具备基于前向链和后向链推理的规定引擎,容许疾速牢靠地评估业务规定和简单的事件处理。
  2. jBPM 是一个灵便的业务流程治理套件,容许通过形容实现这些指标所需执行的步骤来为您的业务指标建模。
  3. OptaPlanner 是一个束缚求解器,可优化员工排班、车辆路线、任务分配和云优化等用例。
  4. UberFire 是一个基于 Eclipse 的富客户端平台 web 框架。
2.1.2 Drools 介绍

Drools 的基本功能是将传入的数据或事实与规定的条件进行匹配,并确定是否以及如何执行规定。

Drools 的劣势:基于 Java 编写易于学习和把握,能够通过决策表动静生成规定脚本对业务人员非常敌对。

Drools 应用以下根本组件:

  • rule(规定):用户定义的业务规定,所有规定必须至多蕴含触发规定的条件和规定规定的操作。
  • Facts(事实):输出或更改到 Drools 引擎中的数据,Drools 引擎匹配规定条件以执行实用规定。
  • production memory(生产内存):用于寄存规定的内存。
  • working memory(工作内存):用于寄存事实的内存。
  • Pattern matcher(匹配器):将规定库中的所有规定与工作内存中的 fact 对象进行模式匹配,匹配胜利后放入议程中
  • Agenda(议程):寄存匹配器匹配胜利后激活的规定以筹备执行。

当用户在 Drools 中增加或更新规定相干信息时,该信息会以一个或多个事实的模式插入 Drools 引擎的工作内存中。Drools 引擎将这些事实与存储在生产内存中的规定条件进行模式匹配。

当满足规定条件时,Drools 引擎会激活并在议程中注册规定,而后 Drools 引擎会依照优先级进行排序并筹备执行。

2.2 规定(rule)

2.2.1 规定文件解析

DRL(Drools 规定语言)是在 drl 文本文件中定义的业务规定。次要蕴含:package,import,function,global,query,rule end 等,同时 Drools 也反对 Excel 文件格式。

package  // 包名,这个包名只是逻辑上的包名,不用与物理包门路统一。import   // 导入类 同 java
 
function  //  自定义函数
 
query  //  查问
 
global   //  全局变量
 
rule "rule name"  //  定义规定名称,名称惟一不能反复
    attribute //  规定属性
    when
        //  规定条件
    then
        //  触发行为
end
 
rule "rule2 name"
 
...
  • function

规定文件中的办法和咱们平时代码中定义的办法相似,晋升规定逻辑的复用。

应用案例:

function String hello(String applicantName) {return "Hello" + applicantName + "!";}
 
rule "Using a function"
  when
    // Empty
  then
    System.out.println(hello( "James") );
end
  • query

DRL 文件中的查问是在 Drools 引擎的工作内存中搜寻与 DRL 文件中的规定相干的事实。在 DRL 文件中增加查问定义,而后在利用程序代码中获取匹配后果。查问搜寻一组定义的条件,不须要 when 或 then 标准。

查问名称对于 KIE 库是全局的,因而在我的项目中的所有其余规定查问中必须是惟一的。返回查问后果 ksession.getQueryResults(“name”),其中 ”name” 是查问名称。

应用案例:

规定:

query "people under the age of 21"
    $person : Person(age < 21)
end
QueryResults results = ksession.getQueryResults("people under the age of 21");
System.out.println("we have" + results.size() + "people under the age  of 21" );
  • 全局变量 global

通过 KIE 会话配置在 Drools 引擎的工作内存中设置全局值,在 DRL 文件中的规定上方申明全局变量,而后在规定的操作 (then) 局部中应用它。

应用案例:

List<String> list = new ArrayList<>();
KieSession kieSession = kiebase.newKieSession();
kieSession.setGlobal("myGlobalList", list);
global java.util.List myGlobalList;
 
rule "Using a global"
  when
    // Empty
  then
    myGlobalList.add("My global list");
end
  • 规定属性
  • 模式匹配

当事实被插入到工作内存中后,规定引擎会把事实和规定库里的模式进行匹配,对于匹配胜利的规定再由 Agenda 执行推理算法中规定的 (then) 局部。

  • when

规定的“when”局部也称为规定的左侧 (LHS)蕴含执行操作必须满足的条件。如果该 when 局部为空,则默认为 true。如果规定条件有多个能够应用(and,or),默认连词是 and。如银行要求贷款申请人年满 21 岁,那么规定的 when 条件是 Applicant(age < 21)

rule "Underage"
  when
    application : LoanApplication()// 示意存在 Application 事实对象且 age 属性满足 <21
    Applicant(age < 21)
  then
    // Actions
end
  • then

规定的“then”局部也称为规定的右侧(RHS)蕴含在满足规定的条件局部时要执行的操作。如银行要求贷款申请人年满 21 岁(Applicant(age < 21))。不满足则回绝贷款 setApproved(false)

rule "Underage"
  when
    application : LoanApplication()
    Applicant(age < 21)
  then
    application.setApproved(false);
end
  • 内置办法

Drools 次要通过 insert、update 办法对工作内存中的 fact 数据进行操作,来达到管制规定引擎的目标。

操作实现之后规定引擎会从新匹配规定,原来没有匹配胜利的规定在咱们批改完数据之后有可能就匹配胜利了。

留神:这些办法会导致从新匹配,有可能会导致死循环问题,在编写中最好设置属性 no-loop 或者 lock-on-active 属性来躲避。

(1)insert:

作用:向工作内存中插入 fact 数据,并让相干规定从新匹配

rule "Underage"
  when
    Applicant(age < 21)
  then
    Applicant application = new application();
    application.setAge(22);
    insert(application);// 插入 fact 从新匹配规定,age>21 的规定间接被触发
end

(2)update:

作用:批改工作内存中 fact 数据,并让相干规定从新匹配

rule "Underage"
  when
    Applicant(age < 21)
  then
    Applicant application = new application();
    application.setAge(22);
    insert(application);// 插入 fact 从新匹配规定,age>21 的规定间接被触发
end

比拟操作符

2.3 工程引入

2.3.1 配置文件的引入

须要有一个配置文件通知代码规定文件 drl 在哪里,在 drools 中这个文件就是 kmodule.xml,搁置到 resources/META-INF 目录下。

阐明:kmodule 是 6.0 之后引入的一种新的配置和约定办法来构建 KIE 库,而不是应用之前的程序化构建器办法。

<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.drools.org/xsd/kmodule">
  <kbase name="KBase1" default="true" packages="org.domain.pkg1">
    <ksession name="KSession2_1" type="stateful" default="true"/>
    <ksession name="KSession2_2" type="stateless" default="false"/>
  </kbase>
  <kbase name="KBase2" default="false" packages="org.domain.pkg2, org.domain.pkg3">
    <ksession name="KSession3_1" type="stateful" default="false">
    </ksession>
  </kbase>
</kmodule>
  • Kmodule 中能够蕴含一个到多个 kbase, 别离对应 drl 的规定文件。
  • Kbase 是所有应用程序常识定义的存储库,蕴含了若干的规定、流程、办法等。须要一个惟一的 name, 能够取任意字符串。

KBase 的 default 属性示意以后 KBase 是不是默认的, 如果是默认的则不必名称就能够查找到该 KBase, 但每个 module 最多只能有一个默认 KBase。

KBase 上面能够有一个或多个 ksession,ksession 的 name 属性必须设置, 且必须惟一。

  • packages 为 drl 文件所在 resource 目录下的门路,多个包用逗号分隔,通常 drl 规定文件会放在工程中的 resource 目录下。
2.3.2 代码中的应用

KieServices:能够拜访所有 Kie 构建和运行时的接口,通过它来获取的各种对象(例如:KieContainer)来实现规定构建、治理和执行等操作。

KieContainer:KieContainer 是一个 KModule 的容器,提供了获取 KBase 的办法和创立 KSession 的办法。其中获取 KSession 的办法外部仍旧通过 KBase 来创立 KSession。

KieSession:KieSession 是一个到规定引擎的对话连贯,通过它就能够跟规定引擎通信,并且发动执行规定的操作。例如:通过 kSession.insert 办法来将事实(Fact)插入到引擎中,也就是 Working Memory 中,而后通过 kSession.fireAllRules 办法来告诉规定引擎执行规定。

KieServices kieServices = KieServices.Factory.get();
KieContainer kContainer = kieServices.getKieClasspathContainer();
KieBase kBase1 = kContainer.getKieBase("KBase1"); // 获取指定的 KBase
KieSession kieSession1 = kContainer.newKieSession("KSession2_1"); // 获取指定的 KSession
kieSession1.insert(facts);// 规定插入到工作内存
kSession.fireAllRules();// 开始执行
kSession.dispose();// 敞开对话

阐明:以上案例是应用的 Kie 的 API(6.x 之后的版本)

2.4 模式匹配算法 -RETE

Rete 算法由 Charles Forgy 博士创造,并在 1978-79 年的博士论文中记录。Rete 算法能够分为两局部:规定编译和运行时执行。

编译算法形容了如何解决生产内存中的规定以生成无效的决策网络。在非技术术语中,决策网络用于在数据通过网络流传时对其进行过滤。

网络顶部的节点会有很多匹配,随着网络向下延长匹配会越来越少,在网络的最底部是终端节点。

对于 RETE 算法官网给出的阐明比拟形象,这里咱们联合具体案例进行阐明。

2.4.1 案例阐明

假如有以下事实对象:

A(a1=1,a2=”A”)

A(a1=2,a2=”A2″)

B(b1=1,b2=”B”)

B(b1=1,b2=”B2″)

B(b1=2,b2=”B3″)

C(c1=1,c2=”B”)

现有规定:

rule "Rete"
  when
    A(a1==1,$a:a1)
    B(b1==1,b1==$a,$b:b2)
    C(c2==$b)
  then
    System.out.print("匹配胜利");
end

Bete 网络:

2.4.2 节点阐明

1.Root Node:根节点是所有对象进入网络的中央

2.one-input-node(单输出节点)

【ObjectTypeNode】:对象类型节点是根节点的后继节点,用来判断类型是否统一

【AlphaNode】:用于判断文本条件,例如(name == “cheddar”,strength == “strong”)

【LeftInputAdapterNode】:将对象作为输出并流传单个对象。

3.two-input-node(双输出节点)

【BetaNode】:用于比拟两个对象,两个对象可能是雷同或不同的类型。上述案例中用到的 join node 就是 betaNode 的一种类型。join node 用于连贯左右输出,左部输出的是事实对象列表,右部输出一个事实对象,在 Join 节点依照对象类型或对象字段进行比对。BetaNodes 也有内存。右边的输出称为 Beta Memory,它会记住所有传入的对象列表。左边的输出称为 Alpha Memory,它会记住所有传入的事实对象。

4.TerminalNode:

示意一条规定已匹配其所有条件,带有“或”条件的规定会为每个可能的逻辑分支生成子规定,因而一个规定能够有多个终端节点。

2.4.3 RETE 网络构建流程
  1. 创立虚构根节点
  2. 取出一个规定,例如 “Rete”
  3. 取出一个模式例如 a1==1(模式:就是指 when 语句的条件,这里 when 条件可能是有几个更小的条件组成的大条件。模式就是指的不能再持续宰割上来的最小的原子条件),查看参数类型(ObjectTypeNode),如果是新类型则退出一个类型节点;
  4. 查看模式的条件束缚:对于单类型束缚 a1==1,查看对应的 alphaNode 是否已存在,如果不存在将该束缚作为一个 alphaNode 退出链的后继节点;
  5. 若为多类型束缚 a1==b1,则创立相应的 betaNode,其左输出为 LeftInputAdapterNode,右输出为以后链的 alphaNode;
  6. 反复 4,直到该模式的所有束缚处理完毕;
  7. 反复 3 -5,直到所有的模式处理完毕,创立 TerminalNode,每个模式链的开端连到 TerminalNode;
  8. 将(Then)局部封装成输入节点。
2.4.4 运行时执行
  1. 从工作内存中取一工作存储区元素 WME(Working Memory Element,简称 WME)放入根节点进行匹配。WME 是为事实建设的元素,是用于和非根结点代表的模式进行匹配的元素。
  2. 遍历每个 alphaNode 和 ObjectTypeNode,如果约束条件与该 WME 统一,则将该 WME 存在该 alphaNode 的匹配内存中,并向其后继节点流传。
  3. 对每个 betaNode 进行匹配,将左内存中的对象列表与右内存中的对象依照节点束缚进行匹配,符合条件则将该事实对象与左部对象列表合并,并传递到下一节点。
  4. 和 3 都实现之后事实对象列表进入到 TerminalNode。对应的规定被触活,将规定注册进议程(Agenda)。
  5. 对 Agenda 里的规定依照优先级执行。
2.4.5 共享模式

以下是模式共享的案例,两个规定共享第一个模式 Cheese($cheddar : name == “cheddar”)

rule "Rete1"
when
    Cheese($cheddar : name == "cheddar")
    $person : Person(favouriteCheese == $cheddar)
then
    System.out.println($person.getName() + "likes cheddar" );
end
 
rule "Rete2"
when
    Cheese($cheddar : name == "cheddar")
    $person : Person(favouriteCheese != $cheddar)
then
    System.out.println($person.getName() + "does not like cheddar" );
end

网络图:(右边的类型为 Cheese,左边类型为 Person)

2.4.6 小结

rete 算法实质上是通过共享规定节点和缓存匹配后果,取得性能晋升。

【状态保留】:

事实汇合中的每次变动,其匹配后的状态都被保留到 alphaMemory 和 betaMemory 中。在下一次事实汇合发生变化时(绝大多数的后果都不须要变动)通过从内存中取值,防止了大量的反复计算。

Rete 算法次要是为那些事实汇合变动不大的零碎设计的,当每次事实汇合的变动十分激烈时,rete 的状态保留算法成果并不现实。

【节点共享】:

例如下面的案例不同规定之间含有雷同的模式,能够共享同一个节点。

【hash 索引】:

每次将 AlphaNode 增加到 ObjectTypeNode 后继节点时,它都会将文字值作为键增加到 HashMap,并将 AlphaNode 作为值。当一个新实例进入 ObjectType 节点时,它不会流传到每个 AlphaNode,而是能够从 HashMap 中检索正确的 AlphaNode,从而防止不必要的文字查看。

存在问题:

  1. 存在状态反复保留的问题,匹配过多个模式的事实要同时保留在这些模式的节点缓存中,将占用较多空间并影响匹配效率。
  2. 不适宜频繁变动的数据与规定(数据变动引起节点保留的长期事实频繁变动,这将让 rete 失去增量匹配的劣势;数据的变动使得对规定网络的种种优化办法如索引、条件排序等失去成果)。
  3. rete 算法应用了 alphaMemory 和 betaMemory 存储已计算的两头后果, 以就义空间换取工夫, 从而放慢零碎的速度。然而当解决海量数据与规定时,beta 内存依据规定的条件与事实的数目而成指数级增长, 所以当规定与事实很多时, 会耗尽系统资源。

在 Drools 晚期版本中应用的匹配算法是 Rete,从 6.x 开始引入了 phreak 算法来解决 Rete 带来的问题。

对于 phreak 算法能够看官网介绍:

https://docs.drools.org/6.5.0.Final/drools-docs/html/ch05.html#PHREAK

三、催收业务中的利用

3.1 问题解决

文章结尾问题引出的例子中能够通过编写 drl 规定脚本实现,每次规定的变更只须要批改 drl 文件即可。

package vivoPhoneTaskRule;
 
import com.worldline.wcs.service.rule.CaseSumNewWrapper;
 
 
rule "rule1"
    salience 1
    when
        caseSumNew:CaseSumNewWrapper(overdueDD > a && overdueDD < b && overdueAmt <= W)
    then
        caseSumNew.setTaskType("A 队列");
end
 
rule "rule2"
    salience 2
    when
        caseSumNew:CaseSumNewWrapper(overdueDD > c && overdueDD < d && overdueAmt <= W)
    then
        caseSumNew.setTaskType("B 队列");
end
 
rule "rule3"
    salience 3
    when
        caseSumNew:CaseSumNewWrapper(overdueDD > e && overdueDD < f && overdueAmt <= W)
    then
        caseSumNew.setTaskType("C 队列");
end
 
rule "rule4"
    salience 4
    when
        caseSumNew:CaseSumNewWrapper(overdueDD > h && overdueDD < g && overdueAmt > W)
    then
        caseSumNew.setTaskType("D 队列");
end

产生一个新的问题:

尽管通过编写 drl 能够解决规定保护的问题,然而让业务人员去编写这样一套规定脚本显然是有难度的,那么在催收零碎中是怎么做的呢,咱们持续往下看。

3.2 规定的设计

3.2.1 决策表设计

催收零碎自研了一套决策表的解决方案,将 drl 中的条件和后果语句形象成结构化数据进行存储并在前端做了可视化页面提供给业务人员进行编辑不须要编写规定脚本。例如新增规定:

将逾期天数大于 a 天小于 b 天且逾期总金额小于等于 c 的案件调配到 A 队列中。

表中的每一行都对应一个 rule,业务人员能够依据规定状况进行批改和增加,同时也能够依据条件定义对决策表进行拓展。

决策表的次要形成:

  • 规定条件定义:定义了一些规定中用到的条件,例如:逾期天数,逾期金额等。
  • 规定后果定义:定义了一些规定中的后果,例如:调配到哪些队列中,在队列中停留时间等。
  • 条件字段:在编辑一条规定时,须要用到的条件字段(从条件定义列表中选取)。
  • 比拟操作符与值:比拟操作符包含:<、<=、>、>=、==、!=, 临时不反对 contain,member Of,match 等。

条件值目前蕴含数字和字符。条件字段 + 比拟操作符 + 值,就形成了一个条件语句。

  • 后果:满足条件后最终失去的后果也就是后果定义中的字段值。
3.2.2 规定生成

催收零碎提供了 可视化页面配置来动静生成脚本 的性能(业务人员依据条件定义和后果定义来编辑决策表进而制订相应规定)。

外围流程:

1. 依据规定类型解析相应的事实对象映射文件,并封装成条件实体 entitys 与后果实体 resultDefs,文件内容如下图:

事实对象映射 xml

<rule package="phoneTask">
    <entitys>
        <entity note="collectionCaseInfo" cls="com.worldline.wcs.service.rule.FactWrapper" alias="caseSumNew">
            <attribute attr="caseSumNew.overdueDD" />
            <attribute attr="caseSumNew.totalOverdueAmt"/>
        </entity>
    </entitys>
    <resultDefs>
        <resultDef key="1" seq="1" enumKey="ruleTaskType">
            <script><![CDATA[caseSumNew.setTaskType("@param");]]></script>
        </resultDef>
    </resultDefs>
</rule>

2. 依据规定类型查问规定集残缺数据

3. 将规定集数据与 xml 解析后的对象进行整合,拼装成一个 drl 脚本

4. 将拼装好的脚本保留到数据库规定集表中

/**
* 生成规定脚本
* rule 规定根本信息:包含规定表字段名定义等
* def 业务人员具体录入规定集的条件和后果等数据
*/
public String generateDRLScript(DroolsRuleEditBO rule, DroolsRuleTableBO def) {
        // 解析事实对象映射 XML 文件,生成条件定义与后果定义
        RuleSetDef ruleSetDef = RuleSetDefHelper.getRuleSetDef(rule.getRuleTypeCode());
 
        // 1. 申明规定包
        StringBuilder drl = new StringBuilder("package").append(rule.getRuleTypeCode()).append(";\n\n");
        HashMap<String, String> myEntityMap = Maps.newHashMap(); // k,v => caseSumNew,CaseSumNewWrapper
        // 2. 导入 entity 对应执行类
        ruleSetDef.getEntitys().forEach(d -> {String cls = d.getCls();
            drl.append("import").append(cls).append(";\n\n");
            myEntityMap.put(d.getAlias(), cls.substring(cls.lastIndexOf('.') + 1));
        });
        // 3. 规定脚本正文
        drl.append("//").append(rule.getRuleTypeCode()).append(":").append(rule.getRuleTypeName()).append("\n");
        drl.append("// version :").append(rule.getCode()).append("\n");
        drl.append("// createTime :").append(DateUtil.getSysDate(DateUtil.PATTERN_TIME_DEFAULT)).append("\n\n");
 
        Map<String, String> myResultMap = def.getResultDefs().stream().collect(Collectors.toMap(DroolsRuleCondBO::getCondKey, DroolsRuleCondBO::getScript));
 
        // 4. 写规定
        AtomicInteger maxRowSize = new AtomicInteger(0); // 总规定数
        rule.getTables().forEach(table -> {String tableCode = table.getTableCode();
            table.getRows().stream().filter(r -> !Objects.equals(r.getStatus(), 3))
           .forEach(row -> {
                // 3.1. 规定属性及优先级
                drl.append("// generated from row:").append(row.getRowCode()).append("\n");
                //TODO 须要保障 row.getRowSort()不反复,否则生成同样的规定编号
                drl.append("rule \"").append(rule.getRuleTypeCode()).append("_").append(tableCode).append("_TR_").append(row.getRowSort()).append("\"\n");  // pkg_tableCode_TR_rowSort
                drl.append("\tsalience").append((maxRowSize.incrementAndGet())).append("\n");
 
                // 4.2. 条件断定
                drl.append("\twhen\n");
                // 每个 entity 一行, 多条件合并
                // when=condEntityKey:cls(condKeyMethod colOperator.drlStr colValue), 其中 cls=myEntityMap.value(key=condEntityKey)
                drl.append(row.getColumns()
                                .stream().collect(Collectors.groupingBy(d -> d.getCondition().getCondEntityKey()))
                                .entrySet().stream()
                                .map(entityType -> "\t\t" + entityType.getKey() + ":" + myEntityMap.get(entityType.getKey()) + "(" +
                                        entityType.getValue().stream()
                                                .filter(col -> StringUtils.isNotBlank(col.getColValue())) // 排除有效条件
                                                .sorted(Comparator.comparing(col -> col.getCondition().getCondSort())) // 排序
                                                .map(col -> {String condKey = col.getCondition().getCondKey();
                                                    String condKeyMethod = condKey.substring(condKey.indexOf('.') + 1);
                                                    String[] exec = ParamTypeHelper.get(col.getColOperator()).getDrlStr(condKeyMethod, col.getColValue());
                                                    if (exec.length > 0) {return Arrays.stream(exec).filter(StringUtils::isNotBlank).collect(Collectors.joining("&&"));
                                                    }
                                                    return null;
                                                })
                                                .collect(Collectors.joining("&&")) + ")\n"
                                )
                                .collect(Collectors.joining()));
 
                // 4.3. 规定后果
                drl.append("\tthen\n");
                row.getResults().forEach(r -> {String script = myResultMap.get(r.getResultKey());
                    drl.append("\t\t").append(script.replace("@param", r.getResultValue())).append("\n"); // 应用 resultValue 替换 @param
                });
                drl.append("end\n\n");
            });
        });
        return drl.toString();}
3.2.3 规定执行

外围流程:

// 外围流程代码:KnowledgeBuilder kb = KnowledgeBuilderFactory.newKnowledgeBuilder();
kb.add(ResourceFactory.newByteArrayResource(script.getBytes(StandardCharsets.UTF_8)), ResourceType.DRL); //script 为规定脚本
InternalKnowledgeBase base = KnowledgeBaseFactory.newKnowledgeBase();
KieSession ksession = base.newKieSession();
AgendaFilter filter = RuleConstant.DroolsRuleNameFilter.getFilter(ruleTypeCode);// 获取一个过滤器
kSession.insert(fact);
kSession.fireAllRules(filter);
kSession.dispose();
  1. 依据规定类型从规定集表中查问 drl 脚本
  2. 将脚步增加至 KnowledgeBuilder 中构建知识库
  3. 获取知识库 InternalKnowledgeBase(在新版本中对应 Kmodule 中的 Kbase)
  4. 通过 InternalKnowledgeBase 创立 KieSession 会话链接
  5. 创立 AgendaFilter 来制订执行某一个或某一些规定
  6. 调用 insert 办法将事实对象 fact 插入工作内存
  7. 调用 fireAllRules 办法执行规定
  8. 最初调用 dispose 敞开连贯

四、总结

本文次要由催收零碎中的一个案例引出规定引擎 Drools,而后具体介绍了 Drools 的概念与用法以及模式匹配的原理 Rete 算法。最初联合催收零碎给大家解说了 Drools 在催收零碎中是如何应用的。

通过规定引擎的引入让开发人员不再须要参加到规定的开发与保护中来,极大节约了开发成本。通过自研的催收零碎可视化决策表,让业务人员能够在零碎中灵便配置保护规定而不须要每次编写简单的规定脚本,解决了业务人员的痛点。零碎实质上还是执行的规定脚本,咱们这里是把脚本的生成做了优化解决,先通过可视化页面录入规定以结构化的数据进行存储,再将其与规定定义进行整合拼装,最终由零碎主动生成规定脚本。

以后催收零碎中的规定引擎依然存在着一些问题,例如:

  1. 催收零碎通过动静生成脚本的形式适宜比较简单的规定逻辑,如果想实现较为简单的规定,须要写很多简单的代码,保护老本比拟高。
  2. 催收零碎尽管应用的 drools7.x 版本,然而应用的形式仍然应用的是 5.x 的程序化构建器办法(Knowledge API)
  3. 催收零碎目前规定固定页面上只能编辑无奈新增规定,只能通过初始化数据库表的形式新增规定。

后续咱们会随着版本的迭代一直降级优化,感激浏览。

参考文档:

  1. 官网文档:Drools Documentation
  2. api 文档:KIE :: Public API 6.5.0.Final API
正文完
 0