Drools规则引擎

最近接到一个根据配置的规则进行路由的需求,实现方案中考虑过drools。然后大致研究了一下规则引擎Drools,借助本文进行一个总结和整理。


Drools简介

规则引擎是什么?
百度百科里面定义如下: 规则引擎是由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语言模块编写业务决策,接收数据输入,解释业务规则,并根据业务规则做出业务决策。

在规则较为复杂的行业,如金融、制造、医疗、物流等行业,规则引擎都是适用的。随着新行业的发展,如团购平台,对规则引擎需求的行业也将越来越多。

规则引擎常用的框架有:JBoss Drools,Rete算法,Mandarax,JLisa,OpenRules,JEOPS,InfoSapient,JRuleEngine,Roolie。其中,Drools就是一种用Java编写的开源的规则引擎。

本文将对Drools进行详细介绍。

Drools与spring集成

网络上有很多直接创建drools工程的实例,这里就不再介绍了。

实际项目中,大多数情形下都是要将drools规则引擎应用到java web项目中,因此这里说下Drools与spring的集成。首先添加drools的依赖:

		<!--drools start-->
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-core</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-decisiontables</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.kie</groupId>
            <artifactId>kie-api</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-compiler</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>knowledge-api</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.kie</groupId>
            <artifactId>kie-internal</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.kie</groupId>
            <artifactId>kie-spring</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jbpm</groupId>
            <artifactId>jbpm-kie-services</artifactId>
            <version>${drools.version}</version>
        </dependency>
        <!--drools end-->

其中drools使用的版本为:<drools.version>6.4.0.Final</drools.version>
接着创建spring-drools.xml配置文件(作为spring容器的一个子配置文件,spring容器配置文件初始化时使用spring-*.xml模式):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:kie="http://drools.org/schema/kie-spring"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://drools.org/schema/kie-spring http://drools.org/schema/kie-spring.xsd">

   <kie:kmodule id="remex-kmodule">
       <kie:kbase name="bookprice_kbase" packages="rules.bookstore.price">
           <kie:ksession name="bookprice_ksession" scope="prototype" />
       </kie:kbase>
       <!--<kie:kbase name="booksupplier_kbase" packages="rules.bookstore.supplier">
           <kie:ksession name="booksupplier_ksession" scope="prototype" />
       </kie:kbase>-->   
 </kie:kmodule>
 <bean id="kiePostProcessor" class="org.kie.spring.annotations.KModuleAnnotationPostProcessor" />
</beans>

说明

kmodule替代了drools工程中的kmodule.xml文件。

Kie:kbase标签的package属性定义了规则文件(规则文件可以用drl或者decision table表示)的本地保存路径。
Kmodule下面可以有多个kbase元素,每个kbase都有一个name,可以任意取名,但是不能重名。然后都有一个packages,其名字一般就是resources下面的文件夹的名称,或者叫包名,规则引擎会根据这里定义的包来查找规则文件,可以同时定义多个包,以逗号分隔开。

每一个kbase下面可以包含多个ksession,每一个ksession都有一个name,名字也不能重复。

KModuleAnnotationPostProcessor是初始化Drools环境的必备处理器,采用标注方式,如果不采用标注,那么可以使用 KModuleBeanFactoryPostProcessor。

接着在resources目录下创建目录rules.bookstore.price,定义相应的drl规则文件。规则文件book_price.drl文件如下:

package rules.bookstore.price;
import cn.tonghao.remex.drools.dto.Book;
dialect  "mvel"

//默认售价为书的原价
rule "default book price rule"
	//执行过一次之后就不再执行,防止后续的条件执行过程中改变变量的值导致重新满足该规则之后再次执行对应的操作
    lock-on-active 
    salience 10  //值越大,优先级越高

    when
        $b : Book()
    then
        $b.salesPrice = $b.basePrice;
end

//计算机类图书打95折
rule "the clz of computer price rule 95% discount"
    lock-on-active
    salience 9

    when
        $b : Book(clz == "computer")
    then
        $b.salesPrice = $b.basePrice * 0.95;
end

//销售区域是中国的话,在优惠的基础上再减两块钱
rule "china area sales price rule"
    salience 8

    when
        $b : Book(salesArea == "china")
    then
        $b.salesPrice = $b.salesPrice - 2;
end

//如果书的出版时间超过2年的话,售价在原价的基础上打8折, 与计算机类图书优惠不能同时享受
rule "years 2+ rule"
    salience 7

    when
        $b : Book(years > 2 &&  clz != "computer")
    then
        $b.salesPrice = $b.basePrice * 0.8;
end

测试方法如下:

