Drools是一个基于java的规则引擎,开源的,能够将复杂多变的规则从硬编码中解放出来,以规则脚本的形式存放在文件中,使得规则的变动不须要修正代码重启机器就能够当即在线上环境生效。html
本文所使用的demo已上传 http://download.csdn.net/source/3002213java
开始语法以前首先要了解一下drools的基本工做过程,一般而言咱们使用一个接口来作事情,首先要穿进去参数,其次要获取到接口的实现执行完毕后的结果,而drools也是同样的,咱们须要传递进去数据,用于规则的检查,调用外部接口,同时还可能须要获取到规则执行完毕后获得的结果。在drools中,这个传递数据进去的对象,术语叫 Fact对象。Fact对象是一个普通的java bean,规则中能够对当前的对象进行任何的读写操做,调用该对象提供的方法,当一个java bean插入到workingMemory中,规则使用的是原有对象的引用,规则经过对fact对象的读写,实现对应用数据的读写,对于其中的属性,须要提供getter setter访问器,规则中,能够动态的往当前workingMemory中插入删除新的fact对象。正则表达式
规则文件可使用 .drl文件,也能够是xml文件,这里咱们使用drl文件。数据库
规则语法:api
package:对一个规则文件而言,package是必须定义的,必须放在规则文件第一行。特别的是,package的名字是随意的,没必要必须对应物理路径,跟java的package的概念不一样,这里只是逻辑上的一种区分。一样的package下定义的function和query等能够直接使用。工具
好比:package com.drools.demo.pointoop
import:导入规则文件须要使用到的外部变量,这里的使用方法跟java相同,可是不一样于java的是,这里的import导入的不只仅能够是一个类,也能够是这个类中的某一个可访问的静态方法。post
好比:测试
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
(写的很基础,可是部分语法写的有些简单,含糊很差理解)
如今咱们模拟一个应用场景:网站伴随业务产生而进行的积分发放操做。好比支付宝信用卡还款奖励积分等。
发放积分可能伴随不一样的运营策略和季节性调整,发放数目和规则彻底不一样,若是使用硬编码的方式去伴随业务调整而修改,代码的修改、管理、优化、测试、上线将是一件很是麻烦的事情,因此,将发放规则部分提取出来,交给Drools管理,能够极大程度的解决这个问题。
(注意一点的是,并不是全部的规则相关内容都建议使用Drools,这其中要考虑系统会运行多久,规则变动频率等一系列条件,若是你的系统只会在线上运行一周,那根本不必选择Drools来加剧你的开发成本,java硬编码的方式则将是首选)
咱们定义一下发放规则:
积分的发放参考因素有:交易笔数、交易金额数目、信用卡还款次数、生日特别优惠等。
定义规则:
// 过生日,则加10分,而且将当月交易比数翻倍后再计算积分
// 2011-01-08 - 2011-08-08每个月信用卡还款3次以上,每满3笔赠送30分
// 当月购物总金额100以上,每100元赠送10分
// 当月购物次数5次以上,每五次赠送50分
// 特别的,若是所有知足了要求,则额外奖励100分
// 发生退货,扣减10分
// 退货金额大于100,扣减100分
在事先分析过程当中,咱们须要全面的考虑对于积分所须要的因素,以此整理抽象Fact对象,经过上述的假设条件,咱们假设积分计算对象以下:
[java] view plain copy
/**
* 积分计算对象
* @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;
}
// 其余getter setter方法省略
}
定义积分规则接口
[java] view plain copy
/**
* 规则接口
* @author quzishen
*/
public interface PointRuleEngine {
/**
* 初始化规则引擎
*/
public void initEngine();
/**
* 刷新规则引擎中的规则
*/
public void refreshEnginRule();
/**
* 执行规则引擎
* @param pointDomain 积分Fact
*/
public void executeRuleEngine(final PointDomain pointDomain);
}
规则接口实现,Drools的API很简单,能够参考相关API文档查看具体用法:
[java] view plain copy
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(r);
}
// 检查脚本是否有问题
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;
}
}
为了获取单实例的RuleBase,咱们定义一个工厂类
[java] view plain copy
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
[java] view plain copy
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.getPoint()+10);
$pointDomain.setBuyNums($pointDomain.getBuyNums()*2);
$pointDomain.setBuyMoney($pointDomain.getBuyMoney()*2);
$pointDomain.setBillThisMonth($pointDomain.getBillThisMonth()*2);
$pointDomain.recordPointLog($pointDomain.getUserName(),"birthdayPoint");
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.getPoint()+$pointDomain.getBillThisMonth()/3*30);
$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.getPoint()+ (int)$pointDomain.getBuyMoney()/100 * 10);
$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.getPoint()+$pointDomain.getBuyNums()/5 * 50);
$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.getPoint()+ 100);
$pointDomain.recordPointLog($pointDomain.getUserName(),"allFitPoint");
end
subpoint.drl
[java] view plain copy
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.getPoint()-10);
$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.getPoint()-10);
$pointDomain.recordPointLog($pointDomain.getUserName(),"subBackMondyPoint");
end
测试方法:
[java] view plain copy
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("刷新规则文件结束.");
}
}
}
执行结果:
-----------------
增长对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