单独写一下rule units吧,rule units也是执行控制中的一部分,这一块儿的内容比较丰富也挺有意思。
一、简述
rule units 需要实现RuleUnit接口,它由运行时数据(date sources of facts),globals,DRL rules 和function组成。可以将一组rules 划分为更小的 rule units,然后逐个执行rule units。rule units之间可以互相调用,一个ruleUnit的执行可以触发另一个ruleunit 的启动,对协调规则的执行很有效。因此 rule units挺适合有一些严格流程的业务场景。例如:我们去人群聚集的场所时需要先通过安检或防疫检测,达到某种消费金额后可以获得折扣,消费满一定金额后可以获得免费停车券,等等
二、示例:去逛商场,进商场前需要通过防疫检测获得健康认证后方可入内,医护人员可享受8折优惠,实际消费金额满500元可获得免费停车券,停车券仅限当日使用。这需求,很明显各步骤是要严格按照顺序执行的,当然可以根据优先级高低设置salience 属性就可以实现的,但是我在实际开发中觉得salience这东西挺难维护的,比如,中间我需要增加一个环节,我需要控制商场内人数时,如果开始各rule 的salience属性值之间并未设置间隔,这里就得重新调整salience,牵一发而动全身。那如果不用salience,而是借助rule units在规则执行完成的时候指定下一个该谁执行,当中间需要增加一个环节时,我就只需要像在单向链表中插入元素一样加入新规则,再调整前一个规则的调用就可以了。
啰嗦这么多其实我是没想出一个合适的场景去演示了,当然这也不是本文的重点(乱套了),本文重点还是介绍rule units 的使用方法,除了salience还有另一种方法去实现上述场景。
1. 来吧展示
构建实体模型
//职业
public class Profession {
private Integer id;
private String name;
private String rank;
private String type;//类型 0-医护;1-其他
...
}
//健康卡
public class HealthCard {
private Integer custId;
private String name;
private String state;//0-健康;1-抱恙
...
//getter、setter
}
//优惠券
public class Counpons {
private Integer id;
private String type;//券种类,0-停车券;1-消费券
private Date start;//有效期(开始时间)
private Date end;//有效期(截止时间)
private String state;//券状态0-可用;1-核销;2-作废
private Double discount;//折扣比例,免费就配成1.00 吧
...
}
//客户
public class Customer {
private Integer id;
private String name;
private List<Counpons> counpons;
private Profession profession;//职业,主业就行了,就不考虑你兼职了
private HealthCard healthCard;//健康卡
...
...
}
//商品
public class Good {
private Integer id;
private String name;
private String categery;
private Double price;
private String unit;//计量单位
...
}
//订单明细
public class Item {
private Integer id;
private Integer oId;//归属订单
private Good good;
private Double count;//数量
private double discount;
....
}
//订单主表
public class Order {
private Integer id;
private Integer custId;
private Date create;
private List<Item> goods;
private Double total;//总计
private Double pay;//应付
private Double discount;//共计优惠
//private String state;//0-未结算;1-结算
private List<Counpons> counpons;//优惠凭证
...
}
创建 rule units
//package很重要,拥有的DRL源文件的package 须要与这里保持一致,硬性要求
package com.helloworld.bk.ruleUnits.units;
import com.helloworld.bk.ruleUnits.entity.Counpons;
import org.kie.api.runtime.rule.DataSource;
import org.kie.api.runtime.rule.RuleUnit;
import java.util.List;
public class MarketUnits implements RuleUnit {
//一般用于存储 facts
private DataSource<Object> data;
//rule unit 内全局变量
private List<Counpons> counpons;
public List<Counpons> getCounpons() {
return counpons;
}
public DataSource<Object> getData() {
return data;
}
/**
*units 开始执行时
*/
@Override
public void onStart() {
System.out.println("购物开始");
}
/**
*units 执行结束时
*/
@Override
public void onEnd() {
System.out.println("购物结束");
}
/**
*调用其他units时
*/
@Override
public void onYield(RuleUnit other) {
System.out.println(this.getUnitIdentity() +",调用了,"+this.getUnitIdentity());
}
}
职业调研
package com.helloworld.bk.ruleUnits.units;
import org.kie.api.runtime.rule.DataSource;
import org.kie.api.runtime.rule.RuleUnit;
public class ProfessionUnits implements RuleUnit {
private DataSource<Object> data;
public ProfessionUnits() { }
public ProfessionUnits(DataSource<Object> data) {
this.data = data;
}
public DataSource<Object> getData() {
return data;
}
@Override
public void onStart() {
System.out.println("职业调研规则开始");
}
@Override
public void onEnd() {
System.out.println("职业调研规则结束");
}
@Override
public void onYield(RuleUnit other) {
System.out.println(this.getUnitIdentity()+",调用,"+other.getUnitIdentity());
}
}
防疫检测
package com.helloworld.bk.ruleUnits.units;
import com.helloworld.bk.ruleUnits.entity.Customer;
import org.kie.api.runtime.rule.DataSource;
import org.kie.api.runtime.rule.RuleUnit;
public class HealthUnits implements RuleUnit {
private DataSource<Object> data;//用于存储所有运行时数据(facts),可以定义多个,这里就不考虑那么多了
private Integer num;//商场内人数
private Integer maxNum;//商场内最多容纳人数
public HealthUnits(){}
public HealthUnits(DataSource<Object> data, Integer num, Integer maxNum) {
this.data = data;
this.num = num;
this.maxNum = maxNum;
}
public DataSource<Object> getData() {
return data;
}
public Integer getNum() {
return num;
}
public Integer getMaxNum() {
return maxNum;
}
@Override
public void onStart() {
//开始执行 rule units 时
System.out.println(" 防疫检测执行开始");
}
@Override
public void onEnd() {
//执行完成 rule units 时
System.out.println(" 防疫检测执行结束");
}
@Override
public void onYield(RuleUnit other) {
//调用其rule units 时
System.out.println(this.getUnitIdentity()+",调用,"+other.getUnitIdentity());
}
}
计算优惠
package com.helloworld.bk.ruleUnits.units;
import com.helloworld.bk.ruleUnits.entity.Counpons;
import org.kie.api.runtime.rule.DataSource;
import org.kie.api.runtime.rule.RuleUnit;
import java.util.List;
public class DiscountUnits implements RuleUnit {
private DataSource<Object> data;//用于存储所有运行时数据(facts),可以定义多个,这里就不考虑那么多了
private List<Counpons> counpons;
public DiscountUnits(){}
public DiscountUnits(DataSource<Object> data,List<Counpons> counpons) {
this.data = data;
this.counpons = counpons;
}
public DataSource<Object> getData() {
return data;
}
public List<Counpons> getCounpons() {
return counpons;
}
@Override
public void onStart() {
//开始执行 rule units
System.out.println(" 优惠规则执行开始");
}
@Override
public void onEnd() {
//执行完成 rule units
System.out.println(" 优惠规则执行结束");
}
@Override
public void onYield(RuleUnit other) {
System.out.println(this.getUnitIdentity()+",调用,"+other.getUnitIdentity());
}
}
核销优惠券
package com.helloworld.bk.ruleUnits.units;
import com.helloworld.bk.ruleUnits.entity.Counpons;
import org.kie.api.runtime.rule.RuleUnit;
import java.util.List;
public class ChargeOffUnits implements RuleUnit {
private List<Counpons> counpons;
public List<Counpons> getCounpons() {
return counpons;
}
@Override
public void onStart() {
System.out.println("核销优惠券开始");
}
@Override
public void onEnd() {
System.out.println("核销优惠券结束");
}
@Override
public void onYield(RuleUnit other) {
System.out.println(this.getUnitIdentity()+",调用,"+other.getUnitIdentity());
}
}
停车
package com.helloworld.bk.ruleUnits.units;
import org.kie.api.runtime.rule.DataSource;
import org.kie.api.runtime.rule.RuleUnit;
public class ParkUnits implements RuleUnit {
private DataSource<Object> data;
public DataSource<Object> getData() {
return data;
}
@Override
public void onStart() {
System.out.println(" 停车费规则开始");
}
@Override
public void onEnd() {
System.out.println(" 停车费规则结束");
}
@Override
public void onYield(RuleUnit other) {
System.out.println(this.getUnitIdentity()+",调用,"+other.getUnitIdentity());
}
}
DRL文件
package com.helloworld.bk.ruleUnits.units;
unit MarketUnits
import com.helloworld.bk.ruleUnits.entity.*
rule come_in
salience 10
when
$c:Customer() from data
then
System.out.println( "欢迎 "+$c.getName() + " 光临!");
drools.run(HealthUnits.class);//强制中断执行当前rule unit,并启动HealthUnits
end
rule leave
when
$c:Customer() from data
$o:Order(custId == $c.id,pay >= 500.00) from data
then
//System.out.println("消费已满500元");
drools.run(ParkUnits.class);
end
防疫检测
package com.helloworld.bk.ruleUnits.units //package 必须要与drl归属rule units的包名一致
unit HealthUnits //声明本drl文件归属 rule units
import com.helloworld.bk.ruleUnits.entity.*
rule health_0
salience 10
when
//data为HealthUnits的data属性,意思是,查询当前客户是否持有健康卡,且身体健康
$c:Customer(healthCard.custId == id, healthCard.state == '0') from data
then
System.out.println($c.getName() + "身体健康,允许入内,购物愉快");
drools.run(ProfessionUnits.class);
drools.run(DiscountUnits.class);
end
职业调研
package com.helloworld.bk.ruleUnits.units;
unit ProfessionUnits
import com.helloworld.bk.ruleUnits.*
import java.util.Date
function boolean test(Customer c){
System.out.println("function 运行:"+c.getProfession().getType());
return true;
}
rule "profession"
when
$c:Customer(healthCard.custId == id, healthCard.state == '0',profession.type == '0') from data
then
Counpons c = new Counpons();
c.setId(1);
c.setType("1");
c.setStart(new Date());
c.setEnd(new Date());
c.setState("0");
c.setDiscount(0.80);
$c.getCounpons().add(c);
System.out.println("医护工作者获得8折优惠券一张");
end
优惠计算
package com.helloworld.bk.ruleUnits.units
unit DiscountUnits
import com.helloworld.bk.ruleUnits.entity.*
import java.util.Date
import java.math.BigDecimal
rule discount_0
salience 10
when
$c:Customer() from data
$coup:Counpons(type == '1',state == '0',end >= new Date()) from $c.counpons
$o:Order(custId == $c.id) from data
$i:Item(oId == $o.id,$g:good) from $o.goods
do[discount] //这里利用的是drools 的 extends
//挺别扭的其实,应该是计算完成以后再核销券的,但实际规则在计算第一件商品优惠后就核销了,知道什么意思就行了,流程上我就不细抠了
not (Counpons(id == $coup.id) from counpons)
then
counpons.add($coup);
drools.run(ChargeOffUnits.class);
then[discount]
BigDecimal gp = BigDecimal.valueOf($g.getPrice());
BigDecimal ic = BigDecimal.valueOf($i.getCount());
BigDecimal cd = BigDecimal.valueOf(1-$coup.getDiscount());
//double discount = $g.getPrice() * $i.getCount() * (1-$coup.getDiscount());
double discount = gp.multiply(ic).multiply(cd).setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
$i.setDiscount(discount);
$o.setPay($o.getPay() - discount);
$o.setDiscount($o.getDiscount() + discount);
System.out.println($g.getName() +",优惠了"+discount+" 元");
end
优惠券核销
package com.helloworld.bk.ruleUnits.units;
unit ChargeOffUnits
rule "chargeOff"
when
$c:Counpons(state == '0') from counpons
then
$c.setState("1");
System.out.println("核销优惠券");
end
停车费用
package com.helloworld.bk.ruleUnits.units;
unit ParkUnits
rule parking
when
eval(true)
then
System.out.println("免费停车");
end
数据准备
public class Service {
public Order createOrder(Integer custId){
List<Item> items = new ArrayList<Item>(){{
add(new Item(1,1,new Good(1,"猪肉","肉类",40.00,"kg"),1.00,0.00));
add(new Item(2,1,new Good(2,"大葱","肉类",6.00,"kg"),1.00,0.00));
add(new Item(3,1,new Good(3,"老陈醋","调料",6.90,"壶"),1.00,0.00));
}};
Order order = new Order();
order.setCustId(custId);
order.setCreate(new Date());
order.setId(1);
order.setDiscount(0.00);
order.setPay(52.90);
order.setGoods(items);
order.setTotal(52.90);
return order;
}
}
规则调用
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
//将DRL文件等资产构建知识库
KieBase kb = kc.getKieBase();
//RuleUnitExecutor 绑定知识库
RuleUnitExecutor executor = RuleUnitExecutor.create().bind(kb);
List<Object> data = new ArrayList<>();
Profession p = new Profession(1,"医护","","0");
Customer c = new Customer(1,"y先生",null,p,new HealthCard(1,"健康卡","0"));
data.add(c);
Service service = new Service();
Order o = service.createOrder(c.getId());
//由于多个rule units 之间共享同一个DataSource,所以运行时数据采用以下方式加入到drools
//多个(组)数据绑定到同一个DataSource上,当units中有DataSource 类型 且名称为“data”时,都可以访问该值,也就是具有以上提到的属性的units之间共享该数据,其他units对数据的修改也是可见的。
executor.newDataSource("data",o,c);
//绑定非DataSource变量
executor.bindVariable("counpons",new ArrayList());
executor.run(MarketUnits.class);
//打印结果
System.out.println("java application print customer:"+c.toString());
System.out.println("java application print order:"+o.toString());
执行结果
购物开始
欢迎 y先生 光临!
org.kie.api.runtime.rule.RuleUnit$Identity@6a5c4021,调用了,org.kie.api.runtime.rule.RuleUnit$Identity@6a5c4021
防疫检测执行开始
y先生身体健康,允许入内,购物愉快
org.kie.api.runtime.rule.RuleUnit$Identity@c03c0f4f,调用,org.kie.api.runtime.rule.RuleUnit$Identity@a39fd0d4
职业调研规则开始
医护工作者获得8折优惠券一张
职业调研规则结束
优惠规则执行开始
老陈醋,优惠了1.38 元
大葱,优惠了1.2 元
猪肉,优惠了8.0 元
org.kie.api.runtime.rule.RuleUnit$Identity@ad01d824,调用,org.kie.api.runtime.rule.RuleUnit$Identity@d37fb0f0
核销优惠券开始
核销优惠券
核销优惠券结束
优惠规则执行开始
优惠规则执行结束
防疫检测执行开始
防疫检测执行结束
购物开始
购物结束
java application print customer:Customer{id=1,name=y先生,counpons=[Counpons{id=1, type='1', start=Sat Nov 28 16:03:53 GMT+08:00 2020, end=Sat Nov 28 16:03:53 GMT+08:00 2020, state='1', discount=0.8}],profession=Profession{id=1, name='医护', rank='', type='0'},healthCard=HealthCard{custId=1, name='健康卡', state='0'}}
java application print order:Order{id=1, custId=1, create=Sat Nov 28 16:03:53 GMT+08:00 2020, goods=[Item{id=1, oId=1, good=Good{id=1, name='猪肉', categery='肉类', price=40.0, unit='kg'}, count=1.0, discount=8.0}, Item{id=2, oId=1, good=Good{id=2, name='大葱', categery='肉类', price=6.0, unit='kg'}, count=1.0, discount=1.2}, Item{id=3, oId=1, good=Good{id=3, name='老陈醋', categery='调料', price=6.9, unit='壶'}, count=1.0, discount=1.38}], total=52.9, pay=42.32, discount=10.58}
从执行结果上看,当通过drools.run()调用其他unit的时候当前unit的执行是被暂停的,等被调用的unit执行完成后,还会继续执行当前unit。这一点应该注意一下的。
2.data source
DateSource是rule unit 要处理的数据源,它是rule unit 计算的入口点。每个rule unit 可以声明多个data source,每个data source 表示rule unit executor 的一个入口点。多个rule unit也可以共享同一个data source,但是需要各rule unit各自声明自身的data source入口点。如上例中 DataSource<Object>。
rule unit是由RuleUnitExecutor绑定KieBase调用的,在不同的场景data source 或者全局变量有不同的绑定方式:
第一种方式:
//第一种,没什么特别之处,一般用这种就可以
DataSource<Object> data = DataSource.create(o,c);
MarketUnits unit = new MarketUnits();
unit.setData(data);
executor.run(unit);
第二种方式:
//第二种,直接将数据绑定到了executor上,这种是可以使facts在所有具有"data"和"ounpons"两个属性名或其中之一的rule unit 之间传递,且修改均可见;
//但是也有局限性,如上例中"data"属性是包含了多个(组)数据的,这种方式却只能给属性赋值一个实例对象。
executor.bindVariable("data",o).bindVariable("counpons",new ArrayList());
第三种方式:
//第三种,这种与第二种类似,也可以使 facts在具有相同属性名称的 rule unit 之间传递,并且可以将多个(组)赋值给DataSource;
//但是,它只能给DataSource赋值,其他变量仍需要使用bingdVariable方法赋值。
executor.newDataSource("data",o,c);
写到这里简单提一下drools 中的OOPath表达式(点击这里查看OOPath表达式),对于简化对象和对象嵌套的访问。使用OOPath表达式可以替代繁琐的 from 写法,例如上例中对订单明细的遍历访问可以写成
rule testUnit
when
//仅访问data中Customer实例
$c:/data#Customer
//仅访问data中Order实例
$o:/data#Order[custId == $c.id]
//"../id"表示获得上一层(Order实例的id属性值)
$i:/data#Order[custId == $c.id]/goods[oId == ../id]
then
System.out.println("OOPath print 成功了"+$i.getClass());
end
等价于
rule testUnit
when
$c:Customer() from data
$o:Order(custId == $c.id) from data
$i:Item(oId == $o.id) from $o.goods
then
System.out.println("OOPath print 成功了"+$i.getClass());
end
RuleUnit 中也可以使用@UnitVar注解给 属性名称设置别名,别名与RuleUnitExecutor 中绑定的参数名称保持一致同样可以获得数据,例如:
public class HealthUnits implements RuleUnit {
@UnitVar("data")
private DataSource<Object> data_1;//DataSource data_1设置别名data
@UnitVar("num")
private Integer num_1;//全局变量num_1设置别名num
private Integer maxNum;
}
3.rule units 间调用
rule unit通过 drools.run()或者drools.guard()方法调用其他rule unit。
drools.run():触发指定规则单元类的执行。此方法强制中断当前rule unit的执行,并激活另一个指定的rule unit。每个匹配数据都会强制中断,然后激活指定rule unit,这也就意味着被指定的rule unit 会多次启动。
drools.guard():触发指定规则单元类的执行。但是不会发生中断,当前rule unit的所有匹配数据处理完成,且当前rule unit 执行结束之后才会启动指定的rule unit ,且仅启动一次,这也是与drools.run()的不同之处。一个rule unit可以通过drools.guard()方法启动多个rule unit。
示例:
//售票窗口
public class BoxOffice {
private Boolean open;
}
//购票人
public class Person {
private Integer id;
private String name;
private Integer age;
}
//门票
public class Ticket {
private Integer id;
private Person person;
}
售票窗口 rule unit
package com.helloworld.bk.ruleUnits.units;
import com.helloworld.bk.ruleUnits.entity.BoxOffice;
import org.kie.api.runtime.rule.DataSource;
import org.kie.api.runtime.rule.RuleUnit;
public class BoxOfficeUnit implements RuleUnit {
private DataSource<BoxOffice> boxOffices;
public DataSource<BoxOffice> getBoxOffices() {
return boxOffices;
}
@Override
public void onStart() {
System.out.println("----------------BoxOfficeUnit 开始了");
}
@Override
public void onEnd() {
System.out.println("----------------BoxOfficeUnit 结束了");
}
@Override
public void onYield(RuleUnit other) {
System.out.println("----------------BoxOfficeUnit 跳转到了其他地方");
}
}
购票 rule unit
package com.helloworld.bk.ruleUnits.units;
import com.helloworld.bk.ruleUnits.entity.Person;
import com.helloworld.bk.ruleUnits.entity.Ticket;
import org.kie.api.runtime.rule.DataSource;
import org.kie.api.runtime.rule.RuleUnit;
import java.util.List;
public class TicketIssuerUnit implements RuleUnit {
private DataSource<Person> persons;
private DataSource<Ticket> tickets;
private List<String> results;
public TicketIssuerUnit() {
}
public TicketIssuerUnit(DataSource<Person> persons, DataSource<Ticket> tickets, List<String> result) {
this.persons = persons;
this.tickets = tickets;
this.results = result;
}
public DataSource<Person> getPersons() {
return persons;
}
public DataSource<Ticket> getTickets() {
return tickets;
}
public List<String> getResult() {
return results;
}
@Override
public void onStart() {
System.out.println("----------------TicketIssuerUnit 开始了");
}
@Override
public void onEnd() {
System.out.println("----------------TicketIssuerUnit 结束了");
}
@Override
public void onYield(RuleUnit other) {
System.out.println("----------------TicketIssuerUnit 跳转了");
}
}
DRL文件,规定至少有一个窗口开放时,允许年满18岁的客户购买门票
package com.helloworld.bk.ruleUnits.units;
unit BoxOfficeUnit
import com.helloworld.bk.ruleUnits.entity.*
rule "boxOfficeOpen"
when
$box:/boxOffices[open]
then
System.out.println("第一个执行了");
// drools.run(TicketIssuerUnit.class);
drools.guard(TicketIssuerUnit.class);
end
package com.helloworld.bk.ruleUnits.units;
unit TicketIssuerUnit
import com.helloworld.bk.ruleUnits.entity.*
rule issuerTicket
salience 1
when
$p:/persons[age >= 18]
then
System.out.println("符合购票要求");
tickets.insert(new Ticket($p));
end
rule registerTicket
when
$t:/tickets
then
System.out.println("购票登记");
result.add($t.getPerson().getName());
end
数据准备
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
KieBase kb = kc.getKieBase();
RuleUnitExecutor executor =RuleUnitExecutor.create().bind(kb);
DataSource<Person> persons = executor.newDataSource( "persons" );
DataSource<BoxOffice> boxOffices = executor.newDataSource( "boxOffices" );
DataSource<Ticket> tickets = executor.newDataSource( "tickets" );
List<String> list = new ArrayList<>();
executor.bindVariable( "results", list );
BoxOffice office1 = new BoxOffice(true);
FactHandle officeFH1 = boxOffices.insert( office1 );
BoxOffice office2 = new BoxOffice(true);
FactHandle officeFH2 = boxOffices.insert( office2 );
persons.insert(new Person(1,"John", 40));
executor.run(BoxOfficeUnit.class);
使用drools.guard()时,执行结果如下:
----------------BoxOfficeUnit 开始了
第一个执行了
第一个执行了
----------------BoxOfficeUnit 结束了
----------------TicketIssuerUnit 开始了
符合购票要求
购票登记
----------------TicketIssuerUnit 结束了
使用drools.run()时,执行结果如下:
----------------BoxOfficeUnit 开始了
第一个执行了
----------------BoxOfficeUnit 跳转到了其他地方
----------------TicketIssuerUnit 开始了
符合购票要求
购票登记
----------------TicketIssuerUnit 结束了
----------------BoxOfficeUnit 开始了
第一个执行了
----------------BoxOfficeUnit 跳转到了其他地方
----------------TicketIssuerUnit 开始了
----------------TicketIssuerUnit 结束了
----------------BoxOfficeUnit 开始了
----------------BoxOfficeUnit 结束了
对于调用drools.run()时的打印结果,可以看到 第二次启动TicketIssuerUnit,只有 “开始”和“结束”,这个原因应该是与drools 创建rule unit 实例都是单例有关,同时也涉及到drools 的工作机制(先将所有rule的conditions运行完毕,agenda再依据冲突策略排序再依次运行consequence);我在试验案例的时候怎么也调不出预期效果了,这个问题先留着,后续更新。
4. rule unit identity 冲突
drools 创建rule unit 实例都是单例,一个rule unit 是可以被drools.guard()或drools.run()多次启动的,这时候需要避免冲突(avoid identity conflicts),方法就是RuleUnit的实现类覆盖getUnitIdentity()方法。