轉自https://blog.csdn.net/jsyxcjw/article/details/84205631
1.理解KieSession會話
1.1 無狀態和有狀態的KieSession會話
-
我們早已經知道,KieSession有兩種不同的形式:stateless(無狀態)和stateful(有狀態).我們所涵蓋的大部分例子只涉及有狀態的KieSession;這是一個很好的理由,即有狀態的KieSession是到目前為止是Drools支持的最強大的會話類型。
-
在我們決定要使用哪種類型的會話之前,我們需要了解這兩種會話類型之間的區別和相似之處。為了這樣做,我們將從最簡單的會話開始: 無狀態KieSession.
1.1.1 無狀態會話stateless KieSession
-
從開發的角度來看,我們希望在特定場景中使用的會話類型不是由規則決定的 ---或者我們想使用的其他資產類型。session會話的類型僅僅由我們在kmoudle.xml中如何定義的來決定,或者是當我們以編程的方式在代碼中實例化它。在大多數情況下,同一組資產(.drl文件,決策表,等等)可以在無狀態或有狀態會話中執行。
-
那么什么是無狀態的 KieSession呢??最好的比喻就是,無狀態的KieSession會將這種會話描述為一個函數,我們知道函數式無狀態以及原子的。
-
通常,函數是接收一組預先定義的參數,處理它們,並生成輸出或結果。在許多編程語言中,函數的結果可以是返回值本身,也可以是一些輸入參數的修改。理想情況下,函數不應該具有任何間接影響,這意味着如果在同一組參數中多次調用該函數,結果應該是相同的。
-
無狀態的 KieSession與我們描述的函數有一些相同的概念:它有一些定義松散的輸入參數集,它以某種方式處理這些參數,並生成響應。就像函數一樣,在相同的無狀態KieSession中不同的調用不會相互干擾。
-
為了得到一個無狀態的 Kie Session,我們首先需要定義我們想要用的KieBase來實例化它。一種方式使通過kmoudle.xml:
-
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-
xmlns="http://jboss.org/kie/6.0.0/kmodule">
-
<kbase name="KBase1">
-
<ksession name="KSession1" type="stateless" default="true/">
-
</kbase>
-
上面代碼中,我們將名為KSession1的KieSession定義為了一個無狀態的會話.
-
PS:如果不指定的話,默認就是以有狀態的會話來生成
-
那么接下來就是實例化這個名為 Ksession1的會話,我們可以使用下面的代碼摘要來完成:
-
KieServices kieServices = KieServices.Factory. get();
-
KieContainer kContainer = kieServices.getKieClasspathContainer();
-
StatelessKieSession statelessKsession = kContainer. newStatelessKieSess
-
ion("KSession1");
暴露API的StatelessKieSession類是與KieSession對等的,我們通常與有狀態會話進行交互的方式是:我們將一組事實數據插入其中,執行任何激活的規則,然后提取我們正在尋找的響應。在有狀態的會話中,插入和執行分為兩種不同的方法:insert()和 fireAllRules() 。在無狀態會話的情況下,這兩個方法結合成一個單獨方法:execute().而這個execute()有下面三個重載版本:
-
execute(Object fact)
-
execute(Iterable facts)
-
execute(Command command)
-
前兩個版本的 execute(...)是將我們傳遞給引擎的事實數據當做參數傳遞進方法,觸發所有激活的規則,並安排我們之前創建的會話。連續的調用者三個方法,只會重復的做着三步。請記住,在esxecute()結束后,任何與先前調用相關的內容都將被丟棄。
-
第三個版本你的 execute()允許我們去通過一個命令模式與會話進行交互.Drools已經有了一組預定義的可用命令,比如:InsertObjectCommand , SetGlobalCommand , FireAllRulesCommand ,等等。雖有的可用命令都可以使用CommmandFactory來創建。使用BatchExecutionCommand接口,可以將命令可以組合在一起。
-
一個無狀態的KieSession的典型用法如下代碼所示:
-
List<Command> cmds = new ArrayList<>();
-
cmds. add( CommandFactory.newSetGlobal( "list1", new ArrayList(), true
-
) );
-
cmds. add( CommandFactory.newInsert( new CustomerBuilder().withId(1L).
-
build(), "customer1" ) );
-
cmds. add( CommandFactory.newQuery( "Get Customers" "getCustomers" );
-
ExecutionResults results = ksession.execute( CommandFactory.
-
newBatchExecution( cmds ) );
-
results.getValue( "list1" ); // returns the ArrayList
-
results.getValue( "customer1" ); // returns the inserted Customer fact
-
results.getValue( "Get Customers" );// returns the query as a QueryResults instance
-
如果無狀態會話提供有狀態對等的操作的子集,那么我們為什么需要它們呢?從技術上講,我們不是。任何可以通過無狀態的KieSession會話完成的事情,都可以用有狀態的方法完成。使用無狀態會話更像是一個顯式的語句,它表示我們只是希望使用一個一次性的會話,它將用於一次性的評估。無狀態會話是對無狀態場景的理想選擇,例如數據驗證、計算(例如風險評估或抵押貸款利率)和數據過濾。
-
由於它的無狀態性,無狀態的Kie Session不需要處理。在每次調用了 execute()方法之后,執行的資源就會被釋放。 此時,如果需要的話,另一個執行輪的無狀態的Kie Session也已經准備好了。因此,每一個execute()的執行都將於前一個是獨立的,就是原子性~~
-
總結,無狀態的Kie Session是理想的無狀態評估,例如:
-
>數據驗證
-
>計算
-
>數據過濾
-
>消息路由
-
>可以描述為規則的任何復雜的函數或者表達式
1.1.2 有狀態的會話 KieSession
-
有狀態會話與無狀態會話最大區別就是,它可以在交互的同時,保持住狀態。此類場景的一個常見示例是我們正在使用的一個監視進程的會話。理想情況下,我們想越快越好的想我們所監控的進程中插入任何傳入事件,我們也希望盡快發現問題。在這些情況下,我們不能等到所有的事件都發生了(因為他們可能會永遠不會停下來),所以我們可以創建一個 BatchExecutionCommand對象,在無狀態會話中執行。更好的方法是在每個事件到達時就插入,執行一個激活的規則,獲取一個生成的結果,然后等待下一個事件到達。當下一次事件到來時,我們不想在一個新的會話中處理它,我們希望使用與之前的事件相同的會話。這就是有狀態的會話。
-
當我們不想再使用 KieSession的時候,那么我們必須顯示的調用dispose()方法來聲明下。這個方法釋放會話可能獲得的任何資源,並釋放它可能分配的任何內存。dispose()方法調用后,,會話保持在一個無效的狀態,如果進一步與這個session會話進行交互,就會得到一個java.lang.IllegalS,tateException異常。
-
就像 StatelessKieSession,KieSession接口也支持以它的execute()方法的命令模式。當我們處理持久化會話時,這個命令模式的交互是相關的。在這種情景下,所有的傳遞給一個單獨的execute()的命令都在一個單獨的事務中被執行。持久化的更多的知識,后續會將.
-
現在我們已經對 Drools提供的不同類型的會話有了更好的理解,讓我們來看看一些高級的配置選項。
1.2 運行時組件(Kie runtime components)
-
Drools為我們提供了幾個會話的配置選項 ---無論它們是無狀態的還是有狀態的。在本節中,我們將介紹一些選項,以使我們能夠充分利用Drools的潛力,從而配置我們的會話。
-
我們與Drools的session會話交互的最普通的方式就是通過插入/修改/收回實數數據,並執行任何可能由於這些操作而發生的規則激活。所有這些操作都針對規則引擎的不同方面 ---例如知識的斷言和推理---但是也有一些其他的方法可以與會話交互,這些會話可以用來提供或從中提取信息。這些操作更面向Drools運行的應用程序,而不是規則引擎本身。我們將在本節中討論的選項是全局、通道、查詢和事件監聽器。
-
PS:盡管上面的四個對於有無狀態的會話都可用,但是我們這里只討論有狀態的會話。
1.2.1 Globals
-
我們之前提過全局變量。
-
即使全局變量可以在會話內部使用,也不會暴露在外部世界中,它們通常被用作從會話中引入/提取信息的一種方式。在許多情況下,全局是會話和外部消息之間的聯系點。
-
當使用有狀態會話時,在KieSession類中有三種方法與全局變量相關。這些方法如下表所示:
-
方法 描述 void setGlobal(String identifier,Object value); 該方法用於設置全局值。 在同一個會話中多次調用此方法將更新任何先前設置的全局值。用於調用此方法的標識符,必須與我們在Knowledge Base(我覺得其實想說的是規則文件)中配置的標識符(的名字)相匹配 Globals getGlobals() 這個方法用於在一個會話中檢索出所有的全局變量。所得到的對象可用於通過標識符來檢索單個全局變量。 Object getGlobal(String identifier); 該方法用於檢索全局變量的標識符。
正如您所看到的,在會話中如何與全局變量進行交互並沒有太多的知識。前面表中描述的三種方法幾乎是自解釋的。
在Drools中使用一個全局的常用方法有四種,如下所示:
在規則的LHS,作為參數化模式條件的一種方法
在規則的LHS,作為一種在會話中引入新信息的方法
在規則的RHS,作為一種從會話中收集信息的方法
在規則的RHS,作為一種與外部系統交互的方式
無論在會話中如何使用全局,重要的是要注意到,全球變量並非事實數據,也就是說它並不會去觸發規則。Drools將以一種完全不同的方式對待全局變量和事實數據;全局變量的變化永遠不會被Drools發現,因此,Drools永遠不會對它們做出反應。
讓我們分析一下我們之前列出的一個全局的四種常見場景。
1.2.1.1 全局變量是參數化模式條件的一種方法
-
通常在Drools中使用全局變量的一種方法是作為一種外部參數來表示規則的條件。其思想是在規則的條件下使用全局變量而不是硬編碼的值。
-
作為一個例子,讓我們回到我們的eShop示例。假設我們想要一個Drools會話來檢測我們的eShop應用程序中客戶的可疑操作。我們將定義一個可疑的操作,作為一個客戶,在等待操作的總金額超過1萬美元。
-
我們的會話的輸入將是我們的應用程序的客戶以及客戶的訂單。對於每一個等待訂單超過1萬美元的客戶,我們將插入一個新的 SuspiciousOperation類型的對象。 SuspiciousOperation 類的結構如下:
-
public class SuspiciousOperation {
-
public static enum Type {
-
SUSPICIOUS_AMOUNT,
-
SUSPICIOUS_FREQUENCY;
-
}
-
private Customer customer;
-
private Type type;
-
private Date date;
-
private String comment;
-
public SuspiciousOperation(Customer customer, Type type) {
-
this.customer = customer;
-
this.type = type;
-
}
-
//setters and getters
-
}
以下規則足以完成檢測是否可疑操作的目標:
- 1
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > 10000.0 ) from accumulate (
-
Order ( customer == $c, state != OrderState.COMPLETED, $total:
-
total),
-
sum($total)
-
)
-
then
-
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.
-
SUSPICIOUS_AMOUNT));
-
end
-
規則很簡單:對於每一個消費者客戶,收集任何OrderState不是COMPLETED 狀態的訂單 ,並計算其總數的總和。如果這個總數大於10000,那么規則就會被激活。當規則的RHS執行的時候,它將會插入一個新的 SuspiciousOperation類型對象進會話內。
-
我們早已知道,如果我們想要去執行規則,我們需要將其作為我們的配置文件中Knowledge Base(我覺得想說的是規則文件)中的一部分,從它里面創建會話,並提供一些事實數據給他。就像下面這樣:
-
// Create a customer with PENDING orders for a value > 10000
-
Customer customer1 = new CustomerBuilder()
-
.withId( 1L).build();
-
Order customer1Order = ModelFactory.getPendingOrderWithTotalValueGreaterThan10000(customer1);
-
// Create a customer with PENDING orders for a value < 10000
-
Customer customer2 = new CustomerBuilder()
-
.withId( 2L).build();
-
Order customer2Order = ModelFactory.getPendingOrderWithTotalValueL
-
essThan10000(customer1);
-
// insert the customers in a session and fire all the rules
-
ksession.insert(customer1);
-
ksession.insert(customer1Order);
-
ksession.insert(customer2);
-
ksession.insert(customer2Order);
-
ksession.fireAllRules();
-
前面代碼的一個運行示例可以在chap-05moudle下的代碼包中找到。
-
前面的示例工作的很好,只要我們認為可疑操作的閾值保持不變。但是,如果我們想要創建這個閾值變量呢?
-
實現這一目標的多種方法的一種方法是用全局變量替換我們規則中的硬編碼值,這個變量可以在我們想運行會話時定義,就像下面這樣:
-
global Double amountThreshold;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( customer == $c, state != OrderState.COMPLETED, $total: total),
-
sum($total)
-
)
-
then
-
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
在前面的例子中,我們可以看到硬編碼的閾值在DRL中不再存在。我們現在再我們的規則中使用一個全局的 Double變量。通過使用這種方法,我們認為是一個可疑的操作的判斷條件就可以通過不同的會話來控制了。
-
PS:在我們的會話執行過程中,沒有任何東西可以阻止我們修改全局變量。即使這是可能的,也不鼓勵修改在運行時在約束中使用的全局值。鑒於Drools的聲明性特性,我們無法預測在這些情況下修改全局變量的值會帶來什么影響。
-
需要指出的一件重要的事情是,當全局變量作為規則約束的一部分使用時,全局變量必須在使用它的模式被評估之前進行設置。為了避免競態條件,在插入任何事實數據之前,設置會話的全局變量被認為是一種良好的實踐。在規則約束中使用全局變量的一個缺點是,它們的值不會被Drools緩存。每當一個全局變量需要被評估時,它的值就被訪問。當Knowledge Base(我覺得想表達式我們的規則文件)很大的時候,這可能會促使出現性能問題。
-
PS:考慮到使用globals對我們的規則參數化的所有缺點,不推薦模式。一個更好的方法是參數化我們的規則的條件,那就是將參數作為事實在我們的會話中,並將它們作為任何其他類型的事實對待。在本書中包含這種模式只是為了完整性。
1.2.1.2 global作為一種在LHS上將新信息引入到會話的方法
-
另一個與glaobal相關的常見模式是作為會話的數據源使用。通常,這種類型的全局封裝了將新對象引入會話的服務(數據庫,內存映射,web service,等等)的調用。這種使用模式通常涉及到 from條件元素。
-
為了演示此場景,我們將修改前一節中介紹的示例,並引入一個服務調用來檢索客戶的訂單。該服務將被建模為OrderService接口,包含一個單一的方法-getOrdersByCustomer-如下代碼所示:
-
public interface OrderService {
-
public Collection<Order> getOrdersByCustomer(String customerId);
-
}
這里的想法是使用這個接口作為全局的,我們的規則可以用來檢索與客戶相關的所有訂單。本示例的DRL的最終版本將與下面的代碼類似:
-
global Double amountThreshold;
-
global OrderService orderService;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
-
sum($total)
-
)
-
then
-
insert( new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
在這個版本的示例中,我們仍然使用全局來保持我們認為可疑操作的閾值,但是現在我們也有了一個新的全局變量orderService。我們的規則是調用全局的getOrdersByCustomer方法來獲取特定客戶的所有訂單,而不是從客戶的orders屬性獲取訂單。
-
在這個簡單的例子中,我們可能沒有意識到這種方法的優點 ---客戶的訂單現在只在需要時才被提取。在之前的版本的規則中,我們必須預先為所有客戶預取所有的訂單,然后將它們插入會話中。在插入的時候,我們不知道session是否真的需要所有客戶的訂單。
-
如前所述,在將任何客戶插入會話之前,我們需要記住設置orderService全局的值,如下所述:
-
OrderService orderServiceImpl = new OrderServiceImpl();
-
//a concrete implementation of OrderService.
-
ksession.setGlobal( "orderService", orderServiceImpl);
-
ksession.insert(customer1);
-
ksession.insert(customer2);
-
ksession.fireAllRules();
-
在前面的代碼中要注意的一件重要的事情是,我們不再將訂單作為事實數據插入。這些命令將根據規則本身的要求進行檢索。不過,有一個問題,規則的條件可以在執行規則時多次被計算。每次對規則進行重新評估時,都會調用數據源。在使用這種模式時,必須考慮數據源的延遲。
-
我們了解了如何將 global作為外部系統的接口,以便檢索和引入(但不插入)新信息到會話中。現在的問題是如何從會話中提取生成的可疑操作對象?
1.2.1.3 全局變量作為一種從會話中收集信息的方式
-
前一個示例中的規則為每個可疑操作插入了一個 SuspiciousOperation對象。問題是這些事實數據不能從會話的外部獲得。從會話中提取信息的一個常見模式是使用全局變量。
-
此模式背后的思想是使用一個全局變量來收集我們想從會話中提取的信息。由於全局可以從會話外部訪問,所以它引用的任何事實、對象或值也可以訪問。這類局最通用的一類是 java.util.Collection或者java.util.Map 。
-
現在,我們將修改前一節中使用的規則文件,添加一個新規則,將任何可疑操作事實數據收集到一個全局集合中:
-
global Double amountThreshold;
-
global OrderService orderService;
-
global Set results;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
-
sum($total)
-
)
-
then
-
insert( new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
-
rule "Collect results"
-
when
-
$so: SuspiciousOperation()
-
then
-
results.add($so);
-
end
-
代碼顯示我們現在有一個新的叫做results的全局變量和一個新規則,這個新的規則它將收集任何 SuspiciousOperation類的實例。
-
執行這個例子的新版本的相關Java代碼如下所示:
-
Set<SuspiciousOperation> results = new HashSet<>();
-
ksession.setGlobal( "results", results);
-
ksession.insert(customer1);
-
ksession.insert(customer2);
-
ksession.fireAllRules();
-
//variable 'results' now holds all the generated SuspiciousOperation objects.
在執行規則之后,全局集將包含在會話執行期間生成的任何SuspiciousOperation對象的引用。然后,我們可以在創建它們的會話之外使用這些對象。
1.2.1.4全局變量是在RHS中與外部系統交互的一種方式
-
我們將要討論的關於全局的最后一個常見的使用模式是,在規則的RHS使用全局變量作為與外部系統交互的一種方式。這個模式背后的想法很簡單,我們看到我們可以使用 global變量將新信息引入到一個模式中(使用from條件元素)。我們還可以使用全局變量在RHS中來與外部系統進行交互。與此外部系統的交互可以是單向的(從系統獲取信息或向系統發送信息)或雙向(從系統發送和接收信息)。
-
繼續前面的例子,假設現在我們想要通知一個外部審計系統發現的每個SuspiciousOperation。這里有兩個選項,我們現在知道我們可以使用上一節介紹的全局集合來訪問這些生成的事實。我們可以從Java代碼中迭代這個列表,並將每個元素發送到審計系統。另一個選擇是在會話本身中利用它。
-
這個新接口將通過一個名為AuditService的接口來表示。該接口將定義一個單獨的方法-notifySuspiciousOperation——下面的代碼所示:
-
public interface AuditService {
-
public void notifySuspiciousOperation(SuspiciousOperation
-
operation);
-
}
我們需要添加這個接口的一個實例作為全局變量並創建一個新規則調用其notifySuspiciousOperation方法或修改Collect results規則,讓這個規則現在也調用notifySuspiciousOperation這個方法。讓我們采用第一個方法,添加一個新規則來通知審計系統:
-
global Double amountThreshold;
-
global OrderService orderService;
-
global AuditService auditService;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
-
sum($total)
-
)
-
then
-
insert( new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
-
rule "Send Suspicious Operation to Audit Service"
-
when
-
$so: SuspiciousOperation()
-
then
-
auditService.notifySuspiciousOperation($so);
-
end
-
我們創建的新規則是使用我們定義的新全局來通知審計系統每個生成的SuspiciousOperation對象.重要的是要記住Drools總是在一個單獨線程中執行規則。理想情況下,我們的規則的RHS不應該涉及到阻塞操作。在需要阻塞操作的情況下,引入異步機制來在單獨的線程中執行阻塞操作,這在大多數情況下被認為是一個不錯的選擇。
-
在Drools中,我們已經討論了globals的四種常見用法模式,接下來討論:管道
1.2.2 管道
-
通道是一種標准化的方式,可以將數據從會話內部傳輸到外部世界。
-
一個通道可以完全用於我們在上一節討論的內容:全局變量作為與RHS外部系統交互的一種方式。我們可以通過使用一個通道來完成相同的任務,而不是使用全局。
-
從技術上講,通道是一個帶有單一方法的Java接口---- void send(Object object),如下:
-
public interface Channel {
-
void send(Object object);
-
}
通道只能在我們的規則的RHS中使用,作為將數據發送到會話外部的一種方式。在使用通道之前,我們需要在會話中注冊它。“KieSession”類提供了以下三種處理管道的方法:
- 1
方法 | 描述 |
---|---|
void registerChannel(String name,Channel channel | 這個方法用於將管道注冊給會話。當一個管道被注冊了,必須要提供一個名字,這個名字被會話用來作為管道的標識 |
void unregisterChannel(String name) | 這個方法與registerChannel相對,被用於從會話中刪除某個已經注冊的管道。傳遞的name屬性是用來定位待刪除管道的 |
Map< String, Channel> getChannels() | 這個方法被用於檢出任何以前注冊的管道。返回的Map的鍵標識的是在注冊管道的時候所指定改的名字 |
在規則的RHS中,無論何時我們想要與通道進行交互,我們都可以通過預定義的channels RHS變量獲得對它的引用。這個變量提供了一個類似於映射的接口,允許我們通過它的名稱引用特定的通道。例如,如果我們已經注冊了一個notifications名稱的通道,我們可以使用以下代碼片段在我們的規則的RHS中與之交互:
channels["notifications"].send(new Object());
- 1
-
通道接口的具體實現可用於將數據路由到外部系統,通知事件,等等。請記住,通道代表一種單向傳輸數據的方式:send()方法在Channel接口中的返回值是 void。
-
讓我們從上一節重構示例,以利用一個通道而不是全局變量來通知審計系統關於可疑操作的情況。
-
我們需要做的第一件事就是去掉我們在規則中所擁有的auditService變量。本例的要點是用一個通道來替換這個全局變量。然后,我們需要從“Send Suspicious Operation to Audit Service”規則里替換RHS,因此,它使用的是一個管道而不是舊的全局變量:
-
rule "Send Suspicious Operation to Audit Channel"
-
when
-
$so: SuspiciousOperation()
-
then
-
channels[ "audit-channel"].send($so);
-
end
現在,在我們根據這個KieBase所依賴的規則文件,執行一個會話之前,我們需要注冊管道給這個會話,管道名字叫“ audit-channel”。為了這樣做,我們使用了我們已經討論過的registerChannel方法,如下:
- 1
-
Channel auditServiceChannel = new Channel(){
-
-
@ Override
-
public void send(Object object) {
-
//notify AuditService here. For testing purposes, we are just
-
//going to store the received object in a Set.
-
results. add((SuspiciousOperation) object);
-
}
-
-
};
-
ksession.registerChannel( "audit-channel", auditChannel);
-
正如我們所看到的,一個通道提供了一個比全局變量更嚴格、但定義良好的契約。就像使用全局變量一樣,我們可以使用通道的不同實現來在規則中提供不同的運行時行為。
-
通道的優點之一是它們提供的多功能性,因為它們使用一個字符串的鍵進行索引。就像使用全局變量一樣,我們可以使用通道的不同實現來在規則中提供不同的運行時行為。通道的鍵可以在運行時,在 LHS中作為綁定或在規則的RHS中來確定。
-
讓我們以更靈活的方式從一個會話中提取信息 :QUERY
1.2.3 Queries
-
在Drools中,一個查詢可以被看作是一個沒有RHS的常規規則。Query和Rule之間的主要區別是前者可能會接受參數。使用查詢,我們可以使用Drools模式匹配語法的所有功能從會話中提取信息。在Runtime時,我們可以執行查詢,並根據結果執行任何操作。在某種程度上,查詢是一個具有動態RHS的規則。
-
PS:查詢也可以作為規則內的常規模式使用。這是Drools逆向鏈接推理能力的基礎。本節只關注查詢作為從會話中提取信息的一種方法。
-
繼續使用我們之前使用的示例,現在讓我們創建一個查詢來從會話中提取所有生成的可疑操作事實。執行此操作所需的Query與下面的查詢相似:
-
query "Get All Suspicious Operations"
-
$so: SuspiciousOperation()
-
end
正如我們所看到的,我們創建的查詢看起來就像一個沒有它RHS的規則。如果我們對某個特定的客戶感興趣,那么我們可以定義另一個查詢,它以用戶ID作為參數,並過濾掉所有相關的SuspiciousOperation對象。
-
query "Get Customer Suspicious Operations" (String $customerId)
-
$so: SuspiciousOperation(customer.customerId == $customerId)
-
end
-
查詢的參數定義類似於Java類中的方法的參數:每個參數都有一個類型和一個名稱
-
從會話外部執行查詢有兩種方法:按需查詢和實時查詢。讓我們更詳細地分析它.
1.2.3.1 按需查詢
按需評估查詢通過調用 KieSession's getQueryResults方法:
public QueryResults getQueryResults(String query, Object... arguments);
該方法接受Query的名稱和它的參數列表(如果有的話)。參數的順序對應於查詢定義中的參數的順序。這個方法的結果是一個QueryResults對象:
-
public interface QueryResults extends Iterable<QueryResultsRow> {
-
String[] getIdentifiers();
-
Iterator<QueryResultsRow> iterator();
-
int size();
-
}
-
QueryResults接口擴展了Iterable,並代表了QueryResultsRow對象的集合.getidentifier()方法返回查詢標識符的數組。查詢中定義的任何綁定變量都將成為其結果中的標識符。比方說,我們的“ Get All Suspicious Operations” 這個Query只定義了一個標識符:$so.當執行查詢時,標識符用於檢索綁定變量的具體值。
-
下面的代碼可以用於執行 " Get All Suspicious Operations"這個Query:
-
QueryResults queryResults = ksession.getQueryResults( "Get All
-
Suspicious Operations");
-
for (QueryResultsRow queryResult : queryResults) {
-
SuspiciousOperation so = (SuspiciousOperation) queryResult.get( "$so");
-
//do whatever we want with so
-
//...
-
}
前面的代碼執行" Get All Suspicious Operations "查詢,然后遍歷結果提取$ so標識符的值,在這種情況下, 即SuspiciousOperation類的實例。
1.2.3.2 實時查詢
-
當我們想要在特定時間點執行特定的查詢時,就會使用按需查詢。Drools還提供了執行查詢的另一種方式,它允許我們將偵聽器附加到查詢中,以便在它們可用時,我們可以得到有關結果的通知。
-
實時查詢使用下面的KieSession方法執行:
-
public LiveQuery openLiveQuery( String query,Object[] arguments,
-
ViewChangedEventListener listener);
-
與按需查詢一樣,我們需要傳遞給這個方法的第一個參數是我們想要附加一個偵聽器的Query的名稱。第二個參數是查詢所期望接收的參數數組。第三個參數是我們想要連接到查詢的實際偵聽器。方法返回值是:LiveQuery的實例。
-
讓我們仔細看一下ViewChangedEventListener接口:
-
public interface ViewChangedEventListener {
-
public void rowInserted(Row row);
-
public void rowDeleted(Row row);
-
public void rowUpdated(Row row);
-
}
-
正如我們所看到的,ViewChangedEventListener接口不僅用於接收與指定Query匹配的新事實數據,而且還可以檢測這些事實數據的修改或撤銷。Drools引擎將在一個事實數據與指定的查詢匹配時通知這個偵聽器,當一個先前匹配的事實數據被修改,或者先前匹配的事實數據的修改將它從查詢的過濾器中排除。
-
在上一節中,我們了解了如何使用全局變量將結果、操作和一般規則執行信息傳遞給外部世界。然而,如果我們想要在不修改現有規則的情況下,以通用的方法來實現這一目標呢?為此,我們還有其他機制,比如事件監聽器。
1.2.4 Event Listeners事件監聽器
-
Drools框架為用戶提供了一種將事件監聽器連接到兩個主要組件:Kie Bases和Kie Bases的機制。
-
Kie Base中的事件與它所包含的包的結構有關。使用org.kie.api. event.kiebase.KieBaseEventListener,例如,我們可以在從KieBase中添加或刪除一個包之前或之后得到通知。使用同樣的監聽器,我們可以更深入地了解在一個KieBase中正在修改的內容,比如單獨的規則、函數和被刪除/刪除的流程。
-
可以使用 KieBase public void addEventListener(KieBaseEventListener listener)方法將KieBaseEventListener連接到KieBase。一個KieBase可以有零個,一個,甚至多個監聽器綁定給它。當一個特殊的事件被觸發了,KieBase將會順序執行在每個以前注冊的事件監聽器中的相應的方法。偵聽器的執行順序並不一定與它們注冊的順序相對應。
-
Kie會話的事件的事件與Drools的運行時執行有關。一個可以被Kie會話觸發的的事件可以分為三個不同的類別:規則執行運行時( org.kie.api.event.rule.RuleRuntimeEventListener),agenda-related事件( org.kie.api.event.rule.AgendaEventListener)以及流程執行運行時( org.kie.api.event.process.ProcessEventListener ).
-
所有這三種類型的事件監聽器都可以使用KieSession addEventListener方法連接到一個Kie會話中:
-
-
public void addEventListener(RuleRuntimeEventListener listener)
-
public void addEventListener(AgendaEventListener listener)
-
public void addEventListener(ProcessEventListener listener)
-
一個RuleRuntimeEventListener可以被用於與在KieSession中事實對象的狀態有關的事件的通知。KieSession中的事實數據,在它們插入,修改或者從會話中檢出的時候,它的狀態會改變。這種類型的偵聽器被用於對會話執行情況的統計分析或報告。
-
AgendaEventListener是一個接口,我們可以用它去通知在Drools的agenda組內發生的事件。Agenda事件與被創造、被取消或被觸發的規則相關聯;agenda組從激活的的agenda棧中被推或者彈出,或是rule-flow組被激活和失活。AgendaEventListeners是一個審計工具的基礎的助手方法。例如,能夠知道規則何時被激活,在分析一個Kie會話的執行時是一個很有價值的信息。
-
ProcessEventListeners與jBPM事件,允許我們在當流程實例啟動或完成時,或在流程實例中的單個節點被觸發之前/觸發之后得到通知。
-
一種更聲明性的方式來配置我們想在會話內使用的事件監聽器,那就是將他們定義在kmoudle.xml中,作為 <ksession>組件的一部分。
-
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-
xmlns="http://jboss.org/kie/6.0.0/kmodule">
-
<kbase name="KBase1" default="true" packages="org.domain">
-
<ksession name="ksession1" type="stateful">
-
<ruleRuntimeEventListener type="org.domain.
-
RuleRuntimeListener"/>
-
<agendaEventListener type="org.domain.FirstAgendaListener"/>
-
<processEventListener type="org.domain.ProcessListener"/>
-
</ksession>
-
</kbase>
-
</kmodule>
Drools中的所有事件偵聽器都在Drools框架運行的同一線程中執行。這種行為有兩個含義:事件偵聽器應該是輕量級和快速的,它們不應該拋出異常。或者甚至是最糟糕的阻塞任務。執行繁重的處理任務的事件偵聽器--或者更糟的是,阻塞任務---在可以的情況下,這都是需要杜絕的。當事件被觸發時,Drools的執行將不會繼續,直到所有注冊的偵聽器都完成了。Drools中可能觸發事件的操作的執行時間是任務本身的執行時間和被觸發的每個事件偵聽器的執行時間。Drools當前的實現不僅在它正在運行的同一線程中執行事件監聽器,而且當事件偵聽器被觸發時也不采取任何預防措施。拋出異常的事件偵聽器將破壞執行的底層操作的執行。如果我們不想在偵聽器中出現問題時干擾Drools執行,那么在事件偵聽器中捕獲任何可能的異常是必須的。在本章的代碼包中,有一些單元測試顯示了偵聽器是如何注冊的,並在Drools中使用。我們強烈建議讀者查看這些測試並運行、調試,甚至增強它們,以便更好地理解Drools的事件偵聽器功能
- 1
1.3 Kie Base組件
-
我們已經介紹了很多knowledge base的組件,比如說:規則,全局變量,查詢,管道。現在我們討論更高級的主題了,這將使我們能夠創建更簡明和可重用的knowledge。
-
在本節中,我們將討論諸如函數、自定義操作符,和自定義函數積累等主題。所有這些組件都可以用於以一種更簡單而有力的方式建模我們的知識。
1.3.1 函數
-
到目前為止,我們已經討論了Drools中最常見的三個最常見的知識聲明:規則、查詢和聲明類型。還有另一種知識聲明可以用於在knowledge Base中表示無狀態邏輯:函數。Drools中的函數基本上是獨立的代碼,可以隨意地獲取參數,也可能不返回值。函數對於我們希望在knowlwdge Base中定義邏輯而不是在外部Java類中定義邏輯的情況非常有用。
-
定義一個函數的語法阿玉在java中定義一個方法是差不多的,不過要在前面加一個function關鍵字。函數有返回值類型(他可以是任何的java類型或者是 void),有函數名字以及函數的參數,如下:
-
function String formatCustomer(Customer c){
-
return String.format(
-
"[%s] %s", c.getCategory(), c.getName());
-
}
-
在前面的例子,有一個叫formatCustomer的函數被定義了。這個函數的參數是一個Customer的實例,返回值是一個字符串。函數體使用一個java的格式化表達式;在本例子中,它使用了 String.format()方法來將分類和提供商的名字連接在一起。
-
就像聲明的類型一樣,定義在knowledge Base的函數是一個把邏輯放在一個地方的好方式。Drools中的函數也為我們提供了修改它們的靈活性,而無需重新編譯任何代碼。
-
PS:前面所說的不需要重新編譯任何代碼不是 100%正確的。
-
即使Drools中函數的使用給了我們一定程度的靈活性,它們也有一些限制,如下所列:
-
>函數不能在它被定義所在的包的外部使用。這樣為着定義在DRL文件內的函數不能背用於任何的其他的DRL文件,除非兩者都有相同的包聲明。
-
>函數並不能使用任何的全局變量,事實數據或者預設變數(比如kxontext)。函數的上下文只是在調用時傳遞來的參數集。
-
>由於之前的限制,函數不能在會話中插入、修改或撤消事實
-
當考慮到可重用性和可維護性時,在knowlesge Base中聲明函數並不是最好的方式。幸運的是,Drools允許我們從Java類中導入靜態方法作為函數,並在我們的規則中使用它。為了導入靜態方法,我們需要使用 function關鍵字與import關鍵字相結合。
-
import function org.drools.devguide.chapter05.utils.CustomerUtils.
-
formatCustomer;
-
正如您所看到的,從某種意義上來說,導入一個類的靜態方法就像在Java中導入靜態方法一樣。
-
不管我們的函數是從Java類導入還是在knowledge Base中聲明,我們在規則中調用它們或從另一個函數中調用它們的方法是簡單地使用它的名稱,如下代碼所示:
-
rule "Prepare Customers List"
-
when
-
$c: Customer()
-
then
-
globalList.add(formatCustomer($c));
-
end
前面的例子顯示了在規則的RHS使用了 formatCustomer函數,但是函數也可以在規則的條件部分中使用,如下所示
- 1
-
rule "Prepare Customers List"
-
when
-
$c: Customer($formatted: formatCustomer($c))
-
then
-
...
-
end
現在讓我們轉到Drools的另一個功能強大的特性,它允許我們使用專門的操作符來增強DRL語言,這些操作符可以用來創建更簡明、可讀和可維護的規則:自定義操作符。
1.3.2 自定義操作符
-
在指定我們的LHS的時候,我們可以使用許多的操作符,比如:== , != , < , >,這些都是Drools所支持的。不過,在某些情況下,可用的操作符還不夠。涉及比較復雜的邏輯,外部服務或語義推理,是Drools的能力不足的好例子。然而,沒有什么可擔心的;Drools提供了創建自定義操作符的機制,在編寫規則時可以使用這些操作符。
-
在Drools中,自定義操作符通過實現org.drools.core. base.evaluators.EvaluatorDefinition 就接口,定義為一個java類。這個接口只表示操作符的定義。具體的實現被委派給了 org.drools.core.spi.Evaluator的一個實現。
-
-
在自定義操作符作為規則的一部分之前,他必須在被使用的knowledge base中注冊。自定義操作符的注冊是在類路徑中使用配置文件或在kmodule.xml中指定的它。然而,在我們繼續討論自定義操作符是如何注冊的之前,我們先看個例子
-
為了澄清一個自定義操作符是什么以及它是如何定義的,讓我們使用我們的eShop用例中的一個例子.對於本例,我們將實現一個簡單的操作符,它將告訴我們一個Order函數是否包含給定其ID的Item。這個示例可能不是定制操作符最有趣的示例,因為它可以用許多不同的方式進行解析。盡管如此,它還是一個很好的例子,展示了如何構建自定義操作符。
-
我們新的自定義操作符的概念是能夠將規則寫入如下:
-
rule "Apply discount to Orders with item 123"
-
when
-
$o: Order( this containsItem 123)
-
Then
-
modify ($o){ setDiscount(new Discount( 0.1))};
-
end
-
在前面的規則中要注意的重要事項是使用自定義操作符,名字是: containsItem。所有的自定義操作符---和拓展的,任何在Drools中的操作符---都是有兩個參數。在這個特殊的情況下,第一個參數是Order類型,第二個參數是Long類型。操作符總是會對布爾值進行評估。在這種情況下,布爾結果將指示指定的項是否存在於所提供的Order中。
-
為了實現我們的自定義操作符,我們需要做的第一件事就是實現 org.drools.core.base.evaluators.EvaluatorDefinition。在我們的樣例中,我們的實現類叫做ContainsItemEvaluatorDefinition:
-
public class ContainsItemEvaluatorDefinition implements EvaluatorDefinition {
-
-
protected static final String containsItemOp = "containsItem";
-
-
public static Operator CONTAINS_ITEM;
-
public static Operator NOT_CONTAINS_ITEM;
-
-
private static String[] SUPPORTED_IDS;
-
-
private ContainsItemEvaluator evaluator;
-
private ContainsItemEvaluator negatedEvaluator;
-
-
static {
-
if (SUPPORTED_IDS == null) {
-
CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, false);
-
NOT_CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, true);
-
SUPPORTED_IDS = new String[]{containsItemOp};
-
}
-
}
-
-
-
public String[] getEvaluatorIds() {
-
return new String[]{containsItemOp};
-
}
-
-
-
public boolean isNegatable() {
-
return true;
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, Operator operator) {
-
return this.getEvaluator(type, operator.getOperatorString(),
-
operator.isNegated(), null);
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, Operator operator,
-
String parameterText) {
-
return this.getEvaluator(type, operator.getOperatorString(),
-
operator.isNegated(), parameterText);
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, String operatorId,
-
boolean isNegated, String parameterText) {
-
return getEvaluator(type, operatorId, isNegated, parameterText,
-
Target.BOTH, Target.BOTH);
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, String operatorId,
-
boolean isNegated, String parameterText, Target leftTarget,
-
Target rightTarget) {
-
return isNegated ?
-
negatedEvaluator == null ?
-
new ContainsItemEvaluator(type, isNegated) : negatedEvaluator
-
: evaluator == null ?
-
new ContainsItemEvaluator(type, isNegated) : evaluator;
-
}
-
-
-
public boolean supportsType(ValueType type) {
-
return true;
-
}
-
-
-
public Target getTarget() {
-
return Target.BOTH;
-
}
-
-
-
public void writeExternal(ObjectOutput out) throws IOException {
-
out.writeObject(evaluator);
-
out.writeObject(negatedEvaluator);
-
}
-
-
-
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
-
evaluator = (ContainsItemEvaluator) in.readObject();
-
negatedEvaluator = (ContainsItemEvaluator) in.readObject();
-
}
-
-
}
上面的類中,我們看到有很多的信息需要被處理,所以我們先來看看。開始時的靜態塊將兩個新的操作符注冊到Drools的操作符注冊表中。這兩個新的操作符就是containsItem和與其相對的 not containsItem。下一個重要的方法是getEvaluatorsIds(),它告訴Drools所有可能的我們正在定義的操作符的id。緊跟着的方法是 isNegatable(),這個方法標識我們正在創建的操作符是否被否定。接着,是四個不同版本的getEvaluator()方法,這些方法將返回,在編譯時, org.drools.core.base.evaluators.EvaluatorDefinition 的具體的實例,以應該用於每個特定的場景。傳遞給這個方法方法的參數如下:
- 1
參數名 | 介紹 |
---|---|
type | 這是操作數的類型。 |
operatorId | 這是被解析的操作符的標識符ID.一個單獨的操作符定義可以處理多個ID |
isNegated | 這表明正在解析的操作符是否使用了not前綴(被否定)。 |
parameterText | Drools中的一個操作數可以固定在尖括號中定義的參數。具有參數的操作符的例子是Drools融合的CEP操作符,下一個章節會講 |
leftTarget/rightTarget | 這兩個參數指定這個操作符是根據事實數據、事實處理,還是兩者都操作。 |
getEvaluator()的四個版本都返回一個ContainsItemEvaluator 實例。 ContainsItemEvaluator是Drools的 org.drools.core.spi.Evaluator的具體實現類,並且是負責我們操作符運行時行為的類。這個類是我們運算符的真正邏輯—檢查一個Item是否被一個Order所包含:
-
public class ContainsItemEvaluator extends BaseEvaluator {
-
-
private final boolean isNegated;
-
-
public ContainsItemEvaluator(ValueType type, boolean isNegated) {
-
super(type ,
-
isNegated?
-
ContainsItemEvaluatorDefinition.NOT_CONTAINS_ITEM :
-
ContainsItemEvaluatorDefinition.CONTAINS_ITEM);
-
this.isNegated = isNegated;
-
}
-
-
-
public boolean evaluate(InternalWorkingMemory workingMemory,
-
InternalReadAccessor extractor, InternalFactHandle factHandle,
-
FieldValue value) {
-
Object order = extractor.getValue(workingMemory, factHandle.getObject());
-
-
return this.isNegated ^ this.evaluateUnsafe(order, value.getValue());
-
}
-
-
-
public boolean evaluate(InternalWorkingMemory workingMemory,
-
InternalReadAccessor leftExtractor, InternalFactHandle left,
-
InternalReadAccessor rightExtractor, InternalFactHandle right) {
-
Object order = leftExtractor.getValue(workingMemory, left.getObject());
-
Object itemId = rightExtractor.getValue(workingMemory, right.getObject());
-
-
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
-
}
-
-
-
public boolean evaluateCachedLeft(InternalWorkingMemory workingMemory,
-
VariableRestriction.VariableContextEntry context,
-
InternalFactHandle right) {
-
Object order = context.getFieldExtractor().getValue(workingMemory,
-
right.getObject());
-
Object itemId = ((ObjectVariableContextEntry)context).left;
-
-
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
-
}
-
-
-
public boolean evaluateCachedRight(InternalWorkingMemory workingMemory,
-
VariableRestriction.VariableContextEntry context,
-
InternalFactHandle left) {
-
Object order = ((ObjectVariableContextEntry)context).right;
-
Object itemId = context.getFieldExtractor().getValue(workingMemory,
-
left.getObject());
-
-
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
-
}
-
-
private boolean evaluateUnsafe(Object order, Object itemId){
-
//if the object is not an Order return false.
-
if (!(order instanceof Order)){
-
throw new IllegalArgumentException(
-
order.getClass()+ " can't be casted to type Order");
-
}
-
-
//if the value we are comparing aginst is not a Long, return false.
-
// if (!(Long.class.isAssignableFrom(itemId.getClass()))){
-
Long itemIdAsLong;
-
try{
-
itemIdAsLong = Long.parseLong(itemId.toString());
-
} catch (NumberFormatException e){
-
throw new IllegalArgumentException(
-
itemId.getClass()+ " can't be converted to Long");
-
}
-
-
return this.evaluate((Order)order, itemIdAsLong);
-
}
-
-
private boolean evaluate(Order order, long itemId){
-
//no order lines -> no item
-
if (order.getOrderLines() == null){
-
return false;
-
}
-
-
return order.getOrderLines().stream()
-
.map(ol -> ol.getItem().getId())
-
.anyMatch(id -> id.equals(itemId));
-
}
-
-
}
-
不是實現的 org.drools.core.spi.Evaluator,ContainsItemEvaluator,而是繼承自 org.drools.core. base.BaseEvaluator,他是一個實現了接口的樣板代碼類。將具體方法的實現保留在操作符評估實際發生的地方。有四個方法我們必須實現,如下所示:
-
>evaluate:這個方法有兩個版本需要使用實現。這個方法被用於當操作符作為處理單個事實數據的條件中的一部分出現的時候。(在Phreak結構里,條件的類型是所謂的alpha網絡的一部分。 )當涉及到文本約束時,第一個版本被使用,第二個版本涉及到變量綁定時被使用。
-
>evaluateCachedLeft/evaluateCachedRight:當操作符在涉及多個事實數據的的條件下使用時,Drools使用這兩種方法。在我們可以在規則中實際使用這個新操作符之前,我們需要將它注冊到我們想要使用它的knowledge base中,有兩種方式:使用drools.packagebuilder.conf文件或者通過kmoudle.xml文件。
-
第一種將自定義操作符注冊的方式使通過特殊的文件。名叫做 drools.packagebuilder.conf。這個文件必須放在META-INF下,Drools的包構建器自動使用它來讀取正在創建的knowledge base的配置參數。為了注冊一個自定義的操作符,我們需要在這個文件中添加以下一行:
-
drools.evaluator.containsItem= org.drools.devguide.chapter05.
-
evaluator.ContainsItemEvaluatorDefinition
-
這一行必須以 drools.evaluator開頭,緊跟着的就是自定義的操作符的ID。在此之后,必須指定的是定義了自定義操作符的類的完全限定名。
-
第二種注冊自定義操作符的方式使使用kmoudle.xml文件。可以在這個文件中定義配置的特定部分,其中可以將屬性指定為鍵/值對。為了在kmodule.xml中注冊我們創建的自定義操作符,下面的配置部分必須添加到它:
-
<kmodule xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
-
xmlns= "http://jboss.org/kie/6.0.0/kmodule">
-
<configuration>
-
<property key="drools.evaluator.containsItem" value="org.
-
drools.devguide.chapter05.evaluator.ContainsItemEvaluatorDefinition"/>
-
</configuration>
-
</kmodule>
到目前為止,我們已經討論了如何定義自己的自定義操作符來為我們的域創建定制的解決方案,並提高Drools模式匹配能力。讓我們換一種方式,我們必須創建一個自定義的邏輯,以便在我們的規則中使用:自定義累積函數。
- 1
1.3.3 Custom accumulate functions
-
在之前我們介紹了 accumulate條件元素以及它的不同的用法。accumulate條件元素的結構由一個模式部分和一個或多個accumulate函數組成。在前一章中,我們看到了兩種不同類型的累積函數:內聯累積函數和內置的累積函數。
-
內聯積累函數在創建規則中顯式地定義。這些函數分為以四個部分,我們已經在前面講了,即 init,action,reverse和result.另一方面,內置函數受Drools支持,開箱機用。這些函數包括sum,count,avg,collectList,等等。
-
即使內聯累積函數是增強 Drools功能的強大且靈活的方法,它們的定義和可維護性也相當復雜。內聯累積函數在編寫、調試和維護上都很麻煩。內聯累積函數基本上是嵌入在DRL中的Java/MVEL基本代碼塊。如果我們沒有實現一個簡單的函數,那么在每個部分中編寫代碼可能會非常混亂。更糟糕的是,在內聯累積函數中進行調試幾乎是不可能的。然而,內聯累積函數最糟糕的地方可能是它們不能被重用。如果在多個規則中需要相同的函數,那么必須在每個規則中重新定義它。由於所有這些不便,內聯累積函數的使用是不鼓勵的。Drools沒有定義嵌入在DRL中的累積函數,而是允許我們在Java中定義它們,然后將它們導入到我們的knowledge base中。將累積函數的定義與它在規則中的用法解耦,解決了我們之前提到的所有問題。
-
自定義積累函數是一個 Java類,它實現了Drools的 org.kie.api.runtime.rule.AccumulateFunction 接口。作為一個例子,讓我們實現一個自定義累積函數來從一組Order訂單里,檢索具有最大總數的項,如下:
-
public class BiggestOrderFunction implements AccumulateFunction{
-
-
public static class Context implements Externalizable{
-
public Order maxOrder = null;
-
public double maxTotal = -Double.MAX_VALUE;
-
-
public Context() {}
-
-
-
public void readExternal(ObjectInput in) throws IOException,
-
ClassNotFoundException {
-
maxOrder = (Order) in.readObject();
-
maxTotal = in.readDouble();
-
}
-
-
-
public void writeExternal(ObjectOutput out) throws IOException {
-
out.writeObject(maxOrder);
-
out.writeDouble(maxTotal);
-
}
-
-
}
-
-
-
public Serializable createContext() {
-
return new Context();
-
}
-
-
-
public void init(Serializable context) throws Exception {
-
}
-
-
-
public void accumulate(Serializable context, Object value) {
-
Context c = (Context)context;
-
-
Order order = (Order) value;
-
double discount =
-
order.getDiscount() == null ? 0 : order.getDiscount()
-
.getPercentage();
-
double orderTotal = order.getTotal() - (order.getTotal() * discount);
-
-
if (orderTotal > c.maxTotal){
-
c.maxOrder = order;
-
c.maxTotal = orderTotal;
-
}
-
-
}
-
-
-
public boolean supportsReverse() {
-
return false;
-
}
-
-
-
public void reverse(Serializable context, Object value) throws Exception {
-
}
-
-
-
public Object getResult(Serializable context) throws Exception {
-
return ((Context)context).maxOrder;
-
}
-
-
-
public Class<?> getResultType() {
-
return Order.class;
-
}
-
-
-
public void writeExternal(ObjectOutput out) throws IOException {
-
}
-
-
-
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
-
}
-
-
}
-
首先本類實現的是 Drools的org.kie.api.runtime.rule.AccumulateFunction接口。這個接口定義了實現一個自定義accumulate函數所必須的方法。然而,在我們實現這個接口之前,我們需要定義一個上下文類。每次在Drools中使用累積函數時,都會為其創建一個單獨的上下文。這個上小文包括讓自定義accumulate函數正常運行的鎖必須的信息。在本例子中,我們定義了餓一個的靜態的Context類,它包含Order實例和一個double類型的maxTotal屬性。這個上下文將跟蹤到目前為止發現的最大的Order類。
-
一旦我們定義了我們的上下文,我們就可以實現 AccumulateFunction接口了。除了createContext()之外,這些方法的名稱和它們的語義與內聯累積函數的不同部分的名稱密切相關,如下所示:
- 1
- 2
可以使用冒號來定義表格的對齊方式,如下:
方法 | 介紹 |
---|---|
createContext | 這個方法是在第一次使用accumulate函數時創建的。這個方法的目的是創建用於這個特定的accumulate函數實例的上下文。在我們的示例中,它正在創建我們的Context類的新實例。 |
init | 這個方法也會在規則中第一次調用累積函數。該方法的參數是createContext()中創建的上下文 |
accumulate | 這就是真正的積累邏輯發生的地方。在我們的例子中,我們正在檢測當前處理的Order是否大於上下文所持有的訂單。如果是,上下文就相應地更新。此方法對應於內聯累積函數的操作部分。 |
supportsReverse | 該方法表明此累計函數是否支持逆操作。在我們的例子中,我們不支持它(否則,我們需要在上下文中保存所有分析的訂單的集合)。 |
reverse | 這個方法包含了一個邏輯,當一個事實先前與累積條件元素的模式相匹配時,這個事實就不再存在了。在我們的例子中,由於我們不支持reverse操作,這個方法仍然是空的。 |
getResult | 該方法返回累積函數的實際結果。在我們的例子中,結果是包含在我們的上下文對象中的訂單實例。 |
getResultType | 他的方法告訴Drools這種累積函數的結果類型。在我們的例子中,類型是order.class |
在我們的自定義accumulate函數可以在我們的規則中使用之前,我們需要將它導入進knowledge的包。自定義累積函數可以以以下方式導入到DRL資產中:
-
import accumulate org.drools.devguide.chapter05.acc.
-
BiggestOrderFunction biggestOrder
-
導入語句以 import accumulate關鍵字開始,接下來的是類的完全限定名,以及函數的實現。import語句的最后一部分是我們想要在DRL中賦予這個函數的名稱。
-
一旦函數被導入,我們就可以將其用作任何 Drools內置的累積函數
-
rule "Find Biggest Order"
-
when
-
$bigO: Order() from accumulate (
-
$o: Order(),
-
biggestOrder($o)
-
)
-
then
-
biggestOrder.setObject($bigO);
-
end
-
1.理解KieSession會話
1.1 無狀態和有狀態的KieSession會話
-
我們早已經知道,KieSession有兩種不同的形式:stateless(無狀態)和stateful(有狀態).我們所涵蓋的大部分例子只涉及有狀態的KieSession;這是一個很好的理由,即有狀態的KieSession是到目前為止是Drools支持的最強大的會話類型。
-
在我們決定要使用哪種類型的會話之前,我們需要了解這兩種會話類型之間的區別和相似之處。為了這樣做,我們將從最簡單的會話開始: 無狀態KieSession.
1.1.1 無狀態會話stateless KieSession
-
從開發的角度來看,我們希望在特定場景中使用的會話類型不是由規則決定的 ---或者我們想使用的其他資產類型。session會話的類型僅僅由我們在kmoudle.xml中如何定義的來決定,或者是當我們以編程的方式在代碼中實例化它。在大多數情況下,同一組資產(.drl文件,決策表,等等)可以在無狀態或有狀態會話中執行。
-
那么什么是無狀態的 KieSession呢??最好的比喻就是,無狀態的KieSession會將這種會話描述為一個函數,我們知道函數式無狀態以及原子的。
-
通常,函數是接收一組預先定義的參數,處理它們,並生成輸出或結果。在許多編程語言中,函數的結果可以是返回值本身,也可以是一些輸入參數的修改。理想情況下,函數不應該具有任何間接影響,這意味着如果在同一組參數中多次調用該函數,結果應該是相同的。
-
無狀態的 KieSession與我們描述的函數有一些相同的概念:它有一些定義松散的輸入參數集,它以某種方式處理這些參數,並生成響應。就像函數一樣,在相同的無狀態KieSession中不同的調用不會相互干擾。
-
為了得到一個無狀態的 Kie Session,我們首先需要定義我們想要用的KieBase來實例化它。一種方式使通過kmoudle.xml:
-
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-
xmlns="http://jboss.org/kie/6.0.0/kmodule">
-
<kbase name="KBase1">
-
<ksession name="KSession1" type="stateless" default="true/">
-
</kbase>
-
上面代碼中,我們將名為KSession1的KieSession定義為了一個無狀態的會話.
-
PS:如果不指定的話,默認就是以有狀態的會話來生成
-
那么接下來就是實例化這個名為 Ksession1的會話,我們可以使用下面的代碼摘要來完成:
-
KieServices kieServices = KieServices.Factory. get();
-
KieContainer kContainer = kieServices.getKieClasspathContainer();
-
StatelessKieSession statelessKsession = kContainer. newStatelessKieSess
-
ion("KSession1");
暴露API的StatelessKieSession類是與KieSession對等的,我們通常與有狀態會話進行交互的方式是:我們將一組事實數據插入其中,執行任何激活的規則,然后提取我們正在尋找的響應。在有狀態的會話中,插入和執行分為兩種不同的方法:insert()和 fireAllRules() 。在無狀態會話的情況下,這兩個方法結合成一個單獨方法:execute().而這個execute()有下面三個重載版本:
-
execute(Object fact)
-
execute(Iterable facts)
-
execute(Command command)
-
前兩個版本的 execute(...)是將我們傳遞給引擎的事實數據當做參數傳遞進方法,觸發所有激活的規則,並安排我們之前創建的會話。連續的調用者三個方法,只會重復的做着三步。請記住,在esxecute()結束后,任何與先前調用相關的內容都將被丟棄。
-
第三個版本你的 execute()允許我們去通過一個命令模式與會話進行交互.Drools已經有了一組預定義的可用命令,比如:InsertObjectCommand , SetGlobalCommand , FireAllRulesCommand ,等等。雖有的可用命令都可以使用CommmandFactory來創建。使用BatchExecutionCommand接口,可以將命令可以組合在一起。
-
一個無狀態的KieSession的典型用法如下代碼所示:
-
List<Command> cmds = new ArrayList<>();
-
cmds. add( CommandFactory.newSetGlobal( "list1", new ArrayList(), true
-
) );
-
cmds. add( CommandFactory.newInsert( new CustomerBuilder().withId(1L).
-
build(), "customer1" ) );
-
cmds. add( CommandFactory.newQuery( "Get Customers" "getCustomers" );
-
ExecutionResults results = ksession.execute( CommandFactory.
-
newBatchExecution( cmds ) );
-
results.getValue( "list1" ); // returns the ArrayList
-
results.getValue( "customer1" ); // returns the inserted Customer fact
-
results.getValue( "Get Customers" );// returns the query as a QueryResults instance
-
如果無狀態會話提供有狀態對等的操作的子集,那么我們為什么需要它們呢?從技術上講,我們不是。任何可以通過無狀態的KieSession會話完成的事情,都可以用有狀態的方法完成。使用無狀態會話更像是一個顯式的語句,它表示我們只是希望使用一個一次性的會話,它將用於一次性的評估。無狀態會話是對無狀態場景的理想選擇,例如數據驗證、計算(例如風險評估或抵押貸款利率)和數據過濾。
-
由於它的無狀態性,無狀態的Kie Session不需要處理。在每次調用了 execute()方法之后,執行的資源就會被釋放。 此時,如果需要的話,另一個執行輪的無狀態的Kie Session也已經准備好了。因此,每一個execute()的執行都將於前一個是獨立的,就是原子性~~
-
總結,無狀態的Kie Session是理想的無狀態評估,例如:
-
>數據驗證
-
>計算
-
>數據過濾
-
>消息路由
-
>可以描述為規則的任何復雜的函數或者表達式
1.1.2 有狀態的會話 KieSession
-
有狀態會話與無狀態會話最大區別就是,它可以在交互的同時,保持住狀態。此類場景的一個常見示例是我們正在使用的一個監視進程的會話。理想情況下,我們想越快越好的想我們所監控的進程中插入任何傳入事件,我們也希望盡快發現問題。在這些情況下,我們不能等到所有的事件都發生了(因為他們可能會永遠不會停下來),所以我們可以創建一個 BatchExecutionCommand對象,在無狀態會話中執行。更好的方法是在每個事件到達時就插入,執行一個激活的規則,獲取一個生成的結果,然后等待下一個事件到達。當下一次事件到來時,我們不想在一個新的會話中處理它,我們希望使用與之前的事件相同的會話。這就是有狀態的會話。
-
當我們不想再使用 KieSession的時候,那么我們必須顯示的調用dispose()方法來聲明下。這個方法釋放會話可能獲得的任何資源,並釋放它可能分配的任何內存。dispose()方法調用后,,會話保持在一個無效的狀態,如果進一步與這個session會話進行交互,就會得到一個java.lang.IllegalS,tateException異常。
-
就像 StatelessKieSession,KieSession接口也支持以它的execute()方法的命令模式。當我們處理持久化會話時,這個命令模式的交互是相關的。在這種情景下,所有的傳遞給一個單獨的execute()的命令都在一個單獨的事務中被執行。持久化的更多的知識,后續會將.
-
現在我們已經對 Drools提供的不同類型的會話有了更好的理解,讓我們來看看一些高級的配置選項。
1.2 運行時組件(Kie runtime components)
-
Drools為我們提供了幾個會話的配置選項 ---無論它們是無狀態的還是有狀態的。在本節中,我們將介紹一些選項,以使我們能夠充分利用Drools的潛力,從而配置我們的會話。
-
我們與Drools的session會話交互的最普通的方式就是通過插入/修改/收回實數數據,並執行任何可能由於這些操作而發生的規則激活。所有這些操作都針對規則引擎的不同方面 ---例如知識的斷言和推理---但是也有一些其他的方法可以與會話交互,這些會話可以用來提供或從中提取信息。這些操作更面向Drools運行的應用程序,而不是規則引擎本身。我們將在本節中討論的選項是全局、通道、查詢和事件監聽器。
-
PS:盡管上面的四個對於有無狀態的會話都可用,但是我們這里只討論有狀態的會話。
1.2.1 Globals
-
我們之前提過全局變量。
-
即使全局變量可以在會話內部使用,也不會暴露在外部世界中,它們通常被用作從會話中引入/提取信息的一種方式。在許多情況下,全局是會話和外部消息之間的聯系點。
-
當使用有狀態會話時,在KieSession類中有三種方法與全局變量相關。這些方法如下表所示:
-
方法 描述 void setGlobal(String identifier,Object value); 該方法用於設置全局值。 在同一個會話中多次調用此方法將更新任何先前設置的全局值。用於調用此方法的標識符,必須與我們在Knowledge Base(我覺得其實想說的是規則文件)中配置的標識符(的名字)相匹配 Globals getGlobals() 這個方法用於在一個會話中檢索出所有的全局變量。所得到的對象可用於通過標識符來檢索單個全局變量。 Object getGlobal(String identifier); 該方法用於檢索全局變量的標識符。
正如您所看到的,在會話中如何與全局變量進行交互並沒有太多的知識。前面表中描述的三種方法幾乎是自解釋的。
在Drools中使用一個全局的常用方法有四種,如下所示:
在規則的LHS,作為參數化模式條件的一種方法
在規則的LHS,作為一種在會話中引入新信息的方法
在規則的RHS,作為一種從會話中收集信息的方法
在規則的RHS,作為一種與外部系統交互的方式
無論在會話中如何使用全局,重要的是要注意到,全球變量並非事實數據,也就是說它並不會去觸發規則。Drools將以一種完全不同的方式對待全局變量和事實數據;全局變量的變化永遠不會被Drools發現,因此,Drools永遠不會對它們做出反應。
讓我們分析一下我們之前列出的一個全局的四種常見場景。
1.2.1.1 全局變量是參數化模式條件的一種方法
-
通常在Drools中使用全局變量的一種方法是作為一種外部參數來表示規則的條件。其思想是在規則的條件下使用全局變量而不是硬編碼的值。
-
作為一個例子,讓我們回到我們的eShop示例。假設我們想要一個Drools會話來檢測我們的eShop應用程序中客戶的可疑操作。我們將定義一個可疑的操作,作為一個客戶,在等待操作的總金額超過1萬美元。
-
我們的會話的輸入將是我們的應用程序的客戶以及客戶的訂單。對於每一個等待訂單超過1萬美元的客戶,我們將插入一個新的 SuspiciousOperation類型的對象。 SuspiciousOperation 類的結構如下:
-
public class SuspiciousOperation {
-
public static enum Type {
-
SUSPICIOUS_AMOUNT,
-
SUSPICIOUS_FREQUENCY;
-
}
-
private Customer customer;
-
private Type type;
-
private Date date;
-
private String comment;
-
public SuspiciousOperation(Customer customer, Type type) {
-
this.customer = customer;
-
this.type = type;
-
}
-
//setters and getters
-
}
以下規則足以完成檢測是否可疑操作的目標:
- 1
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > 10000.0 ) from accumulate (
-
Order ( customer == $c, state != OrderState.COMPLETED, $total:
-
total),
-
sum($total)
-
)
-
then
-
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.
-
SUSPICIOUS_AMOUNT));
-
end
-
規則很簡單:對於每一個消費者客戶,收集任何OrderState不是COMPLETED 狀態的訂單 ,並計算其總數的總和。如果這個總數大於10000,那么規則就會被激活。當規則的RHS執行的時候,它將會插入一個新的 SuspiciousOperation類型對象進會話內。
-
我們早已知道,如果我們想要去執行規則,我們需要將其作為我們的配置文件中Knowledge Base(我覺得想說的是規則文件)中的一部分,從它里面創建會話,並提供一些事實數據給他。就像下面這樣:
-
// Create a customer with PENDING orders for a value > 10000
-
Customer customer1 = new CustomerBuilder()
-
.withId( 1L).build();
-
Order customer1Order = ModelFactory.getPendingOrderWithTotalValueGreaterThan10000(customer1);
-
// Create a customer with PENDING orders for a value < 10000
-
Customer customer2 = new CustomerBuilder()
-
.withId( 2L).build();
-
Order customer2Order = ModelFactory.getPendingOrderWithTotalValueL
-
essThan10000(customer1);
-
// insert the customers in a session and fire all the rules
-
ksession.insert(customer1);
-
ksession.insert(customer1Order);
-
ksession.insert(customer2);
-
ksession.insert(customer2Order);
-
ksession.fireAllRules();
-
前面代碼的一個運行示例可以在chap-05moudle下的代碼包中找到。
-
前面的示例工作的很好,只要我們認為可疑操作的閾值保持不變。但是,如果我們想要創建這個閾值變量呢?
-
實現這一目標的多種方法的一種方法是用全局變量替換我們規則中的硬編碼值,這個變量可以在我們想運行會話時定義,就像下面這樣:
-
global Double amountThreshold;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( customer == $c, state != OrderState.COMPLETED, $total: total),
-
sum($total)
-
)
-
then
-
insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
在前面的例子中,我們可以看到硬編碼的閾值在DRL中不再存在。我們現在再我們的規則中使用一個全局的 Double變量。通過使用這種方法,我們認為是一個可疑的操作的判斷條件就可以通過不同的會話來控制了。
-
PS:在我們的會話執行過程中,沒有任何東西可以阻止我們修改全局變量。即使這是可能的,也不鼓勵修改在運行時在約束中使用的全局值。鑒於Drools的聲明性特性,我們無法預測在這些情況下修改全局變量的值會帶來什么影響。
-
需要指出的一件重要的事情是,當全局變量作為規則約束的一部分使用時,全局變量必須在使用它的模式被評估之前進行設置。為了避免競態條件,在插入任何事實數據之前,設置會話的全局變量被認為是一種良好的實踐。在規則約束中使用全局變量的一個缺點是,它們的值不會被Drools緩存。每當一個全局變量需要被評估時,它的值就被訪問。當Knowledge Base(我覺得想表達式我們的規則文件)很大的時候,這可能會促使出現性能問題。
-
PS:考慮到使用globals對我們的規則參數化的所有缺點,不推薦模式。一個更好的方法是參數化我們的規則的條件,那就是將參數作為事實在我們的會話中,並將它們作為任何其他類型的事實對待。在本書中包含這種模式只是為了完整性。
1.2.1.2 global作為一種在LHS上將新信息引入到會話的方法
-
另一個與glaobal相關的常見模式是作為會話的數據源使用。通常,這種類型的全局封裝了將新對象引入會話的服務(數據庫,內存映射,web service,等等)的調用。這種使用模式通常涉及到 from條件元素。
-
為了演示此場景,我們將修改前一節中介紹的示例,並引入一個服務調用來檢索客戶的訂單。該服務將被建模為OrderService接口,包含一個單一的方法-getOrdersByCustomer-如下代碼所示:
-
public interface OrderService {
-
public Collection<Order> getOrdersByCustomer(String customerId);
-
}
這里的想法是使用這個接口作為全局的,我們的規則可以用來檢索與客戶相關的所有訂單。本示例的DRL的最終版本將與下面的代碼類似:
-
global Double amountThreshold;
-
global OrderService orderService;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
-
sum($total)
-
)
-
then
-
insert( new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
在這個版本的示例中,我們仍然使用全局來保持我們認為可疑操作的閾值,但是現在我們也有了一個新的全局變量orderService。我們的規則是調用全局的getOrdersByCustomer方法來獲取特定客戶的所有訂單,而不是從客戶的orders屬性獲取訂單。
-
在這個簡單的例子中,我們可能沒有意識到這種方法的優點 ---客戶的訂單現在只在需要時才被提取。在之前的版本的規則中,我們必須預先為所有客戶預取所有的訂單,然后將它們插入會話中。在插入的時候,我們不知道session是否真的需要所有客戶的訂單。
-
如前所述,在將任何客戶插入會話之前,我們需要記住設置orderService全局的值,如下所述:
-
OrderService orderServiceImpl = new OrderServiceImpl();
-
//a concrete implementation of OrderService.
-
ksession.setGlobal( "orderService", orderServiceImpl);
-
ksession.insert(customer1);
-
ksession.insert(customer2);
-
ksession.fireAllRules();
-
在前面的代碼中要注意的一件重要的事情是,我們不再將訂單作為事實數據插入。這些命令將根據規則本身的要求進行檢索。不過,有一個問題,規則的條件可以在執行規則時多次被計算。每次對規則進行重新評估時,都會調用數據源。在使用這種模式時,必須考慮數據源的延遲。
-
我們了解了如何將 global作為外部系統的接口,以便檢索和引入(但不插入)新信息到會話中。現在的問題是如何從會話中提取生成的可疑操作對象?
1.2.1.3 全局變量作為一種從會話中收集信息的方式
-
前一個示例中的規則為每個可疑操作插入了一個 SuspiciousOperation對象。問題是這些事實數據不能從會話的外部獲得。從會話中提取信息的一個常見模式是使用全局變量。
-
此模式背后的思想是使用一個全局變量來收集我們想從會話中提取的信息。由於全局可以從會話外部訪問,所以它引用的任何事實、對象或值也可以訪問。這類局最通用的一類是 java.util.Collection或者java.util.Map 。
-
現在,我們將修改前一節中使用的規則文件,添加一個新規則,將任何可疑操作事實數據收集到一個全局集合中:
-
global Double amountThreshold;
-
global OrderService orderService;
-
global Set results;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
-
sum($total)
-
)
-
then
-
insert( new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
-
rule "Collect results"
-
when
-
$so: SuspiciousOperation()
-
then
-
results.add($so);
-
end
-
代碼顯示我們現在有一個新的叫做results的全局變量和一個新規則,這個新的規則它將收集任何 SuspiciousOperation類的實例。
-
執行這個例子的新版本的相關Java代碼如下所示:
-
Set<SuspiciousOperation> results = new HashSet<>();
-
ksession.setGlobal( "results", results);
-
ksession.insert(customer1);
-
ksession.insert(customer2);
-
ksession.fireAllRules();
-
//variable 'results' now holds all the generated SuspiciousOperation objects.
在執行規則之后,全局集將包含在會話執行期間生成的任何SuspiciousOperation對象的引用。然后,我們可以在創建它們的會話之外使用這些對象。
1.2.1.4全局變量是在RHS中與外部系統交互的一種方式
-
我們將要討論的關於全局的最后一個常見的使用模式是,在規則的RHS使用全局變量作為與外部系統交互的一種方式。這個模式背后的想法很簡單,我們看到我們可以使用 global變量將新信息引入到一個模式中(使用from條件元素)。我們還可以使用全局變量在RHS中來與外部系統進行交互。與此外部系統的交互可以是單向的(從系統獲取信息或向系統發送信息)或雙向(從系統發送和接收信息)。
-
繼續前面的例子,假設現在我們想要通知一個外部審計系統發現的每個SuspiciousOperation。這里有兩個選項,我們現在知道我們可以使用上一節介紹的全局集合來訪問這些生成的事實。我們可以從Java代碼中迭代這個列表,並將每個元素發送到審計系統。另一個選擇是在會話本身中利用它。
-
這個新接口將通過一個名為AuditService的接口來表示。該接口將定義一個單獨的方法-notifySuspiciousOperation——下面的代碼所示:
-
public interface AuditService {
-
public void notifySuspiciousOperation(SuspiciousOperation
-
operation);
-
}
我們需要添加這個接口的一個實例作為全局變量並創建一個新規則調用其notifySuspiciousOperation方法或修改Collect results規則,讓這個規則現在也調用notifySuspiciousOperation這個方法。讓我們采用第一個方法,添加一個新規則來通知審計系統:
-
global Double amountThreshold;
-
global OrderService orderService;
-
global AuditService auditService;
-
-
rule "Detect suspicious amount operations"
-
when
-
$c: Customer()
-
Number( doubleValue > amountThreshold ) from accumulate (
-
Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),
-
sum($total)
-
)
-
then
-
insert( new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
-
end
-
-
rule "Send Suspicious Operation to Audit Service"
-
when
-
$so: SuspiciousOperation()
-
then
-
auditService.notifySuspiciousOperation($so);
-
end
-
我們創建的新規則是使用我們定義的新全局來通知審計系統每個生成的SuspiciousOperation對象.重要的是要記住Drools總是在一個單獨線程中執行規則。理想情況下,我們的規則的RHS不應該涉及到阻塞操作。在需要阻塞操作的情況下,引入異步機制來在單獨的線程中執行阻塞操作,這在大多數情況下被認為是一個不錯的選擇。
-
在Drools中,我們已經討論了globals的四種常見用法模式,接下來討論:管道
1.2.2 管道
-
通道是一種標准化的方式,可以將數據從會話內部傳輸到外部世界。
-
一個通道可以完全用於我們在上一節討論的內容:全局變量作為與RHS外部系統交互的一種方式。我們可以通過使用一個通道來完成相同的任務,而不是使用全局。
-
從技術上講,通道是一個帶有單一方法的Java接口---- void send(Object object),如下:
-
public interface Channel {
-
void send(Object object);
-
}
通道只能在我們的規則的RHS中使用,作為將數據發送到會話外部的一種方式。在使用通道之前,我們需要在會話中注冊它。“KieSession”類提供了以下三種處理管道的方法:
- 1
方法 | 描述 |
---|---|
void registerChannel(String name,Channel channel | 這個方法用於將管道注冊給會話。當一個管道被注冊了,必須要提供一個名字,這個名字被會話用來作為管道的標識 |
void unregisterChannel(String name) | 這個方法與registerChannel相對,被用於從會話中刪除某個已經注冊的管道。傳遞的name屬性是用來定位待刪除管道的 |
Map< String, Channel> getChannels() | 這個方法被用於檢出任何以前注冊的管道。返回的Map的鍵標識的是在注冊管道的時候所指定改的名字 |
在規則的RHS中,無論何時我們想要與通道進行交互,我們都可以通過預定義的channels RHS變量獲得對它的引用。這個變量提供了一個類似於映射的接口,允許我們通過它的名稱引用特定的通道。例如,如果我們已經注冊了一個notifications名稱的通道,我們可以使用以下代碼片段在我們的規則的RHS中與之交互:
channels["notifications"].send(new Object());
- 1
-
通道接口的具體實現可用於將數據路由到外部系統,通知事件,等等。請記住,通道代表一種單向傳輸數據的方式:send()方法在Channel接口中的返回值是 void。
-
讓我們從上一節重構示例,以利用一個通道而不是全局變量來通知審計系統關於可疑操作的情況。
-
我們需要做的第一件事就是去掉我們在規則中所擁有的auditService變量。本例的要點是用一個通道來替換這個全局變量。然后,我們需要從“Send Suspicious Operation to Audit Service”規則里替換RHS,因此,它使用的是一個管道而不是舊的全局變量:
-
rule "Send Suspicious Operation to Audit Channel"
-
when
-
$so: SuspiciousOperation()
-
then
-
channels[ "audit-channel"].send($so);
-
end
現在,在我們根據這個KieBase所依賴的規則文件,執行一個會話之前,我們需要注冊管道給這個會話,管道名字叫“ audit-channel”。為了這樣做,我們使用了我們已經討論過的registerChannel方法,如下:
- 1
-
Channel auditServiceChannel = new Channel(){
-
-
@ Override
-
public void send(Object object) {
-
//notify AuditService here. For testing purposes, we are just
-
//going to store the received object in a Set.
-
results. add((SuspiciousOperation) object);
-
}
-
-
};
-
ksession.registerChannel( "audit-channel", auditChannel);
-
正如我們所看到的,一個通道提供了一個比全局變量更嚴格、但定義良好的契約。就像使用全局變量一樣,我們可以使用通道的不同實現來在規則中提供不同的運行時行為。
-
通道的優點之一是它們提供的多功能性,因為它們使用一個字符串的鍵進行索引。就像使用全局變量一樣,我們可以使用通道的不同實現來在規則中提供不同的運行時行為。通道的鍵可以在運行時,在 LHS中作為綁定或在規則的RHS中來確定。
-
讓我們以更靈活的方式從一個會話中提取信息 :QUERY
1.2.3 Queries
-
在Drools中,一個查詢可以被看作是一個沒有RHS的常規規則。Query和Rule之間的主要區別是前者可能會接受參數。使用查詢,我們可以使用Drools模式匹配語法的所有功能從會話中提取信息。在Runtime時,我們可以執行查詢,並根據結果執行任何操作。在某種程度上,查詢是一個具有動態RHS的規則。
-
PS:查詢也可以作為規則內的常規模式使用。這是Drools逆向鏈接推理能力的基礎。本節只關注查詢作為從會話中提取信息的一種方法。
-
繼續使用我們之前使用的示例,現在讓我們創建一個查詢來從會話中提取所有生成的可疑操作事實。執行此操作所需的Query與下面的查詢相似:
-
query "Get All Suspicious Operations"
-
$so: SuspiciousOperation()
-
end
正如我們所看到的,我們創建的查詢看起來就像一個沒有它RHS的規則。如果我們對某個特定的客戶感興趣,那么我們可以定義另一個查詢,它以用戶ID作為參數,並過濾掉所有相關的SuspiciousOperation對象。
-
query "Get Customer Suspicious Operations" (String $customerId)
-
$so: SuspiciousOperation(customer.customerId == $customerId)
-
end
-
查詢的參數定義類似於Java類中的方法的參數:每個參數都有一個類型和一個名稱
-
從會話外部執行查詢有兩種方法:按需查詢和實時查詢。讓我們更詳細地分析它.
1.2.3.1 按需查詢
按需評估查詢通過調用 KieSession's getQueryResults方法:
public QueryResults getQueryResults(String query, Object... arguments);
該方法接受Query的名稱和它的參數列表(如果有的話)。參數的順序對應於查詢定義中的參數的順序。這個方法的結果是一個QueryResults對象:
-
public interface QueryResults extends Iterable<QueryResultsRow> {
-
String[] getIdentifiers();
-
Iterator<QueryResultsRow> iterator();
-
int size();
-
}
-
QueryResults接口擴展了Iterable,並代表了QueryResultsRow對象的集合.getidentifier()方法返回查詢標識符的數組。查詢中定義的任何綁定變量都將成為其結果中的標識符。比方說,我們的“ Get All Suspicious Operations” 這個Query只定義了一個標識符:$so.當執行查詢時,標識符用於檢索綁定變量的具體值。
-
下面的代碼可以用於執行 " Get All Suspicious Operations"這個Query:
-
QueryResults queryResults = ksession.getQueryResults( "Get All
-
Suspicious Operations");
-
for (QueryResultsRow queryResult : queryResults) {
-
SuspiciousOperation so = (SuspiciousOperation) queryResult.get( "$so");
-
//do whatever we want with so
-
//...
-
}
前面的代碼執行" Get All Suspicious Operations "查詢,然后遍歷結果提取$ so標識符的值,在這種情況下, 即SuspiciousOperation類的實例。
1.2.3.2 實時查詢
-
當我們想要在特定時間點執行特定的查詢時,就會使用按需查詢。Drools還提供了執行查詢的另一種方式,它允許我們將偵聽器附加到查詢中,以便在它們可用時,我們可以得到有關結果的通知。
-
實時查詢使用下面的KieSession方法執行:
-
public LiveQuery openLiveQuery( String query,Object[] arguments,
-
ViewChangedEventListener listener);
-
與按需查詢一樣,我們需要傳遞給這個方法的第一個參數是我們想要附加一個偵聽器的Query的名稱。第二個參數是查詢所期望接收的參數數組。第三個參數是我們想要連接到查詢的實際偵聽器。方法返回值是:LiveQuery的實例。
-
讓我們仔細看一下ViewChangedEventListener接口:
-
public interface ViewChangedEventListener {
-
public void rowInserted(Row row);
-
public void rowDeleted(Row row);
-
public void rowUpdated(Row row);
-
}
-
正如我們所看到的,ViewChangedEventListener接口不僅用於接收與指定Query匹配的新事實數據,而且還可以檢測這些事實數據的修改或撤銷。Drools引擎將在一個事實數據與指定的查詢匹配時通知這個偵聽器,當一個先前匹配的事實數據被修改,或者先前匹配的事實數據的修改將它從查詢的過濾器中排除。
-
在上一節中,我們了解了如何使用全局變量將結果、操作和一般規則執行信息傳遞給外部世界。然而,如果我們想要在不修改現有規則的情況下,以通用的方法來實現這一目標呢?為此,我們還有其他機制,比如事件監聽器。
1.2.4 Event Listeners事件監聽器
-
Drools框架為用戶提供了一種將事件監聽器連接到兩個主要組件:Kie Bases和Kie Bases的機制。
-
Kie Base中的事件與它所包含的包的結構有關。使用org.kie.api. event.kiebase.KieBaseEventListener,例如,我們可以在從KieBase中添加或刪除一個包之前或之后得到通知。使用同樣的監聽器,我們可以更深入地了解在一個KieBase中正在修改的內容,比如單獨的規則、函數和被刪除/刪除的流程。
-
可以使用 KieBase public void addEventListener(KieBaseEventListener listener)方法將KieBaseEventListener連接到KieBase。一個KieBase可以有零個,一個,甚至多個監聽器綁定給它。當一個特殊的事件被觸發了,KieBase將會順序執行在每個以前注冊的事件監聽器中的相應的方法。偵聽器的執行順序並不一定與它們注冊的順序相對應。
-
Kie會話的事件的事件與Drools的運行時執行有關。一個可以被Kie會話觸發的的事件可以分為三個不同的類別:規則執行運行時( org.kie.api.event.rule.RuleRuntimeEventListener),agenda-related事件( org.kie.api.event.rule.AgendaEventListener)以及流程執行運行時( org.kie.api.event.process.ProcessEventListener ).
-
所有這三種類型的事件監聽器都可以使用KieSession addEventListener方法連接到一個Kie會話中:
-
-
public void addEventListener(RuleRuntimeEventListener listener)
-
public void addEventListener(AgendaEventListener listener)
-
public void addEventListener(ProcessEventListener listener)
-
一個RuleRuntimeEventListener可以被用於與在KieSession中事實對象的狀態有關的事件的通知。KieSession中的事實數據,在它們插入,修改或者從會話中檢出的時候,它的狀態會改變。這種類型的偵聽器被用於對會話執行情況的統計分析或報告。
-
AgendaEventListener是一個接口,我們可以用它去通知在Drools的agenda組內發生的事件。Agenda事件與被創造、被取消或被觸發的規則相關聯;agenda組從激活的的agenda棧中被推或者彈出,或是rule-flow組被激活和失活。AgendaEventListeners是一個審計工具的基礎的助手方法。例如,能夠知道規則何時被激活,在分析一個Kie會話的執行時是一個很有價值的信息。
-
ProcessEventListeners與jBPM事件,允許我們在當流程實例啟動或完成時,或在流程實例中的單個節點被觸發之前/觸發之后得到通知。
-
一種更聲明性的方式來配置我們想在會話內使用的事件監聽器,那就是將他們定義在kmoudle.xml中,作為 <ksession>組件的一部分。
-
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-
xmlns="http://jboss.org/kie/6.0.0/kmodule">
-
<kbase name="KBase1" default="true" packages="org.domain">
-
<ksession name="ksession1" type="stateful">
-
<ruleRuntimeEventListener type="org.domain.
-
RuleRuntimeListener"/>
-
<agendaEventListener type="org.domain.FirstAgendaListener"/>
-
<processEventListener type="org.domain.ProcessListener"/>
-
</ksession>
-
</kbase>
-
</kmodule>
Drools中的所有事件偵聽器都在Drools框架運行的同一線程中執行。這種行為有兩個含義:事件偵聽器應該是輕量級和快速的,它們不應該拋出異常。或者甚至是最糟糕的阻塞任務。執行繁重的處理任務的事件偵聽器--或者更糟的是,阻塞任務---在可以的情況下,這都是需要杜絕的。當事件被觸發時,Drools的執行將不會繼續,直到所有注冊的偵聽器都完成了。Drools中可能觸發事件的操作的執行時間是任務本身的執行時間和被觸發的每個事件偵聽器的執行時間。Drools當前的實現不僅在它正在運行的同一線程中執行事件監聽器,而且當事件偵聽器被觸發時也不采取任何預防措施。拋出異常的事件偵聽器將破壞執行的底層操作的執行。如果我們不想在偵聽器中出現問題時干擾Drools執行,那么在事件偵聽器中捕獲任何可能的異常是必須的。在本章的代碼包中,有一些單元測試顯示了偵聽器是如何注冊的,並在Drools中使用。我們強烈建議讀者查看這些測試並運行、調試,甚至增強它們,以便更好地理解Drools的事件偵聽器功能
- 1
1.3 Kie Base組件
-
我們已經介紹了很多knowledge base的組件,比如說:規則,全局變量,查詢,管道。現在我們討論更高級的主題了,這將使我們能夠創建更簡明和可重用的knowledge。
-
在本節中,我們將討論諸如函數、自定義操作符,和自定義函數積累等主題。所有這些組件都可以用於以一種更簡單而有力的方式建模我們的知識。
1.3.1 函數
-
到目前為止,我們已經討論了Drools中最常見的三個最常見的知識聲明:規則、查詢和聲明類型。還有另一種知識聲明可以用於在knowledge Base中表示無狀態邏輯:函數。Drools中的函數基本上是獨立的代碼,可以隨意地獲取參數,也可能不返回值。函數對於我們希望在knowlwdge Base中定義邏輯而不是在外部Java類中定義邏輯的情況非常有用。
-
定義一個函數的語法阿玉在java中定義一個方法是差不多的,不過要在前面加一個function關鍵字。函數有返回值類型(他可以是任何的java類型或者是 void),有函數名字以及函數的參數,如下:
-
function String formatCustomer(Customer c){
-
return String.format(
-
"[%s] %s", c.getCategory(), c.getName());
-
}
-
在前面的例子,有一個叫formatCustomer的函數被定義了。這個函數的參數是一個Customer的實例,返回值是一個字符串。函數體使用一個java的格式化表達式;在本例子中,它使用了 String.format()方法來將分類和提供商的名字連接在一起。
-
就像聲明的類型一樣,定義在knowledge Base的函數是一個把邏輯放在一個地方的好方式。Drools中的函數也為我們提供了修改它們的靈活性,而無需重新編譯任何代碼。
-
PS:前面所說的不需要重新編譯任何代碼不是 100%正確的。
-
即使Drools中函數的使用給了我們一定程度的靈活性,它們也有一些限制,如下所列:
-
>函數不能在它被定義所在的包的外部使用。這樣為着定義在DRL文件內的函數不能背用於任何的其他的DRL文件,除非兩者都有相同的包聲明。
-
>函數並不能使用任何的全局變量,事實數據或者預設變數(比如kxontext)。函數的上下文只是在調用時傳遞來的參數集。
-
>由於之前的限制,函數不能在會話中插入、修改或撤消事實
-
當考慮到可重用性和可維護性時,在knowlesge Base中聲明函數並不是最好的方式。幸運的是,Drools允許我們從Java類中導入靜態方法作為函數,並在我們的規則中使用它。為了導入靜態方法,我們需要使用 function關鍵字與import關鍵字相結合。
-
import function org.drools.devguide.chapter05.utils.CustomerUtils.
-
formatCustomer;
-
正如您所看到的,從某種意義上來說,導入一個類的靜態方法就像在Java中導入靜態方法一樣。
-
不管我們的函數是從Java類導入還是在knowledge Base中聲明,我們在規則中調用它們或從另一個函數中調用它們的方法是簡單地使用它的名稱,如下代碼所示:
-
rule "Prepare Customers List"
-
when
-
$c: Customer()
-
then
-
globalList.add(formatCustomer($c));
-
end
前面的例子顯示了在規則的RHS使用了 formatCustomer函數,但是函數也可以在規則的條件部分中使用,如下所示
- 1
-
rule "Prepare Customers List"
-
when
-
$c: Customer($formatted: formatCustomer($c))
-
then
-
...
-
end
現在讓我們轉到Drools的另一個功能強大的特性,它允許我們使用專門的操作符來增強DRL語言,這些操作符可以用來創建更簡明、可讀和可維護的規則:自定義操作符。
1.3.2 自定義操作符
-
在指定我們的LHS的時候,我們可以使用許多的操作符,比如:== , != , < , >,這些都是Drools所支持的。不過,在某些情況下,可用的操作符還不夠。涉及比較復雜的邏輯,外部服務或語義推理,是Drools的能力不足的好例子。然而,沒有什么可擔心的;Drools提供了創建自定義操作符的機制,在編寫規則時可以使用這些操作符。
-
在Drools中,自定義操作符通過實現org.drools.core. base.evaluators.EvaluatorDefinition 就接口,定義為一個java類。這個接口只表示操作符的定義。具體的實現被委派給了 org.drools.core.spi.Evaluator的一個實現。
-
-
在自定義操作符作為規則的一部分之前,他必須在被使用的knowledge base中注冊。自定義操作符的注冊是在類路徑中使用配置文件或在kmodule.xml中指定的它。然而,在我們繼續討論自定義操作符是如何注冊的之前,我們先看個例子
-
為了澄清一個自定義操作符是什么以及它是如何定義的,讓我們使用我們的eShop用例中的一個例子.對於本例,我們將實現一個簡單的操作符,它將告訴我們一個Order函數是否包含給定其ID的Item。這個示例可能不是定制操作符最有趣的示例,因為它可以用許多不同的方式進行解析。盡管如此,它還是一個很好的例子,展示了如何構建自定義操作符。
-
我們新的自定義操作符的概念是能夠將規則寫入如下:
-
rule "Apply discount to Orders with item 123"
-
when
-
$o: Order( this containsItem 123)
-
Then
-
modify ($o){ setDiscount(new Discount( 0.1))};
-
end
-
在前面的規則中要注意的重要事項是使用自定義操作符,名字是: containsItem。所有的自定義操作符---和拓展的,任何在Drools中的操作符---都是有兩個參數。在這個特殊的情況下,第一個參數是Order類型,第二個參數是Long類型。操作符總是會對布爾值進行評估。在這種情況下,布爾結果將指示指定的項是否存在於所提供的Order中。
-
為了實現我們的自定義操作符,我們需要做的第一件事就是實現 org.drools.core.base.evaluators.EvaluatorDefinition。在我們的樣例中,我們的實現類叫做ContainsItemEvaluatorDefinition:
-
public class ContainsItemEvaluatorDefinition implements EvaluatorDefinition {
-
-
protected static final String containsItemOp = "containsItem";
-
-
public static Operator CONTAINS_ITEM;
-
public static Operator NOT_CONTAINS_ITEM;
-
-
private static String[] SUPPORTED_IDS;
-
-
private ContainsItemEvaluator evaluator;
-
private ContainsItemEvaluator negatedEvaluator;
-
-
static {
-
if (SUPPORTED_IDS == null) {
-
CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, false);
-
NOT_CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, true);
-
SUPPORTED_IDS = new String[]{containsItemOp};
-
}
-
}
-
-
-
public String[] getEvaluatorIds() {
-
return new String[]{containsItemOp};
-
}
-
-
-
public boolean isNegatable() {
-
return true;
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, Operator operator) {
-
return this.getEvaluator(type, operator.getOperatorString(),
-
operator.isNegated(), null);
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, Operator operator,
-
String parameterText) {
-
return this.getEvaluator(type, operator.getOperatorString(),
-
operator.isNegated(), parameterText);
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, String operatorId,
-
boolean isNegated, String parameterText) {
-
return getEvaluator(type, operatorId, isNegated, parameterText,
-
Target.BOTH, Target.BOTH);
-
}
-
-
-
public Evaluator getEvaluator(ValueType type, String operatorId,
-
boolean isNegated, String parameterText, Target leftTarget,
-
Target rightTarget) {
-
return isNegated ?
-
negatedEvaluator == null ?
-
new ContainsItemEvaluator(type, isNegated) : negatedEvaluator
-
: evaluator == null ?
-
new ContainsItemEvaluator(type, isNegated) : evaluator;
-
}
-
-
-
public boolean supportsType(ValueType type) {
-
return true;
-
}
-
-
-
public Target getTarget() {
-
return Target.BOTH;
-
}
-
-
-
public void writeExternal(ObjectOutput out) throws IOException {
-
out.writeObject(evaluator);
-
out.writeObject(negatedEvaluator);
-
}
-
-
-
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
-
evaluator = (ContainsItemEvaluator) in.readObject();
-
negatedEvaluator = (ContainsItemEvaluator) in.readObject();
-
}
-
-
}
上面的類中,我們看到有很多的信息需要被處理,所以我們先來看看。開始時的靜態塊將兩個新的操作符注冊到Drools的操作符注冊表中。這兩個新的操作符就是containsItem和與其相對的 not containsItem。下一個重要的方法是getEvaluatorsIds(),它告訴Drools所有可能的我們正在定義的操作符的id。緊跟着的方法是 isNegatable(),這個方法標識我們正在創建的操作符是否被否定。接着,是四個不同版本的getEvaluator()方法,這些方法將返回,在編譯時, org.drools.core.base.evaluators.EvaluatorDefinition 的具體的實例,以應該用於每個特定的場景。傳遞給這個方法方法的參數如下:
- 1
參數名 | 介紹 |
---|---|
type | 這是操作數的類型。 |
operatorId | 這是被解析的操作符的標識符ID.一個單獨的操作符定義可以處理多個ID |
isNegated | 這表明正在解析的操作符是否使用了not前綴(被否定)。 |
parameterText | Drools中的一個操作數可以固定在尖括號中定義的參數。具有參數的操作符的例子是Drools融合的CEP操作符,下一個章節會講 |
leftTarget/rightTarget | 這兩個參數指定這個操作符是根據事實數據、事實處理,還是兩者都操作。 |
getEvaluator()的四個版本都返回一個ContainsItemEvaluator 實例。 ContainsItemEvaluator是Drools的 org.drools.core.spi.Evaluator的具體實現類,並且是負責我們操作符運行時行為的類。這個類是我們運算符的真正邏輯—檢查一個Item是否被一個Order所包含:
-
public class ContainsItemEvaluator extends BaseEvaluator {
-
-
private final boolean isNegated;
-
-
public ContainsItemEvaluator(ValueType type, boolean isNegated) {
-
super(type ,
-
isNegated?
-
ContainsItemEvaluatorDefinition.NOT_CONTAINS_ITEM :
-
ContainsItemEvaluatorDefinition.CONTAINS_ITEM);
-
this.isNegated = isNegated;
-
}
-
-
-
public boolean evaluate(InternalWorkingMemory workingMemory,
-
InternalReadAccessor extractor, InternalFactHandle factHandle,
-
FieldValue value) {
-
Object order = extractor.getValue(workingMemory, factHandle.getObject());
-
-
return this.isNegated ^ this.evaluateUnsafe(order, value.getValue());
-
}
-
-
-
public boolean evaluate(InternalWorkingMemory workingMemory,
-
InternalReadAccessor leftExtractor, InternalFactHandle left,
-
InternalReadAccessor rightExtractor, InternalFactHandle right) {
-
Object order = leftExtractor.getValue(workingMemory, left.getObject());
-
Object itemId = rightExtractor.getValue(workingMemory, right.getObject());
-
-
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
-
}
-
-
-
public boolean evaluateCachedLeft(InternalWorkingMemory workingMemory,
-
VariableRestriction.VariableContextEntry context,
-
InternalFactHandle right) {
-
Object order = context.getFieldExtractor().getValue(workingMemory,
-
right.getObject());
-
Object itemId = ((ObjectVariableContextEntry)context).left;
-
-
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
-
}
-
-
-
public boolean evaluateCachedRight(InternalWorkingMemory workingMemory,
-
VariableRestriction.VariableContextEntry context,
-
InternalFactHandle left) {
-
Object order = ((ObjectVariableContextEntry)context).right;
-
Object itemId = context.getFieldExtractor().getValue(workingMemory,
-
left.getObject());
-
-
return this.isNegated ^ this.evaluateUnsafe(order, itemId);
-
}
-
-
private boolean evaluateUnsafe(Object order, Object itemId){
-
//if the object is not an Order return false.
-
if (!(order instanceof Order)){
-
throw new IllegalArgumentException(
-
order.getClass()+ " can't be casted to type Order");
-
}
-
-
//if the value we are comparing aginst is not a Long, return false.
-
// if (!(Long.class.isAssignableFrom(itemId.getClass()))){
-
Long itemIdAsLong;
-
try{
-
itemIdAsLong = Long.parseLong(itemId.toString());
-
} catch (NumberFormatException e){
-
throw new IllegalArgumentException(
-
itemId.getClass()+ " can't be converted to Long");
-
}
-
-
return this.evaluate((Order)order, itemIdAsLong);
-
}
-
-
private boolean evaluate(Order order, long itemId){
-
//no order lines -> no item
-
if (order.getOrderLines() == null){
-
return false;
-
}
-
-
return order.getOrderLines().stream()
-
.map(ol -> ol.getItem().getId())
-
.anyMatch(id -> id.equals(itemId));
-
}
-
-
}
-
不是實現的 org.drools.core.spi.Evaluator,ContainsItemEvaluator,而是繼承自 org.drools.core. base.BaseEvaluator,他是一個實現了接口的樣板代碼類。將具體方法的實現保留在操作符評估實際發生的地方。有四個方法我們必須實現,如下所示:
-
>evaluate:這個方法有兩個版本需要使用實現。這個方法被用於當操作符作為處理單個事實數據的條件中的一部分出現的時候。(在Phreak結構里,條件的類型是所謂的alpha網絡的一部分。 )當涉及到文本約束時,第一個版本被使用,第二個版本涉及到變量綁定時被使用。
-
>evaluateCachedLeft/evaluateCachedRight:當操作符在涉及多個事實數據的的條件下使用時,Drools使用這兩種方法。在我們可以在規則中實際使用這個新操作符之前,我們需要將它注冊到我們想要使用它的knowledge base中,有兩種方式:使用drools.packagebuilder.conf文件或者通過kmoudle.xml文件。
-
第一種將自定義操作符注冊的方式使通過特殊的文件。名叫做 drools.packagebuilder.conf。這個文件必須放在META-INF下,Drools的包構建器自動使用它來讀取正在創建的knowledge base的配置參數。為了注冊一個自定義的操作符,我們需要在這個文件中添加以下一行:
-
drools.evaluator.containsItem= org.drools.devguide.chapter05.
-
evaluator.ContainsItemEvaluatorDefinition
-
這一行必須以 drools.evaluator開頭,緊跟着的就是自定義的操作符的ID。在此之后,必須指定的是定義了自定義操作符的類的完全限定名。
-
第二種注冊自定義操作符的方式使使用kmoudle.xml文件。可以在這個文件中定義配置的特定部分,其中可以將屬性指定為鍵/值對。為了在kmodule.xml中注冊我們創建的自定義操作符,下面的配置部分必須添加到它:
-
<kmodule xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
-
xmlns= "http://jboss.org/kie/6.0.0/kmodule">
-
<configuration>
-
<property key="drools.evaluator.containsItem" value="org.
-
drools.devguide.chapter05.evaluator.ContainsItemEvaluatorDefinition"/>
-
</configuration>
-
</kmodule>
到目前為止,我們已經討論了如何定義自己的自定義操作符來為我們的域創建定制的解決方案,並提高Drools模式匹配能力。讓我們換一種方式,我們必須創建一個自定義的邏輯,以便在我們的規則中使用:自定義累積函數。
- 1
1.3.3 Custom accumulate functions
-
在之前我們介紹了 accumulate條件元素以及它的不同的用法。accumulate條件元素的結構由一個模式部分和一個或多個accumulate函數組成。在前一章中,我們看到了兩種不同類型的累積函數:內聯累積函數和內置的累積函數。
-
內聯積累函數在創建規則中顯式地定義。這些函數分為以四個部分,我們已經在前面講了,即 init,action,reverse和result.另一方面,內置函數受Drools支持,開箱機用。這些函數包括sum,count,avg,collectList,等等。
-
即使內聯累積函數是增強 Drools功能的強大且靈活的方法,它們的定義和可維護性也相當復雜。內聯累積函數在編寫、調試和維護上都很麻煩。內聯累積函數基本上是嵌入在DRL中的Java/MVEL基本代碼塊。如果我們沒有實現一個簡單的函數,那么在每個部分中編寫代碼可能會非常混亂。更糟糕的是,在內聯累積函數中進行調試幾乎是不可能的。然而,內聯累積函數最糟糕的地方可能是它們不能被重用。如果在多個規則中需要相同的函數,那么必須在每個規則中重新定義它。由於所有這些不便,內聯累積函數的使用是不鼓勵的。Drools沒有定義嵌入在DRL中的累積函數,而是允許我們在Java中定義它們,然后將它們導入到我們的knowledge base中。將累積函數的定義與它在規則中的用法解耦,解決了我們之前提到的所有問題。
-
自定義積累函數是一個 Java類,它實現了Drools的 org.kie.api.runtime.rule.AccumulateFunction 接口。作為一個例子,讓我們實現一個自定義累積函數來從一組Order訂單里,檢索具有最大總數的項,如下:
-
public class BiggestOrderFunction implements AccumulateFunction{
-
-
public static class Context implements Externalizable{
-
public Order maxOrder = null;
-
public double maxTotal = -Double.MAX_VALUE;
-
-
public Context() {}
-
-
-
public void readExternal(ObjectInput in) throws IOException,
-
ClassNotFoundException {
-
maxOrder = (Order) in.readObject();
-
maxTotal = in.readDouble();
-
}
-
-
-
public void writeExternal(ObjectOutput out) throws IOException {
-
out.writeObject(maxOrder);
-
out.writeDouble(maxTotal);
-
}
-
-
}
-
-
-
public Serializable createContext() {
-
return new Context();
-
}
-
-
-
public void init(Serializable context) throws Exception {
-
}
-
-
-
public void accumulate(Serializable context, Object value) {
-
Context c = (Context)context;
-
-
Order order = (Order) value;
-
double discount =
-
order.getDiscount() == null ? 0 : order.getDiscount()
-
.getPercentage();
-
double orderTotal = order.getTotal() - (order.getTotal() * discount);
-
-
if (orderTotal > c.maxTotal){
-
c.maxOrder = order;
-
c.maxTotal = orderTotal;
-
}
-
-
}
-
-
-
public boolean supportsReverse() {
-
return false;
-
}
-
-
-
public void reverse(Serializable context, Object value) throws Exception {
-
}
-
-
-
public Object getResult(Serializable context) throws Exception {
-
return ((Context)context).maxOrder;
-
}
-
-
-
public Class<?> getResultType() {
-
return Order.class;
-
}
-
-
-
public void writeExternal(ObjectOutput out) throws IOException {
-
}
-
-
-
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
-
}
-
-
}
-
首先本類實現的是 Drools的org.kie.api.runtime.rule.AccumulateFunction接口。這個接口定義了實現一個自定義accumulate函數所必須的方法。然而,在我們實現這個接口之前,我們需要定義一個上下文類。每次在Drools中使用累積函數時,都會為其創建一個單獨的上下文。這個上小文包括讓自定義accumulate函數正常運行的鎖必須的信息。在本例子中,我們定義了餓一個的靜態的Context類,它包含Order實例和一個double類型的maxTotal屬性。這個上下文將跟蹤到目前為止發現的最大的Order類。
-
一旦我們定義了我們的上下文,我們就可以實現 AccumulateFunction接口了。除了createContext()之外,這些方法的名稱和它們的語義與內聯累積函數的不同部分的名稱密切相關,如下所示:
- 1
- 2
可以使用冒號來定義表格的對齊方式,如下:
方法 | 介紹 |
---|---|
createContext | 這個方法是在第一次使用accumulate函數時創建的。這個方法的目的是創建用於這個特定的accumulate函數實例的上下文。在我們的示例中,它正在創建我們的Context類的新實例。 |
init | 這個方法也會在規則中第一次調用累積函數。該方法的參數是createContext()中創建的上下文 |
accumulate | 這就是真正的積累邏輯發生的地方。在我們的例子中,我們正在檢測當前處理的Order是否大於上下文所持有的訂單。如果是,上下文就相應地更新。此方法對應於內聯累積函數的操作部分。 |
supportsReverse | 該方法表明此累計函數是否支持逆操作。在我們的例子中,我們不支持它(否則,我們需要在上下文中保存所有分析的訂單的集合)。 |
reverse | 這個方法包含了一個邏輯,當一個事實先前與累積條件元素的模式相匹配時,這個事實就不再存在了。在我們的例子中,由於我們不支持reverse操作,這個方法仍然是空的。 |
getResult | 該方法返回累積函數的實際結果。在我們的例子中,結果是包含在我們的上下文對象中的訂單實例。 |
getResultType | 他的方法告訴Drools這種累積函數的結果類型。在我們的例子中,類型是order.class |
在我們的自定義accumulate函數可以在我們的規則中使用之前,我們需要將它導入進knowledge的包。自定義累積函數可以以以下方式導入到DRL資產中:
-
import accumulate org.drools.devguide.chapter05.acc.
-
BiggestOrderFunction biggestOrder
-
導入語句以 import accumulate關鍵字開始,接下來的是類的完全限定名,以及函數的實現。import語句的最后一部分是我們想要在DRL中賦予這個函數的名稱。
-
一旦函數被導入,我們就可以將其用作任何 Drools內置的累積函數
-
rule "Find Biggest Order"
-
when
-
$bigO: Order() from accumulate (
-
$o: Order(),
-
biggestOrder($o)
-
)
-
then
-
biggestOrder.setObject($bigO);
-
end
-