1. 程式人生 > >JAVA規則引擎 -- Drools

JAVA規則引擎 -- Drools

Drools是一個基於java的規則引擎,開源的,可以將複雜多變的規則從硬編碼中解放出來,以規則指令碼的形式存放在檔案中,使得規則的變更不需要修正程式碼重啟機器就可以立即在線上環境生效。

1、Drools語法

開始語法之前首先要了解一下drools的基本工作過程,通常而言我們使用一個介面來做事情,首先要穿進去引數,其次要獲取到介面的實現執行完畢後的結果,而drools也是一樣的,我們需要傳遞進去資料,用於規則的檢查,呼叫外部介面,同時還可能需要獲取到規則執行完畢後得到的結果。在drools中,這個傳遞資料進去的物件,術語叫 Fact物件。Fact物件是一個普通的java bean,規則中可以對當前的物件進行任何的讀寫操作,呼叫該物件提供的方法,當一個java bean插入到workingMemory中,規則使用的是原有物件的引用,規則通過對fact物件的讀寫,實現對應用資料的讀寫,對於其中的屬性,需要提供getter setter訪問器,規則中,可以動態的往當前workingMemory中插入刪除新的fact物件。

規則檔案可以使用 .drl檔案,也可以是xml檔案,這裡我們使用drl檔案。

規則語法:

package:對一個規則檔案而言,package是必須定義的,必須放在規則檔案第一行。特別的是,package的名字是隨意的,不必必須對應物理路徑,跟java的package的概念不同,這裡只是邏輯上的一種區分。同樣的package下定義的function和query等可以直接使用。

比如:package com.drools.demo.point

import:匯入規則檔案需要使用到的外部變數,這裡的使用方法跟java相同,但是不同於java的是,這裡的import匯入的不僅僅可以是一個類,也可以是這個類中的某一個可訪問的靜態方法。

比如:

import com.drools.demo.point.PointDomain;

import com.drools.demo.point.PointDomain.getById;

rule:定義一個規則。rule "ruleName"。一個規則可以包含三個部分:

屬性部分:定義當前規則執行的一些屬性等,比如是否可被重複執行、過期時間、生效時間等。

條件部分,即LHS,定義當前規則的條件,如  when Message(); 判斷當前workingMemory中是否存在Message物件。

結果部分,即RHS,這裡可以寫普通java程式碼,即當前規則條件滿足後執行的操作,可以直接呼叫Fact物件的方法來操作應用。

規則事例:

rule "name"

       no-loop true

       when

               $message:Message(status == 0)

       then

               System.out.println("fit");

               $message.setStatus(1);

               update($message);

end

上述的屬性中:

no-loop : 定義當前的規則是否不允許多次迴圈執行,預設是false,也就是當前的規則只要滿足條件,可以無限次執行。什麼情況下會出現一條規則執行過一次又被多次重複執行呢?drools提供了一些api,可以對當前傳入workingMemory中的Fact物件進行修改或者個數的增減,比如上述的update方法,就是將當前的workingMemory中的Message型別的Fact物件進行屬性更新,這種操作會觸發規則的重新匹配執行,可以理解為Fact物件更新了,所以規則需要重新匹配一遍,那麼疑問是之前規則執行過並且修改過的那些Fact物件的屬性的資料會不會被重置?結果是不會,已經修改過了就不會被重置,update之後,之前的修改都會生效。當然對Fact物件資料的修改並不是一定需要呼叫update才可以生效,簡單的使用set方法設定就可以完成,這裡類似於java的引用呼叫,所以何時使用update是一個需要仔細考慮的問題,一旦不慎,極有可能會造成規則的死迴圈。上述的no-loop true,即設定當前的規則,只執行一次,如果本身的RHS部分有update等觸發規則重新執行的操作,也不要再次執行當前規則。

但是其他的規則會被重新執行,豈不是也會有可能造成多次重複執行,資料紊亂甚至死迴圈?答案是使用其他的標籤限制,也是可以控制的:lock-on-active true

lock-on-active true:通過這個標籤,可以控制當前的規則只會被執行一次,因為一個規則的重複執行不一定是本身觸發的,也可能是其他規則觸發的,所以這個是no-loop的加強版。當然該標籤正規的用法會有其他的標籤的配合,後續提及。