@RequestMapping(value = "/order")
@ResponseBody 
public String orderBook(){
	  Book b = new Book();
	  b.setBasePrice(120.50);
	  b.setClz("computer");
      b.setName("C plus programing");
      b.setSalesArea("china");
      b.setYears(2);
      double realPrice = bookService.getBookSalePrice(b);
      System.out.println(b.getName() + ":" + realPrice);
      return "name:" + b.getName() + ". price:" + b.getSalesPrice();
 }

BookService如下:

@Service
public class BookService {

	@KSession("bookprice_ksession")
	private KieSession priceKsession;

	/**
	 * 获取一本书的当前实际售价
	 * @param b
	 * @return
	 */
	public double getBookSalePrice(Book b) {
		if (b == null) {
			throw new NullPointerException("Book can not be null.");
		}
		priceKsession.insert(b);
		priceKsession.fireAllRules();
		return b.getSalesPrice();
	}
}

在地址栏访问控制器映射的地址后,返回:name:C plus programing. price:112.475。可以看到,图书售价在实际价格上执行了打折后减去2的规则。(注意:这里有一个很大的坑需要注意一下,引入drool相关jar包之后,系统在调用KModuleBeanFactoryPostProcessor.createKieModule这个方法时,会对当前路径进行截断,导致windows环境下路径少了盘符。java中File类对于没有盘符的路径默认在当前分区查找,如果你的工作区跟tomcat不在一个磁盘分区,可能会出现找不到相应的路径。 解决办法是把tomcat和workspace放到一个分区,也可以把server location指定到跟tomcat同一个分区的某个目录。)

Drools规则

先看一个规则文件demo:

package os.nut.drools
import os.nut.drools.Message;

rule "Hello World"
    when
        m : Message(status == Message.HELLO, myMessage : message)
    then
        System.out.println(myMessage);
        m.setMessage("Goodbye cruel world");
        m.setStatus(Message.GOODBYE);
        update(m);
end

rule "GoodBye"
    when
        Message(status == Message.GOODBYE, myMessage : message)
    then
        System.out.println(myMessage);
end

上面这个规则文件包含4个部分:package,import和两个rule。

package语句的定义跟Java里面的package语句类似,唯一不同的就是在DRL文件中package后面跟的包名是不需要跟文件目录有对应关系的。

import语句的含义跟java中是一样的,就是如果在本文件中需要使用某些类的话,需要通过import语句引入进来。

在rule中,when表示条件,当when成立时执行then。
m : Message( status == Message.HELLO, myMessage : message )条件表示的意思是:当Working Memory中存在一个Message对象(也称之为一个Fact事实),并且这个对象的status字段值为Message.HELLO时条件成立,然后将该对象的message字段值赋值给myMessage,该对象赋值给m。条件成立时会执行then部分,m会更改message和status的字段值,然后将m更新到Working Memory中。此时第二个规则的条件就成立了,因此第二个规则的then也会执行,输出message字段值:Goodbye cruel world。

对于一个规则文件而言,首先声明package是必须的,除了package之外,其他对象在规则文件中的顺序是任意的,也就是说,在规则文件当中必须要有一个package声明,同时package声明必须要放在规则文件的第一行。一个规则通常包括三个部分:属性部分(attribute),条件部分(LHS)和结果部分(RHS)。一个标准规则的结构如下:

rule "name"
attributes
when
	LHS
then
	RHS
end

规则属性

属性部分可以指定规则执行次数,优先级等

属性共有13个,分别是:activation-group、agenda-group、 auto-focus、 date-effective、 date-expires、 dialect、duration、enabled、lock-on-active、no-loop、ruleflow-group、salience、when,这些属性分别适用于不同的场景,下面我们就来分别介绍这些属性的含义及用法。

salience
用来指定规则的优先级,其值是一个数字,数字越大执行优先级越高,同时它的值可以是一个负数,默认情况下为0,所以如果不手动设置规则的salience属性,它的执行顺序将会是随机的(但是一般都是按照加载顺序)。

no-loop
该属性用来控制已经执行过的规则在条件满足时是否再次执行,默认情况下规则的no-loop属性值为false,如果设置为true,则该规则只会被引擎检查一次。no-loop是对于自己规则的操作引起的重新匹配是否再次执行进行限制,但如果由其他规则的操作引起的重新匹配则不会限制,仍然会再次执行。此时需要用lock-on-actice属性保证只匹配一次

lock-on-active
确认规则只执行一次,将lock-on-active属性的值设为true,可避免因某些fact对象被修改而使已经执行过的规则再次被激活执行。Lock-on-active是no-loop的增强版属性,默认为false。

date-effective
控制规则只有在系统时间到达设置的时间后才会触发,可接受的日期格式为:dd-MMM-yyy,例如2018年3月25日,规则如下:

