本文节选自《疯狂Workflow讲义(第2版)》
疯狂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打了七折。
本文节选自《疯狂Workflow讲义(第2版)》
疯狂Activiti电子书:https://my.oschina.net/JavaLaw/blog/1570397
工作流Activiti教学视频:https://my.oschina.net/JavaLaw/blog/1577577
本书代码目录:https://gitee.com/yangenxiong/CrazyActiviti