date-expires:設定規則的過期時間,預設的時間格式:“日-月-年”,中英文格式相同,但是寫法要用各自對應的語言,比如中文:"29-七月-2010",但是還是推薦使用更為精確和習慣的格式,這需要手動在java程式碼中設定當前系統的時間格式,後續提及。屬性用法舉例:date-expires "2011-01-31 23:59:59" // 這裡我們使用了更為習慣的時間格式

date-effective:設定規則的生效時間,時間格式同上。

duration:規則定時,duration 3000   3秒後執行規則

salience:優先順序,數值越大越先執行,這個可以控制規則的執行順序。

其他的屬性可以參照相關的api文件檢視具體用法,此處略。

規則的條件部分,即LHS部分:

when:規則條件開始。條件可以單個,也可以多個,多個條件一次排列,比如

 when

         eval(true)

         $customer:Customer()

         $message:Message(status==0)

上述羅列了三個條件,當前規則只有在這三個條件都匹配的時候才會執行RHS部分,三個條件中第一個

eval(true):是一個預設的api,true 無條件執行,類似於 while(true)

$message:Message(status==0) 這句話標示的:當前的workingMemory存在Message型別並且status屬性的值為0的Fact物件,這個物件通常是通過外部java程式碼插入或者自己在前面已經執行的規則的RHS部分中insert進去的。

前面的$message代表著當前條件的引用變數,在後續的條件部分和RHS部分中,可以使用當前的變數去引用符合條件的FACT物件,修改屬性或者呼叫方法等。可選,如果不需要使用,則可以不寫。

條件可以有組合,比如:

Message(status==0 || (status > 1 && status <=100))

RHS中對Fact物件private屬性的操作必須使用getter和setter方法,而RHS中則必須要直接用.的方法去使用,比如

  $order:Order(name=="qu")
  $message:Message(status==0 && orders contains $order && $order.name=="qu")

特別的是,如果條件全部是 &&關係,可以使用“,”來替代,但是兩者不能混用

如果現在Fact物件中有一個List,需要判斷條件,如何判斷呢?

看一個例子:

Message {

        int status;

        List<String> names;

}

$message:Message(status==0 && names contains "網易" && names.size >= 1)

上述的條件中,status必須是0,並且names列表中含有“網易”並且列表長度大於等於1

contains:對比是否包含操作,操作的被包含目標可以是一個複雜物件也可以是一個簡單的值。

Drools提供了十二中型別比較操作符:

>  >=  <  <=  ==  !=  contains / not contains / memberOf / not memberOf /matches/ not matches

not contains:與contains相反。

memberOf:判斷某個Fact屬性值是否在某個集合中,與contains不同的是他被比較的物件是一個集合,而contains被比較的物件是單個值或者物件。

not memberOf:正好相反。

matches:正則表示式匹配,與java不同的是,不用考慮'/'的轉義問題

not matches:正好相反。

規則的結果部分

當規則條件滿足,則進入規則結果部分執行,結果部分可以是純java程式碼,比如:

then

       System.out.println("OK"); //會在控制檯打印出ok

end

當然也可以呼叫Fact的方法,比如  $message.execute();操作資料庫等等一切操作。

結果部分也有drools提供的方法:

insert:往當前workingMemory中插入一個新的Fact物件,會觸發規則的再次執行,除非使用no-loop限定;

update:更新

modify:修改,與update語法不同,結果都是更新操作

retract:刪除

RHS部分除了呼叫Drools提供的api和Fact物件的方法,也可以呼叫規則檔案中定義的方法,方法的定義使用 function 關鍵字

function void console {

   System.out.println();

   StringUtils.getId();// 呼叫外部靜態方法,StringUtils必須使用import匯入,getId()必須是靜態方法

}

Drools還有一個可以定義類的關鍵字:

declare 可以再規則檔案中定義一個class,使用起來跟普通java物件相似,你可以在RHS部分中new一個並且使用getter和setter方法去操作其屬性。