rule "rule1"
date-effective "25-九月-2018"
when
eval(true);
then
System.out.println("rule1 is execution!");
end

如果是英文系统应该写成25-Sep-2018。

date-expires
该属性的作用与date-effective恰好相反,用来设置规则的有效期。即只有当前系统时间小于date-expires的值时规则才会执行。具体用法与date-effectivce类似。

enabled
该属性用来定义一个规则是否可用,默认为true,如果显式设置为false,则不会被执行。

dialect
该属性用来定义规则中要使用的语言类型,目前Drools版本中支持两种类型的语言,mvel和java,默认情况下为java。在前文与spring集成的例子中就是用的mvel方言,mvel支持导航式的写法。如果使用java,那么规则文件应该是这样的:

package rules.bookstore.price;
import cn.tonghao.remex.drools.dto.Book;

function void printInfo(double price){
    System.out.println(price);
}

//默认售价为书的原价
rule "default book price rule"
    lock-on-active //执行过一次之后就不再执行,防止后续的条件执行过程中改变变量的值导致重新满足该规则之后再次执行对应的操作
    salience 10  //值越大,优先级越高

    when
        $b : Book($salesPrice : salesPrice)
    then
        $salesPrice = $b.getBasePrice();
        printInfo($salesPrice);
end

//计算机类图书打95折
rule "the clz of computer price rule 95% discount"
    //no-loop
    lock-on-active
    salience 9

    when
        $b : Book( clz == "computer", $salesPrice : salesPrice)
    then
        $salesPrice = $b.getBasePrice() * 0.95;
        $b.setSalesPrice($salesPrice);
        update($b);
end


//销售区域是中国的话,在优惠的基础上再减两块钱
rule "china area sales price rule"
    //no-loop
    lock-on-active
    salience 8

    when
        $b : Book(salesArea == "china", $salesPrice : salesPrice)
    then
        $salesPrice = $salesPrice - 2;
        System.out.println($salesPrice);
        $b.setSalesPrice($salesPrice);
        update($b);

end


//如果书的出版时间超过2年的话,售价在原价的基础上打8折, 与计算机类图书优惠不能同时享受
rule "years 2+ rule"
    lock-on-active
    salience 7

    when
        $b : Book(years > 2 &&  clz != "computer",$salesPrice : salesPrice)
    then
        $salesPrice = $salesPrice * 0.8;
        $b.setSalesPrice($salesPrice);
        update($b);
end

duration
该属性指定该规则滞后执行的时间,单位是毫秒。如果设置了该属性,则规则将在该属性指定的值之后的另外一个线程里触发。示例:

