http://blog.csdn.net/quzishen/archive/2011/01/25/6163012.aspx
Drools是一個基於java的規則引擎,開源的,可以將復雜多變的規則從硬編碼中解放出來,以規則腳本的形式存放在文件中,使得規則的變更不需要修正代碼重啟機器就可以立即在線上環境生效。
轉自https://blog.csdn.net/hc_ttxs/article/details/85248696
本文所使用的demo已上傳 http://download.csdn.net/source/3002213
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()的方法來定義一個對象。
更多的規則語法,可以參考其他互聯網資料,推薦:
http://wenku.baidu.com/view/a6516373f242336c1eb95e7c.html
(寫的很基礎,但是部分語法寫的有些簡單,含糊不好理解)
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對象,通過上述的假設條件,我們假設積分計算對象如下:
/** * 積分計算對象 * @author quzishen */ public class PointDomain { // 用戶名 private String userName; // 是否當日生日 private boolean birthDay; // 增加積分數目 private long point; // 當月購物次數 private int buyNums; // 當月退貨次數 private int backNums; // 當月購物總金額 private double buyMoney; // 當月退貨總金額 private double backMondy; // 當月信用卡還款次數 private int billThisMonth;
/**
* 記錄積分發送流水,防止重復發放
* @param userName 用戶名
* @param type 積分發放類型
*/
public void recordPointLog(String userName, String type){
System.out.println("增加對"+userName+"的類型為"+type+"的積分操作記錄.");
}
public String getUserName() {
return userName;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
// 其他getter setter方法省略
}
定義積分規則接口
/** * 規則接口 * @author quzishen */ public interface PointRuleEngine {
/**
* 初始化規則引擎
*/
public void initEngine();
/**
* 刷新規則引擎中的規則
*/
public void refreshEnginRule();
/**
* 執行規則引擎
* @param pointDomain 積分Fact
*/
public void executeRuleEngine(final PointDomain pointDomain);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
}
規則接口實現,Drools的API很簡單,可以參考相關API文檔查看具體用法:
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.List;
import org.drools.RuleBase;
import org.drools.StatefulSession;
import org.drools.compiler.DroolsParserException;
import org.drools.compiler.PackageBuilder;
import org.drools.spi.Activation;
/**
-
規則接口實現類
-
@author quzishen
*/
public class PointRuleEngineImpl implements PointRuleEngine {
private RuleBase ruleBase;/* (non-Javadoc)
- @see com.drools.demo.point.PointRuleEngine#initEngine()
*/
public void initEngine() {
// 設置時間格式
System.setProperty(“drools.dateformat”, “yyyy-MM-dd HH:mm:ss”);
ruleBase = RuleBaseFacatory.getRuleBase();
try {
PackageBuilder backageBuilder = getPackageBuilderFromDrlFile();
ruleBase.addPackages(backageBuilder.getPackages());
} catch (DroolsParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
/* (non-Javadoc)
-
@see com.drools.demo.point.PointRuleEngine#refreshEnginRule()
*/
public void refreshEnginRule() {
ruleBase = RuleBaseFacatory.getRuleBase();
org.drools.rule.Package[] packages = ruleBase.getPackages();
for(org.drools.rule.Package pg : packages) {
ruleBase.removePackage(pg.getName());
}initEngine();
}
/* (non-Javadoc)
-
@see com.drools.demo.point.PointRuleEngine#executeRuleEngine(com.drools.demo.point.PointDomain)
*/
public void executeRuleEngine(final PointDomain pointDomain) {
if(null == ruleBase.getPackages() || 0 == ruleBase.getPackages().length) {
return;
}StatefulSession statefulSession = ruleBase.newStatefulSession();
statefulSession.insert(pointDomain);// fire
statefulSession.fireAllRules(new org.drools.spi.AgendaFilter() {
public boolean accept(Activation activation) {
return !activation.getRule().getName().contains("_test");
}
});statefulSession.dispose();
}
/**
-
從Drl規則文件中讀取規則
-
@return
-
@throws Exception
*/
private PackageBuilder getPackageBuilderFromDrlFile() throws Exception {
// 獲取測試腳本文件
List<String> drlFilePath = getTestDrlFile();
// 裝載測試腳本文件
List<Reader> readers = readRuleFromDrlFile(drlFilePath);PackageBuilder backageBuilder = new PackageBuilder();
for (Reader r : readers) {
backageBuilder.addPackageFromDrl®;
}// 檢查腳本是否有問題
if(backageBuilder.hasErrors()) {
throw new Exception(backageBuilder.getErrors().toString());
}return backageBuilder;
}
/**
-
@param drlFilePath 腳本文件路徑
-
@return
-
@throws FileNotFoundException
*/
private List<Reader> readRuleFromDrlFile(List<String> drlFilePath) throws FileNotFoundException {
if (null == drlFilePath || 0 == drlFilePath.size()) {
return null;
}List<Reader> readers = new ArrayList<Reader>();
for (String ruleFilePath : drlFilePath) {
readers.add(new FileReader(new File(ruleFilePath)));
}return readers;
}
/**
-
獲取測試規則文件
-
@return
*/
private List<String> getTestDrlFile() {
List<String> drlFilePath = new ArrayList<String>();
drlFilePath
.add(“D:/workspace2/DroolsDemo/src/com/drools/demo/point/addpoint.drl”);
drlFilePath
.add(“D:/workspace2/DroolsDemo/src/com/drools/demo/point/subpoint.drl”);return drlFilePath;
}
}
- @see com.drools.demo.point.PointRuleEngine#initEngine()
為了獲取單實例的RuleBase,我們定義一個工廠類
import org.drools.RuleBase; import org.drools.RuleBaseFactory;
/**
-
RuleBaseFacatory 單實例RuleBase生成工具
-
@author quzishen
*/
public class RuleBaseFacatory {
private static RuleBase ruleBase;public static RuleBase getRuleBase(){
return null != ruleBase ? ruleBase : RuleBaseFactory.newRuleBase();
}
}
剩下的就是定義兩個規則文件,分別用於積分發放和積分扣減
addpoint.drl
package com.drools.demo.point
import com.drools.demo.point.PointDomain;
rule birthdayPoint
// 過生日,則加10分,並且將當月交易比數翻倍后再計算積分
salience 100
lock-on-active true
when
$pointDomain : PointDomain(birthDay == true)
then
pointDomain.setPoint(pointDomain.setPoint(pointDomain.setPoint(pointDomain.getPoint()+10);
pointDomain.setBuyNums(pointDomain.setBuyNums(pointDomain.setBuyNums(pointDomain.getBuyNums()*2);
pointDomain.setBuyMoney(pointDomain.setBuyMoney(pointDomain.setBuyMoney(pointDomain.getBuyMoney()*2);
pointDomain.setBillThisMonth(pointDomain.setBillThisMonth(pointDomain.setBillThisMonth(pointDomain.getBillThisMonth()*2);
$pointDomain.recordPointLog($pointDomain.getUserName(),"birthdayPoint");
- 1
end
rule billThisMonthPoint
// 2011-01-08 - 2011-08-08每月信用卡還款3次以上,每滿3筆贈送30分
salience 99
lock-on-active true
date-effective “2011-01-08 23:59:59”
date-expires “2011-08-08 23:59:59”
when
$pointDomain : PointDomain(billThisMonth >= 3)
then
pointDomain.setPoint(pointDomain.setPoint(pointDomain.setPoint(pointDomain.getPoint()+$pointDomain.getBillThisMonth()/3*30);
pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.getUserName(),“billThisMonthPoint”);
end
rule buyMoneyPoint
// 當月購物總金額100以上,每100元贈送10分
salience 98
lock-on-active true
when
$pointDomain : PointDomain(buyMoney >= 100)
then
pointDomain.setPoint(pointDomain.setPoint(pointDomain.setPoint(pointDomain.getPoint()+ (int)$pointDomain.getBuyMoney()/100 * 10);
pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.getUserName(),“buyMoneyPoint”);
end
rule buyNumsPoint
// 當月購物次數5次以上,每五次贈送50分
salience 97
lock-on-active true
when
$pointDomain : PointDomain(buyNums >= 5)
then
pointDomain.setPoint(pointDomain.setPoint(pointDomain.setPoint(pointDomain.getPoint()+$pointDomain.getBuyNums()/5 * 50);
pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.getUserName(),“buyNumsPoint”);
end
rule allFitPoint
// 特別的,如果全部滿足了要求,則額外獎勵100分
salience 96
lock-on-active true
when
$pointDomain:PointDomain(buyNums >= 5 && billThisMonth >= 3 && buyMoney >= 100)
then
pointDomain.setPoint(pointDomain.setPoint(pointDomain.setPoint(pointDomain.getPoint()+ 100);
pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.getUserName(),“allFitPoint”);
end
subpoint.drl
package com.drools.demo.point
import com.drools.demo.point.PointDomain;
rule subBackNumsPoint
// 發生退貨,扣減10分
salience 10
lock-on-active true
when
$pointDomain : PointDomain(backNums >= 1)
then
pointDomain.setPoint(pointDomain.setPoint(pointDomain.setPoint(pointDomain.getPoint()-10);
pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.getUserName(),“subBackNumsPoint”);
end
rule subBackMondyPoint
// 退貨金額大於100,扣減100分
salience 9
lock-on-active true
when
$pointDomain : PointDomain(backMondy >= 100)
then
pointDomain.setPoint(pointDomain.setPoint(pointDomain.setPoint(pointDomain.getPoint()-10);
pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.recordPointLog(pointDomain.getUserName(),“subBackMondyPoint”);
end
測試方法:
public static void main(String[] args) throws IOException { PointRuleEngine pointRuleEngine = new PointRuleEngineImpl(); while(true){ InputStream is = System.in; BufferedReader br = new BufferedReader(new InputStreamReader(is)); String input = br.readLine();
if(null != input && "s".equals(input)){
System.out.println("初始化規則引擎...");
pointRuleEngine.initEngine();
System.out.println("初始化規則引擎結束.");
}else if("e".equals(input)){
final PointDomain pointDomain = new PointDomain();
pointDomain.setUserName("hello kity");
pointDomain.setBackMondy(100d);
pointDomain.setBuyMoney(500d);
pointDomain.setBackNums(1);
pointDomain.setBuyNums(5);
pointDomain.setBillThisMonth(5);
pointDomain.setBirthDay(true);
pointDomain.setPoint(0l);
pointRuleEngine.executeRuleEngine(pointDomain);
System.out.println("執行完畢BillThisMonth:"+pointDomain.getBillThisMonth());
System.out.println("執行完畢BuyMoney:"+pointDomain.getBuyMoney());
System.out.println("執行完畢BuyNums:"+pointDomain.getBuyNums());
System.out.println("執行完畢規則引擎決定發送積分:"+pointDomain.getPoint());
} else if("r".equals(input)){
System.out.println("刷新規則文件...");
pointRuleEngine.refreshEnginRule();
System.out.println("刷新規則文件結束.");
}
}
}</textarea></p>
執行結果:
-----------------
增加對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