declare Address
 @author(quzishen) // 元資料,僅用於描述資訊

 @createTime(2011-1-24)
 city : String @maxLengh(100)
 postno : int
end

上述的'@'是什麼呢?是元資料定義,用於描述資料的資料~,沒什麼執行含義

你可以在RHS部分中使用Address address = new Address()的方法來定義一個物件。

更多的規則語法,可以參考其他網際網路資料,推薦:

(寫的很基礎,但是部分語法寫的有些簡單,含糊不好理解)

2、Drools應用例項:

現在我們模擬一個應用場景:網站伴隨業務產生而進行的積分發放操作。比如支付寶信用卡還款獎勵積分等。

發放積分可能伴隨不同的運營策略和季節性調整,發放數目和規則完全不同,如果使用硬編碼的方式去伴隨業務調整而修改,程式碼的修改、管理、優化、測試、上線將是一件非常麻煩的事情,所以,將發放規則部分提取出來,交給Drools管理,可以極大程度的解決這個問題。

(注意一點的是,並非所有的規則相關內容都建議使用Drools,這其中要考慮系統會執行多久,規則變更頻率等一系列條件,如果你的系統只會在線上執行一週,那根本沒必要選擇Drools來加重你的開發成本,java硬編碼的方式則將是首選)

我們定義一下發放規則:

積分的發放參考因素有:交易筆數、交易金額數目、信用卡還款次數、生日特別優惠等。

定義規則:

// 過生日,則加10分,並且將當月交易比數翻倍後再計算積分

// 2011-01-08 - 2011-08-08每月信用卡還款3次以上,每滿3筆贈送30分

// 當月購物總金額100以上,每100元贈送10分

// 當月購物次數5次以上,每五次贈送50分

// 特別的,如果全部滿足了要求,則額外獎勵100分

// 發生退貨,扣減10分

// 退貨金額大於100,扣減100分

在事先分析過程中,我們需要全面的考慮對於積分所需要的因素,以此整理抽象Fact物件,通過上述的假設條件,我們假設積分計算物件如下:

  1. /** 
  2.  * 積分計算物件 
  3.  * @author quzishen 
  4.  */  
  5. public class PointDomain {  
  6.     // 使用者名稱  
  7.     private String userName;  
  8.     // 是否當日生日  
  9.     private boolean birthDay;  
  10.     // 增加積分數目  
  11.     private long point;  
  12.     // 當月購物次數  
  13.     private int buyNums;  
  14.     // 當月退貨次數  
  15.     private int backNums;  
  16.     // 當月購物總金額  
  17.     private double buyMoney;  
  18.     // 當月退貨總金額  
  19.     private double backMondy;  
  20.     // 當月信用卡還款次數  
  21.     private int billThisMonth;  
  22.     /** 
  23.      * 記錄積分發送流水,防止重複發放 
  24.      * @param userName 使用者名稱 
  25.      * @param type 積分發放型別 
  26.      */  
  27.     public void recordPointLog(String userName, String type){  
  28.         System.out.println("增加對"+userName+"的型別為"+type+"的積分操作記錄.");  
  29.     }  
  30.     public String getUserName() {  
  31.         return userName;  
  32.     }  
  33. // 其他getter setter方法省略  
  34. }  

定義積分規則介面

  1. /** 
  2.  * 規則介面 
  3.  * @author quzishen 
  4.  */  
  5. public interface PointRuleEngine {  
  6.     /** 
  7.      * 初始化規則引擎 
  8.      */  
  9.     public void initEngine();  
  10.     /** 
  11.      * 重新整理規則引擎中的規則 
  12.      */  
  13.     public void refreshEnginRule();  
  14.     /** 
  15.      * 執行規則引擎 
  16.      * @param pointDomain 積分Fact 
  17.      */  
  18.     public void executeRuleEngine(final PointDomain pointDomain);  
  19. }  

