本文节选自《疯狂工作流讲义(第2版)》
京东购买地址:https://item.jd.com/12246565.html
疯狂Activiti电子书:https://my.oschina.net/JavaLaw/blog/1570397
工作流Activiti教学视频:https://my.oschina.net/JavaLaw/blog/1577577
Activiti与Drools整合
使用Activiti中的业务规则任务(Business Rule Task)可以执行一个或者多个业务规则,当前Activiti只支持Drools。根据流程任务章节可知,每个流程活动都会有自己的行为,那么Activiti在实例业务规则任务行为的时候,只需要使用Drools的API,就可以实现规则文件的加载、事实实例的插入和规则触发等操作,任务的定义者只需要提供参数、规则和计算结果等信息,就可以在Activiti中调用规则。
业务规则任务详解
在调用规则前,需要告诉规则引擎加载哪些规则文件,而对于Activiti来说,这些文件都会被看作资源(数据被保存在ACT_GE_BYTEARRAY表中),因此在部署流程资源文件时,就需要提供这些规则文件。当执行流到达业务规则任务时,就会执行业务规则任务的行为,Activiti中对应的行为实现类是BusinessRuleTaskActivityBehavior,那么根据本章前面几节中Drools的API可以知道,这个类的实现应该是创建(获取缓存中的)KnowledgeBase实例,然后创建一个StatefulKnowledgeSession实例,插入事实实例,最后调用fireAllRules方法触发规则。BusinessRuleTaskActivityBehavior的实现大致如代码清单14-26所示。
代码清单14-26:
// 创建一个KnowledgeBuilder
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory
.newKnowledgeBuilder();
// 添加规则资源到 KnowledgeBuilder
kbuilder.add(ResourceFactory.newClassPathResource("rule/MyDrools.drl",
FirstTest.class), ResourceType.DRL);
if (kbuilder.hasErrors()) {
System.out.println(kbuilder.getErrors().toString());
System.exit(0);
}
// 获取知识包集合
Collection<KnowledgePackage> pkgs = kbuilder
.getKnowledgePackages();
// 创建KnowledgeBase实例
KnowledgeBase kbase = kbuilder.newKnowledgeBase(); ①
// 将知识包部署到KnowledgeBase中
kbase.addKnowledgePackages(pkgs);
// 使用KnowledgeBase创建StatefulKnowledgeSession
StatefulKnowledgeSession ksession = kbase
.newStatefulKnowledgeSession();
// 创建事实
Person p1 = new Person("person 1", 11);
// 插入到Working Memory
ksession.insert(p1);
// 匹配规则
ksession.fireAllRules();
// 关闭当前session的资源
ksession.dispose();
从代码清单14-26的①开始,将会是BusinessRuleTaskActivityBehavior所做的工作,Activiti的实现与代码清单14-26存在差异,KnowledgeBase实例的创建将由 Activiti的其他类完成,包括KnowledgeBuilder的创建、编译信息输出等工作,BusinessRuleTaskActivityBehavior的实现中,得到KnowledgeBase后,会创建一个StatefulKnowledgeSession,然后根据任务节点的配置,解析为事实实例,调用StatefulKnowledgeSession的insert方法插入到Working Memory中,最后会触发全部的规则并关闭资源。需要注意的是,触发规则时,会读取任务所配置的规则来添加一个规则拦截器,调用StatefulKnowledgeSession的fireAllRules(AgendaFilter filte)方法来触发规则,如果在任务中没有配置使用(或者不使用)的规则,那么将调用无参数的fireAllRules方法。在接下来的两个小节,将以一个销售流程为基础,在Activiti中调用规则。
制定销售单优惠规则
假设当前有一个销售流程,销售人员在录入销售商品后,系统需要对录入的商品进行规则处理,例如在单笔消费100元以上打九折、200元以上打八折等优惠策略,都可以在规则文件中定义,然后通过业务规则任务的调用,最后通过一个ServiceTask来输出计算后的结果。在设定销售流程前,可以先设计相应的销售对象。代码清单14-27为一个销售单对象和一个销售单明细对象。
代码清单14-27:
codes\14\14.7\drools-sale\src\org\crazyit\activiti\Sale.java,
codes\14\14.7\drools-sale\src\org\crazyit\activiti\SaleItem.java
// 销售单对象
public class Sale implements Serializable {
// 销售单号
private String saleCode;
// 销售日期
private Date date;
// 销售明细
private List<SaleItem> items;
//折扣
private BigDecimal discount = new BigDecimal(1);
public Sale(String saleCode, Date date) {
super();
this.saleCode = saleCode;
this.date = date;
this.items = new ArrayList<SaleItem>();
}
// 返回日期为星期几
public int getDayOfWeek() {
Calendar c = Calendar.getInstance();
c.setTime(this.date);
int dow = c.get(Calendar.DAY_OF_WEEK);
return dow;
}
// 返回该销售单的总金额(优惠前)
public BigDecimal getTotal() {
BigDecimal total = new BigDecimal(0);
for (SaleItem item : this.items) {
BigDecimal itemTotal = item.getPrice().multiply(item.getAmount());
total = total.add(itemTotal);
}
total = total.setScale(2, BigDecimal.ROUND_HALF_UP);
return total;
}
// 返回优惠后的总金额
public BigDecimal getDiscountTotal() {
BigDecimal total = getTotal();
total = total.multiply(this.discount).setScale(2, BigDecimal.ROUND_HALF_UP);
return total;
}
public void setDiscount(BigDecimal dicsount) {
this.discount = dicsount.setScale(2, BigDecimal.ROUND_HALF_UP);
}
public BigDecimal getDiscount() {
return this.discount;
}
...省略setter和getter方法
}
// 销售明细
public class SaleItem implements Serializable {
//商品名称
private String goodsName;
//商品单价
private BigDecimal price;
//数量
private BigDecimal amount;
public SaleItem(String goodsName, BigDecimal price, BigDecimal amount) {
super();
this.goodsName = goodsName;
this.price = price;
this.amount = amount;
}
...省略setter和getter方法
}
代码清单14-27中的Sale对象,表示在销售过程中产生的一笔交易,一张销售单中有多个销售明细,每个明细表示所销售的商品信息,包括商品名称、单价和数量。在代码清单14-27中,Sale对象提供了getDayOfWeek和getTotal方法,用于返回销售单日期是星期几和销售单总金额,这两个方法将会被规则的条件所调用,判断是否符合规则触发的条件,Sale对象中的getDiscountTotal方法,用于返回优惠后销售单的总金额,这个方法将会用于显示结果值。销售单中有一个discount的属性,用来标识销售单的打折情况。
编写规则文件
设计完事实对象后,就可以制定各种销售规则,只需要按照具体的业务和Drools的语法来制定规则。假设需要满足以下的销售规则:每周六和周日,全部商品打九折;消费满100打八折,满200打七折。根据该业务,设定的Drools规则如代码清单14-28所示。
代码清单14-28:codes\14\14.7\drools-sale\resource\rule\Sale.drl
package org.crazyit.activiti;
import java.util.*;
import java.math.*;
// 周六周日打九折
rule "Sat. and Sun. 90%"
no-loop true
lock-on-active true
salience 1
when
$s : Sale(getDayOfWeek() == 1 || getDayOfWeek() == 7)
then
$s.setDiscount(new BigDecimal(0.9));
update($s);
end
// 100元打八折
rule "100 80%"
no-loop true
lock-on-active true
salience 2
when
$s : Sale(getTotal() >= 100)
then
$s.setDiscount(new BigDecimal(0.8));
update($s);
end
// 200元打七折
rule "200 70%"
no-loop true
lock-on-active true
salience 3
when
$s : Sale(getTotal() >= 200)
then
$s.setDiscount(new BigDecimal(0.7));
update($s);
end
代码清单14-28中定义了三个规则,这三个规则都设置了no-loop和lock-on-active属性为true,表示一个规则被触发后,其他规则(包括自身)将不会被再次触发,三个规则中均设置了规则的优先级,200元打七折的优先级最高,周六周日打九折的规则优先级最低,如果一笔销售发生在周六日,同时也满200元的话,这时只会触发“200元打七折”的业务规则。代码清单中的三个规则,符合条件后,均会调用Sale的setDiscount方法设置销售单的折扣属性。
实现销售流程
制定了销售规则后,就可以在Activiti中设计销售流程,本例的销售流程较为简单,在销售员录入销售数据后(使用User Task),将数据交给业务规则任务(Business Rule Task)进行处理,最后使用一个简单的Service Task进行输出,流程结束,当然,在实际应用的过程中,会有更复杂的后续流程,但并不是本例的重点。本例设计的销售流程如图14-3所示,对应的流程文件内容为代码清单14-28。
图14-3 销售流程
代码清单14-30:codes\14\14.7\drools-sale\resource\bpmn\SaleRule.bpmn
<process id="process1" name="process1">
<startEvent id="startevent1" name="Start"></startEvent>
<businessRuleTask id="businessruletask1" name="进行优惠策略应用"
activiti:ruleVariablesInput="${sale1}, ${sale2}, ${sale3}, ${sale4}"
activiti:resultVariable="saleResults"></businessRuleTask>
…省略其他元素
</process>
代码清单14-30的粗体字代码,使用了businessRuleTask,该任务中会以四个流程参数(sale1到sale4)作为规则事实,交给规则引擎进行处理,最终返回结果的名称为“saleResults”,结果类型是一个集合。本例中的四个Sale流程参数,为代码清单14-25中的Sale对象,需要匹配的规则为代码清单14-26的规则(周六日打九折、100元以上打八折、200元以上打七折)。为了让流程引擎能加载规则文件(drl),需要在资源部署时将规则文件一并部署到流程引擎中,流程的部署以及运行,如代码清单14-31所示。
代码清单14-31:codes\14\14.7\drools-sale\src\org\crazyit\activiti\SaleProcess.java
public static void main(String[] args) {
// 创建流程引擎
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 得到流程存储服务组件
RepositoryService repositoryService = engine.getRepositoryService();
// 得到运行时服务组件
RuntimeService runtimeService = engine.getRuntimeService();
// 得到任务服务组件
TaskService taskService = engine.getTaskService();
// 部署流程文件
repositoryService.createDeployment()
.addClasspathResource("rule/Sale.drl")
.addClasspathResource("bpmn/SaleRule.bpmn").deploy();
ProcessInstance pi = runtimeService
.startProcessInstanceByKey("process1");
// 创建事实实例,符合周六日打九折条件
Sale s1 = new Sale("001", createDate("2017-07-01")); ①
SaleItem s1Item1 = new SaleItem("矿泉水", new BigDecimal(5),
new BigDecimal(4));
s1.addItem(s1Item1);
// 满100打八折
Sale s2 = new Sale("002", createDate("2017-07-03")); ②
SaleItem s2Item1 = new SaleItem("爆米花", new BigDecimal(20),
new BigDecimal(5));
s2.addItem(s2Item1);
// 满200打七折
Sale s3 = new Sale("003", createDate("2017-07-03")); ③
SaleItem s3Item1 = new SaleItem("可乐一箱", new BigDecimal(70), new BigDecimal(3));
s3.addItem(s3Item1);
// 星期天满200
Sale s4 = new Sale("004", createDate("2017-07-02")); ④
SaleItem s4Item1 = new SaleItem("爆米花一箱", new BigDecimal(80), new BigDecimal(3));
s4.addItem(s4Item1);
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("sale1", s1);
vars.put("sale2", s2);
vars.put("sale3", s3);
vars.put("sale4", s4);
// 查找任务
Task task = taskService.createTaskQuery().processInstanceId(pi.getId())
.singleResult();
taskService.complete(task.getId(), vars);
}
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 根据字符串创建日期对象
static Date createDate(String date) {
try {
return sdf.parse(date);
} catch (Exception e) {
throw new RuntimeException("parse date error: " + e.getMessage());
}
}
代码清单14-31中的粗体字代码,除了正常部署流程文件(.bpmn)外,还将一份Sale.drl部署到流程引擎中,该份文件内容与代码清单14-26内容一致。本例中创建了4个Sale对象,代码清单14-31中的①创建了第一个销售单实例,该实例将会满足周六日打九折的条件。②创建的Sale对象,总金额等于100元,符合满100元打八折的条件。③创建的Sale对象,总金额为210元,符合满200打七折的条件。④创建的Sale对象,总金额为240元,并且发生在周日,即同时满足两个规则的条件,但是根据代码清单14-26中的规则,200元打七折的规则比周六日打九折的规则优先级高,因此可以知道,第四个Sale对象只会触发“200元打七折”的规则。如果需要成功运行代码清单14-31,还需要配置activiti.cfg.xml,为其加入规则文件的部署实现类,本例中activiti.cfg.xml的配置如下:
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
…省略其他元素
<property name="customPostDeployers">
<list>
<bean class="org.activiti.engine.impl.rules.RulesDeployer" />
</list>
</property>
</bean>
以上配置的粗体部分为新加入的规则部署者。在整个销售流程中,当业务规则任务完成后,执行流会到达一个Service Task,在本例中,这个Service Task仅仅用于将规则处理后的销售单结果输出,Service Task的实现如代码清单14-32所示。
代码清单14-32:
codes\14\14.7\drools-sale\src\org\crazyit\activiti\SaleJavaDelegate.java
public class SaleJavaDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) {
Collection sales = (Collection) execution.getVariable("saleResults");
System.out.println("输出处理结果:");
for (Object obj : sales) {
Sale sale = (Sale) obj;
System.out.println("销售单:" + sale.getSaleCode() + " 原价:"
+ sale.getTotal() + " 优惠后:" + sale.getDiscountTotal()
+ " 折扣:" + sale.getDiscount());
}
}
}
在流程最后的Service Task中,得到业务规则任务处理后的结果(一个集合),然后对集合进行遍历,强制类型转换为Sale对象,然后将Sale的各个信息输出。运行代码清单14-31,最终输出如下:
输出处理结果:
输出处理结果:
销售单:002 原价:100.00 优惠后:80.00 折扣:0.80
销售单:001 原价:20.00 优惠后:18.00 折扣:0.90
销售单:004 原价:240.00 优惠后:168.00 折扣:0.70
销售单:003 原价:210.00 优惠后:147.00 折扣:0.70
根据结果可知,相应的Sale对象均按预期匹配到不同的规则,销售单001打了九折,销售单002打了八折,销售单003打了七折,销售单004打了七折。
本文节选自《疯狂工作流讲义(第2版)》
京东购买地址:https://item.jd.com/12246565.html
疯狂Activiti电子书:https://my.oschina.net/JavaLaw/blog/1570397
工作流Activiti教学视频:https://my.oschina.net/JavaLaw/blog/1577577
本书代码目录:https://gitee.com/yangenxiong/CrazyActiviti