新聞中心
在日常業(yè)務(wù)開發(fā)工作中我們經(jīng)常會遇到一些根據(jù)業(yè)務(wù)規(guī)則做決策的場景。為了讓開發(fā)人員從大量的規(guī)則代碼的開發(fā)維護中釋放出來,把規(guī)則的維護和生成交由業(yè)務(wù)人員,為了達(dá)到這種目的通常我們會使用規(guī)則引擎來幫助我們實現(xiàn)。

本篇文章主要介紹了規(guī)則引擎的概念以及Kie和Drools的關(guān)系,重點講解了Drools中規(guī)則文件編寫以及匹配算法Rete原理。文章的最后為大家展示了規(guī)則引擎在催收系統(tǒng)中是如何使用的,主要解決的問題等。
一、業(yè)務(wù)背景
1.1 催收業(yè)務(wù)介紹
消費貸作為vivo錢包中的重要業(yè)務(wù)板塊當(dāng)出現(xiàn)逾期的案件需要處理時,我們會將案件統(tǒng)計收集后導(dǎo)入到催收系統(tǒng)中,在催收系統(tǒng)中定義了一系列的規(guī)則來幫助業(yè)務(wù)方根據(jù)客戶的逾期程度、風(fēng)險合規(guī)評估、操作成本及收益回報最大原則制定催收策略。例如“分案規(guī)則” 會根據(jù)規(guī)則將不同類型的案件分配到不同的隊列,再通過隊列分配給各個催收崗位和催收員,最終由催收員去進行催收。下面我會結(jié)合具體場景進行詳細(xì)介紹。
1.2 規(guī)則引擎介紹
1.2.1 問題的引入
案例:根據(jù)上述分案規(guī)則我們列舉了如下的規(guī)則集:
代碼實現(xiàn):將以上規(guī)則集用代碼實現(xiàn)
if(overdueDays>a && overdueDays taskQuene = "A隊列";
}
else if(overdueDays>c && overdueDaystaskQuene = "B隊列";
}
else if(overdueDays>e && overdueDaystaskQuene = "C隊列";
}
else if(overdueDays>h && overdueDaystaskQuene = "D隊列";
}
……
業(yè)務(wù)變化:
- 條件字段和結(jié)果字段可能會增長而且變動頻繁。
- 上面列舉的規(guī)則集只是一類規(guī)則,實際上在我們系統(tǒng)中還有很多其他種類的規(guī)則集。
- 規(guī)則最好由業(yè)務(wù)人員維護,可以隨時修改,不需要開發(fā)人員介入,更不希望重啟應(yīng)用。
問題產(chǎn)生:
可以看出如果規(guī)則很多或者比較復(fù)雜的場景需要在代碼中寫很多這樣if else的代碼,而且不容易維護一旦新增條件或者規(guī)則有變更則需要改動很多代碼。
此時我們需要引入規(guī)則引擎來幫助我們將規(guī)則從代碼中分離出去,讓開發(fā)人員從規(guī)則的代碼邏輯中解放出來,把規(guī)則的維護和設(shè)置交由業(yè)務(wù)人員去管理。
1.2.2 什么是規(guī)則引擎
規(guī)則引擎由推理引擎發(fā)展而來,是一種嵌入在應(yīng)用程序中的組件, 實現(xiàn)了將業(yè)務(wù)決策從應(yīng)用程序代碼中分離出來,并使用預(yù)定義的語義模塊編寫業(yè)務(wù)決策。
通過接收數(shù)據(jù)輸入解釋業(yè)務(wù)規(guī)則,最終根據(jù)業(yè)務(wù)規(guī)則做出業(yè)務(wù)決策。常用的規(guī)則引擎有:Drools,easyRules等等。本篇我們主要來介紹Drools。
二、Drools
2.1 整體介紹
2.1.1 KIE介紹
在介紹Drools之前我們不得不提到一個概念KIE,KIE(Knowledge Is Everything)是一個綜合性項目,將一些相關(guān)技術(shù)整合到一起,同時也是各個技術(shù)的核心,這里面就包含了今天要講到的Drools。
技術(shù)組成:
- Drools是一個業(yè)務(wù)規(guī)則管理系統(tǒng),具有基于前向鏈和后向鏈推理的規(guī)則引擎,允許快速可靠地評估業(yè)務(wù)規(guī)則和復(fù)雜的事件處理。
- jBPM是一個靈活的業(yè)務(wù)流程管理套件,允許通過描述實現(xiàn)這些目標(biāo)所需執(zhí)行的步驟來為您的業(yè)務(wù)目標(biāo)建模。
- OptaPlanner是一個約束求解器,可優(yōu)化員工排班、車輛路線、任務(wù)分配和云優(yōu)化等用例。
- UberFire是一個基于 Eclipse 的富客戶端平臺web框架。
2.1.2 Drools介紹
Drools 的基本功能是將傳入的數(shù)據(jù)或事實與規(guī)則的條件進行匹配,并確定是否以及如何執(zhí)行規(guī)則。
Drools的優(yōu)勢:基于Java編寫易于學(xué)習(xí)和掌握,可以通過決策表動態(tài)生成規(guī)則腳本對業(yè)務(wù)人員十分友好。
Drools 使用以下基本組件:
- rule(規(guī)則):用戶定義的業(yè)務(wù)規(guī)則,所有規(guī)則必須至少包含觸發(fā)規(guī)則的條件和規(guī)則規(guī)定的操作。
- Facts(事實):輸入或更改到 Drools 引擎中的數(shù)據(jù),Drools 引擎匹配規(guī)則條件以執(zhí)行適用規(guī)則。
- production memory(生產(chǎn)內(nèi)存):用于存放規(guī)則的內(nèi)存。
- working memory(工作內(nèi)存):用于存放事實的內(nèi)存。
- Pattern matcher(匹配器):將規(guī)則庫中的所有規(guī)則與工作內(nèi)存中的fact對象進行模式匹配,匹配成功后放入議程中
- Agenda(議程):存放匹配器匹配成功后激活的規(guī)則以準(zhǔn)備執(zhí)行。
當(dāng)用戶在 Drools 中添加或更新規(guī)則相關(guān)信息時,該信息會以一個或多個事實的形式插入 Drools 引擎的工作內(nèi)存中。Drools 引擎將這些事實與存儲在生產(chǎn)內(nèi)存中的規(guī)則條件進行模式匹配。
當(dāng)滿足規(guī)則條件時,Drools 引擎會激活并在議程中注冊規(guī)則,然后Drools 引擎會按照優(yōu)先級進行排序并準(zhǔn)備執(zhí)行。
2.2 規(guī)則(rule)
2.2.1 規(guī)則文件解析
DRL(Drools 規(guī)則語言)是在drl文本文件中定義的業(yè)務(wù)規(guī)則。主要包含:package,import,function,global,query,rule end等,同時Drools也支持Excel文件格式。
package //包名,這個包名只是邏輯上的包名,不必與物理包路徑一致。
import //導(dǎo)入類 同java
function // 自定義函數(shù)
query // 查詢
global // 全局變量
rule "rule name" // 定義規(guī)則名稱,名稱唯一不能重復(fù)
attribute // 規(guī)則屬性
when
// 規(guī)則條件
then
// 觸發(fā)行為
end
rule "rule2 name"
...
function
規(guī)則文件中的方法和我們平時代碼中定義的方法類似,提升規(guī)則邏輯的復(fù)用。
使用案例:
function String hello(String applicantName) {
return "Hello " + applicantName + "!";
}
rule "Using a function"
when
// Empty
then
System.out.println( hello( "James" ) );
endquery
DRL 文件中的查詢是在 Drools 引擎的工作內(nèi)存中搜索與 DRL 文件中的規(guī)則相關(guān)的事實。在 DRL 文件中添加查詢定義,然后在應(yīng)用程序代碼中獲取匹配結(jié)果。查詢搜索一組定義的條件,不需要when或then規(guī)范。
查詢名稱對于 KIE 庫是全局的,因此在項目中的所有其他規(guī)則查詢中必須是唯一的。返回查詢結(jié)果ksession.getQueryResults("name"),其中"name"是查詢名稱。
使用案例:
規(guī)則:
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 引擎的工作內(nèi)存中設(shè)置全局值,在 DRL 文件中的規(guī)則上方聲明全局變量,然后在規(guī)則的操作 ( then) 部分中使用它。
使用案例:
Listlist = 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
規(guī)則屬性
模式匹配
當(dāng)事實被插入到工作內(nèi)存中后,規(guī)則引擎會把事實和規(guī)則庫里的模式進行匹配,對于匹配成功的規(guī)則再由 Agenda 執(zhí)行推理算法中規(guī)則的(then)部分。
- when
規(guī)則的“when”部分也稱為規(guī)則的左側(cè) (LHS)包含執(zhí)行操作必須滿足的條件。如果該when部分為空,則默認(rèn)為true。如果規(guī)則條件有多個可以使用(and,or),默認(rèn)連詞是and。如銀行要求貸款申請人年滿21歲,那么規(guī)則的when條件是Applicant(age < 21)
rule "Underage"
when
application : LoanApplication()//表示存在Application事實對象且age屬性滿足<21
Applicant( age < 21 )
then
// Actions
end
- then
規(guī)則的“then”部分也稱為規(guī)則的右側(cè)(RHS)包含在滿足規(guī)則的條件部分時要執(zhí)行的操作。如銀行要求貸款申請人年滿 21 歲(Applicant( age < 21 ))。不滿足則拒絕貸款setApproved(false)
rule "Underage"
when
application : LoanApplication()
Applicant( age < 21 )
then
application.setApproved( false );
end
內(nèi)置方法
Drools主要通過insert、update方法對工作內(nèi)存中的fact數(shù)據(jù)進行操作,來達(dá)到控制規(guī)則引擎的目的。
操作完成之后規(guī)則引擎會重新匹配規(guī)則,原來沒有匹配成功的規(guī)則在我們修改完數(shù)據(jù)之后有可能就匹配成功了。
注意:這些方法會導(dǎo)致重新匹配,有可能會導(dǎo)致死循環(huán)問題,在編寫中最好設(shè)置屬性no-loop或者lock-on-active屬性來規(guī)避。
(1)insert:
作用:向工作內(nèi)存中插入fact數(shù)據(jù),并讓相關(guān)規(guī)則重新匹配
rule "Underage"
when
Applicant( age < 21 )
then
Applicant application = new application();
application.setAge(22);
insert(application);//插入fact重新匹配規(guī)則,age>21的規(guī)則直接被觸發(fā)
end
(2)update:
作用:修改工作內(nèi)存中fact數(shù)據(jù),并讓相關(guān)規(guī)則重新匹配
rule "Underage1"
when
$application:Applicant( age < 21 )
then
$application.setAge(22);
update($application);//插入fact重新匹配規(guī)則,age>21的規(guī)則直接被觸發(fā)
end
rule "Underage2"
when
Applicant( age > 21 )
then
System.out.print("大于21歲的規(guī)則被觸發(fā)")
end
比較操作符
2.3 工程引入
2.3.1 配置文件的引入
需要有一個配置文件告訴代碼規(guī)則文件drl在哪里,在drools中這個文件就是kmodule.xml,放置到resources/META-INF目錄下。
說明:kmodule是6.0 之后引入的一種新的配置和約定方法來構(gòu)建 KIE 庫,而不是使用之前的程序化構(gòu)建器方法。
xmlns="http://www.drools.org/xsd/kmodule">
- Kmodule 中可以包含一個到多個 kbase,分別對應(yīng) drl 的規(guī)則文件。
- Kbase是所有應(yīng)用程序知識定義的存儲庫,包含了若干的規(guī)則、流程、方法等。需要一個唯一的name,可以取任意字符串。
KBase的default屬性表示當(dāng)前KBase是不是默認(rèn)的,如果是默認(rèn)的則不用名稱就可以查找到該 KBase,但每個 module 最多只能有一個默認(rèn) KBase。
KBase下面可以有一個或多個 ksession,ksession 的 name 屬性必須設(shè)置,且必須唯一。 - packages為drl文件所在resource目錄下的路徑,多個包用逗號分隔,通常drl規(guī)則文件會放在工程中的resource目錄下。
2.3.2 代碼中的使用
KieServices:可以訪問所有 Kie 構(gòu)建和運行時的接口,通過它來獲取的各種對象(例如:KieContainer)來完成規(guī)則構(gòu)建、管理和執(zhí)行等操作。
KieContainer:KieContainer是一個KModule的容器,提供了獲取KBase的方法和創(chuàng)建KSession的方法。其中獲取KSession的方法內(nèi)部依舊通過KBase來創(chuàng)建KSession。
KieSession:KieSession是一個到規(guī)則引擎的對話連接,通過它就可以跟規(guī)則引擎通訊,并且發(fā)起執(zhí)行規(guī)則的操作。例如:通過kSession.insert方法來將事實(Fact)插入到引擎中,也就是Working Memory中,然后通過kSession.fireAllRules方法來通知規(guī)則引擎執(zhí)行規(guī)則。
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);//規(guī)則插入到工作內(nèi)存
kSession.fireAllRules();//開始執(zhí)行
kSession.dispose();//關(guān)閉對話
說明:以上案例是使用的Kie的API(6.x之后的版本)
2.4 模式匹配算法-RETE
Rete算法由Charles Forgy博士發(fā)明,并在1978-79年的博士論文中記錄。Rete算法可以分為兩部分:規(guī)則編譯和運行時執(zhí)行。
編譯算法描述了如何處理生產(chǎn)內(nèi)存中的規(guī)則以生成有效的決策網(wǎng)絡(luò)。在非技術(shù)術(shù)語中,決策網(wǎng)絡(luò)用于在數(shù)據(jù)通過網(wǎng)絡(luò)傳播時對其進行過濾。
網(wǎng)絡(luò)頂部的節(jié)點會有很多匹配,隨著網(wǎng)絡(luò)向下延伸匹配會越來越少,在網(wǎng)絡(luò)的最底部是終端節(jié)點。
關(guān)于RETE算法官方給出的說明比較抽象,這里我們結(jié)合具體案例進行說明。
2.4.1 案例說明
假設(shè)有以下事實對象:
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")
現(xiàn)有規(guī)則:
rule "Rete"
when
A(a1==1,$a:a1)
B(b1==1,b1==$a,$b:b2)
C(c2==$b)
then
System.out.print("匹配成功");
end
Bete網(wǎng)絡(luò):
2.4.2 節(jié)點說明
1.Root Node:根節(jié)點是所有對象進入網(wǎng)絡(luò)的地方
2.one-input-node(單輸入節(jié)點)
- 【ObjectTypeNode】:對象類型節(jié)點是根節(jié)點的后繼節(jié)點,用來判斷類型是否一致
- 【AlphaNode】:用于判斷文本條件,例如(name == "cheddar",strength == "strong")
- 【LeftInputAdapterNode】:將對象作為輸入并傳播單個對象。
3.two-input-node(雙輸入節(jié)點)
- 【BetaNode】:用于比較兩個對象,兩個對象可能是相同或不同的類型。上述案例中用到的join node就是betaNode的一種類型。join node 用于連接左右輸入,左部輸入的是事實對象列表,右部輸入一個事實對象,在Join節(jié)點按照對象類型或?qū)ο笞侄芜M行比對。BetaNodes 也有內(nèi)存。左邊的輸入稱為 Beta Memory,它會記住所有傳入的對象列表。右邊的輸入稱為 Alpha Memory,它會記住所有傳入的事實對象。
4.TerminalNode:
表示一條規(guī)則已匹配其所有條件,帶有“或”條件的規(guī)則會為每個可能的邏輯分支生成子規(guī)則,因此一個規(guī)則可以有多個終端節(jié)點。
2.4.3 RETE網(wǎng)絡(luò)構(gòu)建流程
- 創(chuàng)建虛擬根節(jié)點
- 取出一個規(guī)則,例如 "Rete"
- 取出一個模式例如a1==1(模式:就是指when語句的條件,這里when條件可能是有幾個更小的條件組成的大條件。模式就是指的不能再繼續(xù)分割下去的最小的原子條件),檢查參數(shù)類型(ObjectTypeNode),
如果是新類型則加入一個類型節(jié)點; - 檢查模式的條件約束:對于單類型約束a1==1,檢查對應(yīng)的alphaNode是否已存在,如果不存在將該約束作為一個alphaNode加入鏈的后繼節(jié)點;
若為多類型約束a1==b1,則創(chuàng)建相應(yīng)的betaNode,其左輸入為LeftInputAdapterNode,右輸入為當(dāng)前鏈的alphaNode; - 重復(fù)4,直到該模式的所有約束處理完畢;
- 重復(fù)3-5,直到所有的模式處理完畢,創(chuàng)建TerminalNode,每個模式鏈的末尾連到TerminalNode;
- 將(Then)部分封裝成輸出節(jié)點。
2.4.4 運行時執(zhí)行
- 從工作內(nèi)存中取一工作存儲區(qū)元素WME(Working Memory Element,簡稱WME)放入根節(jié)點進行匹配。WME是為事實建立的元素,是用于和非根結(jié)點代表的模式進行匹配的元素。
- 遍歷每個alphaNode和ObjectTypeNode,如果約束條件與該WME一致,則將該WME存在該alphaNode的匹配內(nèi)存中,并向其后繼節(jié)點傳播。
- 對每個betaNode進行匹配,將左內(nèi)存中的對象列表與右內(nèi)存中的對象按照節(jié)點約束進行匹配,符合條件則將該事實對象與左部對象列表合并,并傳遞到下一節(jié)點。
- 和3都完成之后事實對象列表進入到TerminalNode。對應(yīng)的規(guī)則被觸活,將規(guī)則注冊進議程(Agenda)。
- 對Agenda里的規(guī)則按照優(yōu)先級執(zhí)行。
2.4.5 共享模式
以下是模式共享的案例,兩個規(guī)則共享第一個模式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
網(wǎng)絡(luò)圖:(左邊的類型為Cheese,右邊類型為Person)
2.4.6 小結(jié)
rete算法本質(zhì)上是通過共享規(guī)則節(jié)點和緩存匹配結(jié)果,獲得性能提升。
【狀態(tài)保存】:事實集合中的每次變化,其匹配后的狀態(tài)都被保存到alphaMemory和betaMemory中。在下一次事實集合發(fā)生變化時(絕大多數(shù)的結(jié)果都不需要變化)通過從內(nèi)存中取值,避免了大量的重復(fù)計算。
Rete算法主要是為那些事實集合變化不大的系統(tǒng)設(shè)計的,當(dāng)每次事實集合的變化非常劇烈時,rete的狀態(tài)保存算法效果并不理想。
【節(jié)點共享】:例如上面的案例不同規(guī)則之間含有相同的模式,可以共享同一個節(jié)點。
【hash索引】:每次將 AlphaNode 添加到 ObjectTypeNode 后繼節(jié)點時,它都會將文字值作為鍵添加到 HashMap,并將 AlphaNode 作為值。當(dāng)一個新實例進入 ObjectType 節(jié)點時,它不會傳播到每個 AlphaNode,而是可以從HashMap 中檢索正確的 AlphaNode,從而避免不必要的文字檢查。
存在問題:
- 存在狀態(tài)重復(fù)保存的問題,匹配過多個模式的事實要同時保存在這些模式的節(jié)點緩存中,將占用較多空間并影響匹配效率。
- 不適合頻繁變化的數(shù)據(jù)與規(guī)則(數(shù)據(jù)變化引起節(jié)點保存的臨時事實頻繁變化,這將讓rete失去增量匹配的優(yōu)勢;數(shù)據(jù)的變化使得對規(guī)則網(wǎng)絡(luò)的種種優(yōu)化方法如索引、條件排序等失去效果)。
- rete算法使用了alphaMemory和betaMemory存儲已計算的中間結(jié)果, 以犧牲空間換取時間, 從而加快系統(tǒng)的速度。然而當(dāng)處理海量數(shù)據(jù)與規(guī)則時,beta內(nèi)存根據(jù)規(guī)則的條件與事實的數(shù)目而成指數(shù)級增長, 所以當(dāng)規(guī)則與事實很多時,會耗盡系統(tǒng)資源。
在Drools早期版本中使用的匹配算法是Rete,從6.x開始引入了phreak算法來解決Rete帶來的問題。
關(guān)于phreak算法可以看官方介紹:
??https://docs.drools.org/6.5.0.Final/drools-docs/html/ch05.html#PHREAK??
三、催收業(yè)務(wù)中的應(yīng)用
3.1 問題解決
文章開頭問題引出的例子中可以通過編寫drl規(guī)則腳本實現(xiàn),每次規(guī)則的變更只需要修改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
產(chǎn)生一個新的問題:
雖然通過編寫drl可以解決規(guī)則維護的問題,但是讓業(yè)務(wù)人員去編寫這樣一套規(guī)則腳本顯然是有難度的,那么在催收系統(tǒng)中是怎么做的呢,我們繼續(xù)往下看。
3.2 規(guī)則的設(shè)計
3.2.1 決策表設(shè)計
催收系統(tǒng)自研了一套決策表的解決方案,將drl中的條件和結(jié)果語句抽象成結(jié)構(gòu)化數(shù)據(jù)進行存儲并在前端做了可視化頁面提供給業(yè)務(wù)人員進行編輯不需要編寫規(guī)則腳本。例如新增規(guī)則:
將逾期天數(shù)大于a天小于b天且逾期總金額小于等于c的案件分配到A隊列中。
表中的每一行都對應(yīng)一個rule,業(yè)務(wù)人員可以根據(jù)規(guī)則情況進行修改和添加,同時也可以根據(jù)條件定義對決策表進行拓展。
決策表的主要構(gòu)成:
- 規(guī)則條件定義
定義了一些規(guī)則中用到的條件,例如:逾期天數(shù),逾期金額等。 - 規(guī)則結(jié)果定義
定義了一些規(guī)則中的結(jié)果,例如:分配到哪些隊列中,在隊列中停留時間等。 - 條件字段
在編輯一條規(guī)則時,需要用到的條件字段(從條件定義列表中選?。?。 - 比較操作符與值
比較操作符包括:< 、<=、>、>=、==、!=,暫時不支持contain,member Of,match等
條件值目前包含數(shù)字和字符。條件字段+比較操作符+值,就構(gòu)成了一個條件語句。 - 結(jié)果
滿足條件后最終得到的結(jié)果也就是結(jié)果定義中的字段值。
3.2.2 規(guī)則生成
催收系統(tǒng)提供了可視化頁面配置來動態(tài)生成腳本的功能(業(yè)務(wù)人員根據(jù)條件定義和結(jié)果定義來編輯決策表進而制定相應(yīng)規(guī)則)。
核心流程:
1.根據(jù)規(guī)則類型解析相應(yīng)的事實對象映射文件,并封裝成條件實體entitys與結(jié)果實體resultDefs,文件內(nèi)容如下圖:
事實對象映射xml
2.根據(jù)規(guī)則類型查詢規(guī)則集完整數(shù)據(jù)
3.將規(guī)則集數(shù)據(jù)與xml解析后的對象進行整合,拼裝成一個drl腳本
4.將拼裝好的腳本保存到數(shù)據(jù)庫規(guī)則集表中
/**
* 生成規(guī)則腳本
* rule規(guī)則基本信息:包括規(guī)則表字段名定義等
* def 業(yè)務(wù)人員具體錄入規(guī)則集的條件和結(jié)果等數(shù)據(jù)
*/
public String generateDRLScript(DroolsRuleEditBO rule, DroolsRuleTableBO def) {
//解析事實對象映射XML文件,生成條件定義與結(jié)果定義
RuleSetDef ruleSetDef = RuleSetDefHelper.getRuleSetDef(rule.getRuleTypeCode());
// 1.聲明規(guī)則包
StringBuilder drl = new StringBuilder("package ").append(rule.getRuleTypeCode()).append(";\n\n");
HashMapmyEntityMap = Maps.newHashMap(); // k,v => caseSumNew,CaseSumNewWrapper
// 2.導(dǎo)入 entity 對應(yīng)執(zhí)行類
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.規(guī)則腳本注釋
drl.append("http:// ").append(rule.getRuleTypeCode()).append(" : ").append(rule.getRuleTypeName()).append("\n");
drl.append("http:// version : ").append(rule.getCode()).append("\n");
drl.append("http:// createTime : ").append(DateUtil.getSysDate(DateUtil.PATTERN_TIME_DEFAULT)).append("\n\n");
MapmyResultMap = def.getResultDefs().stream().collect(Collectors.toMap(DroolsRuleCondBO::getCondKey, DroolsRuleCondBO::getScript));
// 4.寫規(guī)則
AtomicInteger maxRowSize = new AtomicInteger(0); // 總規(guī)則數(shù)
rule.getTables().forEach(table -> {
String tableCode = table.getTableCode();
table.getRows().stream().filter(r -> !Objects.equals(r.getStatus(), 3))
.forEach(row -> {
// 3.1.規(guī)則屬性及優(yōu)先級
drl.append("http:// generated from row: ").append(row.getRowCode()).append("\n");
//TODO 需要保證row.getRowSort()不重復(fù),否則生成同樣的規(guī)則編號
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.規(guī)則結(jié)果
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 規(guī)則執(zhí)行
核心流程:
//核心流程代碼:
KnowledgeBuilder kb = KnowledgeBuilderFactory.newKnowledgeBuilder();
kb.add(ResourceFactory.newByteArrayResource(script.getBytes(StandardCharsets.UTF_8)), ResourceType.DRL); //script為規(guī)則腳本
InternalKnowledgeBase base = KnowledgeBaseFactory.newKnowledgeBase();
KieSession ksession = base.newKieSession();
AgendaFilter filter = RuleConstant.DroolsRuleNameFilter.getFilter(ruleTypeCode);//獲取一個過濾器
kSession.insert(fact);
kSession.fireAllRules(filter);
kSession.dispose();
- 根據(jù)規(guī)則類型從規(guī)則集表中查詢drl腳本
- 將腳步添加至KnowledgeBuilder中構(gòu)建知識庫
- 獲取知識庫InternalKnowledgeBase(在新版本中對應(yīng) Kmodule中的Kbase)
- 通過InternalKnowledgeBase創(chuàng)建KieSession會話鏈接
- 創(chuàng)建AgendaFilter來制定執(zhí)行某一個或某一些規(guī)則
- 調(diào)用insert方法將事實對象fact插入工作內(nèi)存
- 調(diào)用fireAllRules方法執(zhí)行規(guī)則
- 最后調(diào)用dispose關(guān)閉連接
四、總結(jié)
本文主要由催收系統(tǒng)中的一個案例引出規(guī)則引擎Drools,然后詳細(xì)介紹了Drools的概念與用法以及模式匹配的原理Rete算法。最后結(jié)合催收系統(tǒng)給大家講解了Drools在催收系統(tǒng)中是如何使用的。
通過規(guī)則引擎的引入讓開發(fā)人員不再需要參與到規(guī)則的開發(fā)與維護中來,極大節(jié)約了開發(fā)成本。通過自研的催收系統(tǒng)可視化決策表,讓業(yè)務(wù)人員可以在系統(tǒng)中靈活配置維護規(guī)則而不需要每次編寫復(fù)雜的規(guī)則腳本,解決了業(yè)務(wù)人員的痛點。系統(tǒng)本質(zhì)上還是執(zhí)行的規(guī)則腳本,我們這里是把腳本的生成做了優(yōu)化處理,先通過可視化頁面錄入規(guī)則以結(jié)構(gòu)化的數(shù)據(jù)進行存儲,再將其與規(guī)則定義進行整合拼裝,最終由系統(tǒng)自動生成規(guī)則腳本。
當(dāng)前催收系統(tǒng)中的規(guī)則引擎仍然存在著一些問題,例如:
- 催收系統(tǒng)通過動態(tài)生成腳本的方式適合比較簡單的規(guī)則邏輯,如果想實現(xiàn)較為復(fù)雜的規(guī)則,需要寫很多復(fù)雜的代碼,維護成本比較高。
- 催收系統(tǒng)雖然使用的drools7.x版本,但是使用的方式依然使用的是5.x的程序化構(gòu)建器方法(Knowledge API)
- 催收系統(tǒng)目前規(guī)則固定頁面上只能編輯無法新增規(guī)則,只能通過初始化數(shù)據(jù)庫表的方式新增規(guī)則。
后續(xù)我們會隨著版本的迭代不斷升級優(yōu)化,感謝閱讀。
參考文檔:
- ??官方文檔:Drools Documentation???
- ??api文檔:KIE :: Public API 6.5.0.Final API??
當(dāng)前題目:規(guī)則引擎Drools在貸后催收業(yè)務(wù)中的應(yīng)用
當(dāng)前URL:http://m.fisionsoft.com.cn/article/dpcdogj.html


咨詢
建站咨詢