規則介面實現,Drools的API很簡單,可以參考相關API文件檢視具體用法:

  1. import java.io.File;  
  2. import java.io.FileNotFoundException;  
  3. import java.io.FileReader;  
  4. import java.io.IOException;  
  5. import java.io.Reader;  
  6. import java.util.ArrayList;  
  7. import java.util.List;  
  8. import org.drools.RuleBase;  
  9. import org.drools.StatefulSession;  
  10. import org.drools.compiler.DroolsParserException;  
  11. import org.drools.compiler.PackageBuilder;  
  12. import org.drools.spi.Activation;  
  13. /** 
  14.  * 規則介面實現類 
  15.  * @author quzishen 
  16.  */  
  17. public class PointRuleEngineImpl implements PointRuleEngine {  
  18.     private RuleBase ruleBase;  
  19.     /* (non-Javadoc) 
  20.      * @see com.drools.demo.point.PointRuleEngine#initEngine() 
  21.      */  
  22.     public void initEngine() {  
  23.         // 設定時間格式  
  24.         System.setProperty("drools.dateformat", "yyyy-MM-dd HH:mm:ss");  
  25.         ruleBase = RuleBaseFacatory.getRuleBase();  
  26.         try {  
  27.             PackageBuilder backageBuilder = getPackageBuilderFromDrlFile();  
  28.             ruleBase.addPackages(backageBuilder.getPackages());  
  29.         } catch (DroolsParserException e) {  
  30.             e.printStackTrace();  
  31.         } catch (IOException e) {  
  32.             e.printStackTrace();  
  33.         } catch (Exception e) {  
  34.             e.printStackTrace();  
  35.         }  
  36.     }  
  37.     /* (non-Javadoc) 
  38.      * @see com.drools.demo.point.PointRuleEngine#refreshEnginRule() 
  39.      */  
  40.     public void refreshEnginRule() {  
  41.         ruleBase = RuleBaseFacatory.getRuleBase();  
  42.         org.drools.rule.Package[] packages = ruleBase.getPackages();  
  43.         for(org.drools.rule.Package pg : packages) {  
  44.             ruleBase.removePackage(pg.getName());  
  45.         }  
  46.         initEngine();  
  47.     }  
  48.     /* (non-Javadoc) 
  49.      * @see com.drools.demo.point.PointRuleEngine#executeRuleEngine(com.drools.demo.point.PointDomain) 
  50.      */  
  51.     public void executeRuleEngine(final PointDomain pointDomain) {  
  52.         if(null == ruleBase.getPackages() || 0 == ruleBase.getPackages().length) {  
  53.             return;  
  54.         }  
  55.         StatefulSession statefulSession = ruleBase.newStatefulSession();  
  56.         statefulSession.insert(pointDomain);  
  57.         // fire  
  58.         statefulSession.fireAllRules(new org.drools.spi.AgendaFilter() {  
  59.             public boolean accept(Activation activation) {  
  60.                 return !activation.getRule().getName().contains("_test");  
  61.             }  
  62.         });  
  63.         statefulSession.dispose();  
  64.     }  
  65.     /** 
  66.      * 從Drl規則檔案中讀取規則 
  67.      * @return 
  68.      * @throws Exception 
  69.      */  
  70.     private PackageBuilder getPackageBuilderFromDrlFile() throws Exception {  
  71.         // 獲取測試指令碼檔案  
  72.         List<String> drlFilePath = getTestDrlFile();  
  73.         // 裝載測試指令碼檔案  
  74.         List<Reader> readers = readRuleFromDrlFile(drlFilePath);  
  75.         PackageBuilder backageBuilder = new PackageBuilder();  
  76.         for (Reader r : readers) {  
  77.             backageBuilder.addPackageFromDrl(r);  
  78.         }  
  79.         // 檢查指令碼是否有問題  
  80.         if(backageBuilder.hasErrors()) {  
  81.             throw new Exception(backageBuilder.getErrors().toString());  
  82.         }  
  83.         return backageBuilder;  
  84.     }  
  85.     /** 
  86.      * @param drlFilePath 指令碼檔案路徑 
  87.      * @return 
  88.      * @throws FileNotFoundException 
  89.      */  
  90.     private List<Reader> readRuleFromDrlFile(List<String> drlFilePath) throws FileNotFoundException {  
  91.         if (null == drlFilePath || 0 == drlFilePath.size()) {  
  92.             return null;  
  93.         }  
  94.         List<Reader> readers = new ArrayList<Reader>();  
  95.         for (String ruleFilePath : drlFilePath) {  
  96.             readers.add(new FileReader(new File(ruleFilePath)));  
  97.         }  
  98.         return readers;  
  99.     }  
  100.     /** 
  101.      * 獲取測試規則檔案 
  102.      *  
  103.      * @return 
  104.      */  
  105.     private List<String> getTestDrlFile() {  
  106.         List<String> drlFilePath = new ArrayList<String>();  
  107.         drlFilePath  
  108.                 .add("D:/workspace2/DroolsDemo/src/com/drools/demo/point/addpoint.drl");  
  109.         drlFilePath  
  110.                 .add("D:/workspace2/DroolsDemo/src/com/drools/demo/point/subpoint.drl");  
  111.         return drlFilePath;  
  112.     }  
  113. }  