rule "rule1"
	duration 3000
	when
		eval(true)
	then
		System.out.println("rule thread
	    id:"+Thread.currentThread().getId());
end

表示该规则将在3000 毫秒之后在另外一个线程里触发。

activation-group
该属性的作用是将若干规则划分为一组,用一个字符串来给这个组命名,这样在执行的时候,具有相同activation-group属性的规则中只要有一个被执行,其他规则都将不再执行,类似于if-else if-else的机制。当然对于具有相同activation-group属性的规则中哪一个会先执行,可以用类似salience之类的属性来实现。示例:

rule "rule1"
	salience 2
	activation-group "test"
	when
		eval(true)
	then
		System.out.println("rule1 execute");
end

rule "rule 2"
	salience 1
	activation-group "test"
	when
		eval(true)
	then
		System.out.println("rule2 execute");
end

agenda-group
该属性用来在agenda的基础之上,对现有的规则进行再次分组,具体的分组方法可以采用为规则添加agenda-group属性来实现。agenda-group属性的值也是一个字符串,通过这个字符串,可以将规则分为若干个agenda-group规则示例:

rule "rule1"
	agenda-group "001"
	when
		eval(true)
	then
		System.out.println("rule1 execute");
end

rule "rule 2"
	agenda-group "002"
	when
		eval(true)
	then
		System.out.println("rule2 execute");
end

java代码:

//getSession 获取KieSession 的方法自己写的。
KieSession ks = getSession();
//设置agenda-group 的auto-focus 使其执行               
ks.getAgenda().getAgendaGroup("group1").setFocus();

auto-focus
该属性作用是在已设置了agenda-group的规则上设置该规则是够可以自动获取focus,如果该属性为true,那么在引擎执行时,就不需要显式的为某个agenda group设置focus,否则需要。

ruleflow-group
在使用规则流的时候要用到ruleflow-group属性,该属性的值为一个字符串,作用是用来将规则划分为一个个的组,然后在规则流中通过使用ruleflow-group属性的值,从而使用对应的规则。

规则条件

LHS(Left Hand Side)可以包含0~n个条件,条件部分的操作符如下:

约束连接

对于条件中多个约束的连接,可以采用“&&”、“||”、“,”来实现。其中“,”默认转换为“&&”。

比较操作符

Drools中提供了十二种类型的比较操作符,分别是:>, >=, <, <=, ==, !=, contains, not contains, memberof, not memberof, matches, not matches; 在这十二种类型的比较操作符中,前六个是比较常用的。

contains和not contains通常用于检查一个Fact 对象的某个字段(该字段要是一个Collection或是一个Array 类型的对象)是否包含一个指定的对象,这个指定的对象可以是一个静态的值,也可以是一个变量。

memberof和not memberof用于判断某个Fact对象的某个字段是否在一个集合(Collection/Array)当中,用法与contains 有些类似,但也有不同,memberOf 的语法如下:Object(fieldName memberOf value[Collection/Array])可以看到memberOf 中集合类型的数据是作为被比较项的,集合类型的数据对象位于memberOf 操作符后面,同时在用memberOf 比较操作符时被比较项一定要是一个变量(绑定变量或者是一个global 对象),而不能是一个静态值。

matches 和not matches是用来对某个Fact 的字段与标准的Java 正则表达式进行相似匹配,被比较的字符串可以是一个标准的Java 正则表达式,但有一点需要注意,那就是正则表达式字符串当中不用考虑“\”的转义问题。

规则执行

RHS(Right Hand Side)部分是规则真正要做事情的部分,可以将因条件满足而触发的动作写在该部分当中,在RHS中可以使用LHS部分定义的绑定变量名,设置的全局变量或者是直接编写Java代码。在规则当中,LHS是用来放置条件的,所以在RHS中虽然可以直接编写Java代码,但不建议在代码中有条件判断,如果需要条件判断,那么请重新考虑将其放在LHS 当中,否则就违背了使用规则的初衷。

在 Drools 当中,在RHS 里面,提供了一些对当前Working Memory 实现快速操作的宏函数或宏对象,比如insert/insertLogical、updatedelete(旧版本为retract) 就可以实现对当前Working Memory中的Fact 对象进行新增、删除或者是修改。
|函数|描述|
|–|–|–|
|set|给属性赋值|
|modify|修改fact对象,并自动更新到工作空间|
|update|更新fact对象|
|insert|插入一个新的fact对象到工作空间|
|insertLogic|insert增强版,需声明撤回事件,或者自动撤回|
|delete|删除fact对象|

Insert
使用方式:insert(new Object()),一旦调用insert宏函数,那么Drools会重新与所有的规则再重新匹配一次,对于没有设置no-loop属性为true的规则,如果条件满足,不管其之前是否执行都将会再次执行一次,这个特性不仅存在于insert宏函数上,update, modify,retract宏函数同样具有该特性,所以在某些情况下因考虑不当调用insert、update、modify或retract容易发生死循环。示例:

when
	eval(true);
then
	Customer cus=new Customer();
	cus.setName("张三");
	insert(cus);
end

update
Update宏函数用来实现对当前working memory当中的fact进行更新。
delete
旧版本中用retract用来将working memory中的某个fact对象删除。在6.x中可以用delete函数来删除。
modify
Modify用来对fact对象进行修改,修改完成后会自动更新到当前的working memory当中。

其他

函数是定义在规则文件当中的一块可以被调用的代码,规则文件中的函数以function标记开头。在前文介绍规则属性中的dialect时给了一个java语言下的规则文件,其中就使用了函数printInfo,并在第一个规则中进行了调用。

但是更常用的是在工具类中定义方法,然后引入到规则文件中进行调用。比如在RuleTools中定义静态方法:

public static void printTest(String str){
   System.out.println(str);
}

然后在规则文件中引入: import function cn.tonghao.remex.drools.tool.RuleTools.printTest;
最后在default book price rule中的RHS部分进行调用:

rule "default book price rule"
	 lock-on-active 
	 salience 10  //值越大,优先级越高
	 
    when
        $b : Book($salesPrice : salesPrice);
    then
        $salesPrice = $b.getBasePrice();
        $b.setSalesPrice($salesPrice);
        update($b);
        printTest("hello");
end

有两点需要注意,一是必须在规则文件中定义一个function,就算该function什么都不做。否则只能在LHS部分使用外部引入的函数,而不能在RHS部分使用。二是在idea中会提示编译错误:方法无法解析。可能是插件原因引起的,但不影响程序运行。

参考资料

[1]. http://blog.csdn.net/mxlmxlmxl33/article/details/78783416
[2]. http://blog.csdn.net/lei32323/article/details/74544593
[3]. http://blog.csdn.net/u012373815/article/details/53872025

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值