為了獲取單例項的RuleBase,我們定義一個工廠類

  1. import org.drools.RuleBase;  
  2. import org.drools.RuleBaseFactory;  
  3. /** 
  4.  * RuleBaseFacatory 單例項RuleBase生成工具 
  5.  * @author quzishen 
  6.  */  
  7. public class RuleBaseFacatory {  
  8.     private static RuleBase ruleBase;  
  9.     public static RuleBase getRuleBase(){  
  10.         return null != ruleBase ? ruleBase : RuleBaseFactory.newRuleBase();  
  11.     }  
  12. }  

剩下的就是定義兩個規則檔案,分別用於積分發放和積分扣減

addpoint.drl

  1. package com.drools.demo.point  
  2. import com.drools.demo.point.PointDomain;  
  3. rule birthdayPoint  
  4.     // 過生日,則加10分,並且將當月交易比數翻倍後再計算積分  
  5.     salience 100  
  6.     lock-on-active true  
  7.     when  
  8.         $pointDomain : PointDomain(birthDay == true)  
  9.     then  
  10.         $pointDomain.setPoint($pointDomain.getPoint()+10);  
  11.         $pointDomain.setBuyNums($pointDomain.getBuyNums()*2);  
  12.         $pointDomain.setBuyMoney($pointDomain.getBuyMoney()*2);  
  13.         $pointDomain.setBillThisMonth($pointDomain.getBillThisMonth()*2);  
  14.         $pointDomain.recordPointLog($pointDomain.getUserName(),"birthdayPoint");  
  15. end  
  16. rule billThisMonthPoint  
  17.     // 2011-01-08 - 2011-08-08每月信用卡還款3次以上,每滿3筆贈送30分  
  18.     salience 99  
  19.     lock-on-active true  
  20.     date-effective "2011-01-08 23:59:59"  
  21.     date-expires "2011-08-08 23:59:59"  
  22.     when  
  23.         $pointDomain : PointDomain(billThisMonth >= 3)  
  24.     then  
  25.         $pointDomain.setPoint($pointDomain.getPoint()+$pointDomain.getBillThisMonth()/3*30);  
  26.         $pointDomain.recordPointLog($pointDomain.getUserName(),"billThisMonthPoint");  
  27. end  
  28. rule buyMoneyPoint  
  29.     // 當月購物總金額100以上,每100元贈送10分  
  30.     salience 98  
  31.     lock-on-active true  
  32.     when  
  33.         $pointDomain : PointDomain(buyMoney >= 100)  
  34.     then  
  35.         $pointDomain.setPoint($pointDomain.getPoint()+ (int)$pointDomain.getBuyMoney()/100 * 10);  
  36.         $pointDomain.recordPointLog($pointDomain.getUserName(),"buyMoneyPoint");  
  37. end  
  38. rule buyNumsPoint  
  39.     // 當月購物次數5次以上,每五次贈送50分  
  40.     salience 97  
  41.     lock-on-active true  
  42.     when  
  43.         $pointDomain : PointDomain(buyNums >= 5)  
  44.     then  
  45.         $pointDomain.setPoint($pointDomain.getPoint()+$pointDomain.getBuyNums()/5 * 50);  
  46.         $pointDomain.recordPointLog($pointDomain.getUserName(),"buyNumsPoint");  
  47. end  
  48. rule allFitPoint  
  49.     // 特別的,如果全部滿足了要求,則額外獎勵100分  
  50.     salience 96  
  51.     lock-on-active true  
  52.     when  
  53.         $pointDomain:PointDomain(buyNums >= 5 && billThisMonth >= 3 && buyMoney >= 100)  
  54.     then  
  55.         $pointDomain.setPoint($pointDomain.getPoint()+ 100);  
  56.         $pointDomain.recordPointLog($pointDomain.getUserName(),"allFitPoint");  
  57. end  

subpoint.drl

  1. package com.drools.demo.point  
  2. import com.drools.demo.point.PointDomain;  
  3. rule subBackNumsPoint  
  4.     // 發生退貨,扣減10分  
  5.     salience 10  
  6.     lock-on-active true  
  7.     when  
  8.         $pointDomain : PointDomain(backNums >= 1)  
  9.     then  
  10.         $pointDomain.setPoint($pointDomain.getPoint()-10);  
  11.         $pointDomain.recordPointLog($pointDomain.getUserName(),"subBackNumsPoint");  
  12. end  
  13. rule subBackMondyPoint  
  14.     // 退貨金額大於100,扣減100分  
  15.     salience 9  
  16.     lock-on-active true  
  17.     when  
  18.         $pointDomain : PointDomain(backMondy >= 100)  
  19.     then  
  20.         $pointDomain.setPoint($pointDomain.getPoint()-10);  
  21.         $pointDomain.recordPointLog($pointDomain.getUserName(),"subBackMondyPoint");  
  22. end  

測試方法:

  1. public static void main(String[] args) throws IOException {  
  2.         PointRuleEngine pointRuleEngine = new PointRuleEngineImpl();  
  3.         while(true){  
  4.             InputStream is = System.in;  
  5.             BufferedReader br = new BufferedReader(new InputStreamReader(is));  
  6.             String input = br.readLine();  
  7.             if(null != input && "s".equals(input)){  
  8.                 System.out.println("初始化規則引擎...");  
  9.                 pointRuleEngine.initEngine();  
  10.                 System.out.println("初始化規則引擎結束.");  
  11.             }else if("e".equals(input)){  
  12.                 final PointDomain pointDomain = new PointDomain();  
  13.                 pointDomain.setUserName("hello kity");  
  14.                 pointDomain.setBackMondy(100d);  
  15.                 pointDomain.setBuyMoney(500d);  
  16.                 pointDomain.setBackNums(1);  
  17.                 pointDomain.setBuyNums(5);  
  18.                 pointDomain.setBillThisMonth(5);  
  19.                 pointDomain.setBirthDay(true);  
  20.                 pointDomain.setPoint(0l);  
  21.                 pointRuleEngine.executeRuleEngine(pointDomain);  
  22.                 System.out.println("執行完畢BillThisMonth:"+pointDomain.getBillThisMonth());  
  23.                 System.out.println("執行完畢BuyMoney:"+pointDomain.getBuyMoney());  
  24.                 System.out.println("執行完畢BuyNums:"+pointDomain.getBuyNums());  
  25.                 System.out.println("執行完畢規則引擎決定傳送積分:"+pointDomain.getPoint());  
  26.             } else if("r".equals(input)){  
  27.                 System.out.println("重新整理規則檔案...");  
  28.                 pointRuleEngine.refreshEnginRule();  
  29.                 System.out.println("重新整理規則檔案結束.");  
  30.             }  
  31.         }  
  32.     }  

執行結果:

-----------------

增加對hello kity的型別為birthdayPoint的積分操作記錄.
增加對hello kity的型別為billThisMonthPoint的積分操作記錄.
增加對hello kity的型別為buyMoneyPoint的積分操作記錄.
增加對hello kity的型別為buyNumsPoint的積分操作記錄.
增加對hello kity的型別為allFitPoint的積分操作記錄.
增加對hello kity的型別為subBackNumsPoint的積分操作記錄.
增加對hello kity的型別為subBackMondyPoint的積分操作記錄.
執行完畢BillThisMonth:10
執行完畢BuyMoney:1000.0
執行完畢BuyNums:10
執行完畢規則引擎決定傳送積分:380