flowable是一个用Java写的轻量级商业流程引擎,用它可以部署BPMN2.0(在工业界被广泛接受的XML标准)流程定义, 并且可以创建流程实例,驱动节点流转,存储相关的历史数据等等。可能更多人先是听说过activiti, 但是flowable实际上是activiti的主要成员在activiti上fork一个新的分支,添加了许多新的特性,也更加稳定实用,感兴趣的可以去了解一下这段历史。本文主要基于flowable官方文档演示如何去用flowable来走通如下的一个请假流程。
1. 创建项目
首先我们创建一个maven项目,引入以下依赖:
- 采用最新版本的flowable流程引擎,
- flowable存储数据默认采用h2内存数据库,但我这里还是用熟悉的mysql
- flowable内部采用SLF4J作为其日志框架
- 在这里我们用log4j日志实现
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-engine</artifactId>
<version>6.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
Log4j需要一个属性文件来进行相关配置,因此需要在src/main/resources
添加具有以下内容的log4j.properties
文件:
log4j.rootLogger=DEBUG, CA
log4j.appender.CA=org.apache.log4j.ConsoleAppender
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
log4j.appender.CA.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n
2. 创建流程引擎
首先,我们需要做的是初始化一个流程引擎(ProcessEngine)实例。它是一个线程安全的对象在一个应用中也只需要初始化一次, 而它又由流程引擎配置(ProceeEngineConfiguration)创建,在ProcessEngineconfiguration中可以配置和调整省流程引擎。通常情况下ProcessEngineConfiguration是通过配置文件来配置的,但是也可以在程序中创建。当然,最少情况下的流程引擎配置也需要一个JDBC连接到数据库。
public class HolidayRequest {
public static void main(String[] args) {
ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
.setJdbcUrl("jdbc:mysql://localhost:3306/flowable?useUnicode=true&zeroDateTimeBehavior=convertToNull")
.setJdbcUsername("root")
.setJdbcPassword("123456")
.setJdbcDriver("com.mysql.jdbc.Driver")
.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
ProcessEngine processEngine = cfg.buildProcessEngine();
}
}
在上面的代码中,创建了一个独立的配置对象,这里的‘独立’是指引擎只由它自己创建和使用(如果是在Spring环境中,则要用SpringProcessEngineConfiguration来替代).该对象先配置了一个数据库连接(注意先要创建flowable数据库),最后的配置表示数据库不存在的话就要去创建它。运行该段代码后我们就可以看到数据库中新增了许多表格,如下所示:
act_ru_
: 表示的是一些流程实例运行(runtime)时的数据
act_hi_
: 表示存储的是一些历史(history)数据
act_id_
: 表示存储的是一些与用户身份(identity)相关的数据
act_re_
: 表示的是一些流程定义(repository)相关的数据
act_ge_
: 表示的是一些一般(general)数据
act_evt_log
表示事件日志,act_prodef_info
表示的是流程定义的一些信息。flowable还有一些其它模块中的表,到后面介绍。
3. 部署流程定义
如开篇那张图所示,我们需要实现的是一个很简单的请假流程, 表示员工的请假首先需要经理审批,经理拒绝则发邮件通知到申请人并结束该该流程,经理同意的话先注册到外部系统,然后通知给申请人进行销假,然后结束。该流程定义可以作为一个模板,每个需要请假的员工可以创建一个流程实例。当员工提供一些信息(如姓名, 请假天数和描述)就可以开始该流程。下面解释一下图中每类元素的意思:
- 开始事件:图中用细线圆圈来表示,是流程实例的开始点
- 箭头:表示节点之间的流转指向。
- 用户任务: 在图中用左上角有人的圆角矩形表示,这些是需要用户来操作的节点。图中有两个,第一个表示需要经理进行审批来同意或拒绝, 第二个表示用户来确认销假。
- 排它网关: 用叉形符号填充的菱形表示,从该图中出来的箭头往往有多个,但只有一个满足条件,流程会沿着满足条件的方向流转。
- 自动化任务 : 左上角有齿轮形状的的圆角矩形,表示自动执行的节点。图中上面的表示请假被经理同意后自动注册通知到外部系统,下面的表示请假被经理拒绝后自动发邮件通知给申请人。
- 结束事件: 图中用粗线圆圈表示,表示流程的结束。图中上面的结束事件表示请假成功结束,下面的表示请假失败结束。
Flowable引擎需要的是一个遵循BPMN2.0标准的XML文档。BPMN2.0标准在工业界被广泛认可, 它对流程中每个节点以及节点与节点之间的流转定义了一套简单清晰易用的标准,并且节点即可以是个人任务也可以是自动化任务等, 因此大家可以用都能理解的方式进行商业化的流程门交流。通常我们可以用一些工具来画出流程图然后生成BPMN2.0文档,但是在这里我们为了熟悉该标准的格式不审采用手动定义的方式。在src/main/resources文件夹下添加名称为
holiday-request.bpmn20.xml
内容如下的文件:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
xmlns:flowable="http://flowable.org/bpmn"
typeLanguage="http://www.w3.org/2001/XMLSchema"
expressionLanguage="http://www.w3.org/1999/XPath"
targetNamespace="http://www.flowable.org/processdef">
<process id="holidayRequest" name="Holiday Request" isExecutable="true">
<startEvent id="startEvent"/>
<sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>
<sequenceFlow sourceRef="approveTask" targetRef="decision"/>
<exclusiveGateway id="decision"/>
<sequenceFlow sourceRef="decision" targetRef="externalSystemCall">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[
${approved}
]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="decision" targetRef="sendRejectionMail">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[
${!approved}
]]>
</conditionExpression>
</sequenceFlow>
<serviceTask id="externalSystemCall" name="Enter holidays in external system"
flowable:class="com.hebaohua.workflow.delegate.CallExternalSystemDelegate"/>
<sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>
<userTask id="holidayApprovedTask" name="Holiday approved" flowable:assignee="${employee}"/>
<sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>
<serviceTask id="sendRejectionMail" name="Send out rejection email"
flowable:class="com.hebaohua.workflow.delegate.CallExternalSystemDelegate"/>
<sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>
<endEvent id="approveEnd"/>
<endEvent id="rejectEnd"/>
</process>
</definitions>
XML文件中最开始几行是一些兼容BPMN2.0标准的约束,每个流程定义都一样。
每一个activity都有一个id属性作为唯一性标识, name属性是为了增加可读性的可选项。
连接activitys的有向箭头称之为sequence flow,有一个起始点和目标点。从排它网关出来的箭头比较特别,这两个都有一个条件标签,表示只有满足里面条件时才会走这条路线。条件表达式${approved}
实际上是${approved == true}
的缩写,approved
被称之为流程变量。流程变量会随着流程实例进行持久化存储并且在整个流程实例的生命周期中都可以被用到, 因此我们必须在用到它之前的某个点对其进行赋值(本例中是当经理的用户任务完成时对approved进行true或false赋值),而不是在流程实例一开始就对其赋值。
现在我们有了流程定义的BPMN2.0 XML文件,下一步就是要把它部署到引擎中。部署流程引擎也就意味着:
- 流程引擎会把XML文件存储在数据库中,因此可以需要的时候查询到它
- 流程定义被解析为一个内部可执行的对象模型,所以我们可以以它为模板启动一个流程实例
部署流程定义到Flowable引擎需要用RepositoryService,可以在ProcessEngine对象中得到它。通过RepositoryService, 指定XML文件位置并调用deploy()方法后即可执行部署这个过程。
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("holiday-request.bpmn20.xml")
.deploy();
通过如下打印的日志可知,在部署的过程中分别在ACT_RE_PROCDEF, ACT_RE_DEPLOYMENT, ACT_GE_BYTEARRAY中插入了相关的记录。
现在我们就可以通过RepositoryService创建的ProcessDefinitionQuery查询到相关的流程定义信息了,如下代码:
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.singleResult();
System.out.println("Found process definition : " + processDefinition.getName());
4. 启动流程实例
现在我们有了流程定义,以它为模板就可启动一个流程实例了。
在启动流程实之前,我们需要提供一些流程变量作为输入。通常通过表单或REST API让用户来填写这些信息,这里为了方便直接定定义相关变量放在Map中。
下一步就需要通过RuntimeService(同样由processEngine产生)启动流程了。如下代码所示,通过key来启动流程实例,这里的key对应BPMN2.0 XML文件中的process id属性,本例中也就是holidayRequest
。
RuntimeService runtimeService = processEngine.getRuntimeService();
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employee", "jack");
variables.put("nrOfHolidays", 3);
variables.put("description", "回家看看");
ProcessInstance processInstance =
runtimeService.startProcessInstanceByKey("holidayRequest", variables);
如下日志,可以看到插入了15条记录,分别是ProcessInstance, Execution, IdentityLInk, Variable, Task相关的运行和历史数据,Execution和Task是运行时的核心表。注意在这一步流程通过启动事件已经到达manager审批结点了,因此会插入两条HistoryActivityInstanceEntity记录。
5. 编写JavaDelegate实现自动化任务
现在虽然任务到了第一个用户任务节点,但该任务节点执行后就会走自动化任务,在XML文件中我们定义对应的类为com.hebaohua.flowable.delegate.CallExternalSystemDelegate
, 如果找不到该类程序流程引擎就会报异常,所以我们先看看如何编写这样的一个自动化执行类。
很简单,实现org.flowable.engine.delegate.JavaDelegate
接口及其execute方法。通过得到的DelegateExecution
对象我们可以得到流程中所有变量及相关信息,因此想要做什么就在这个方法里来实现。如下代码只是一个简单演示:
public class CallExternalSystemDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) {
System.out.println("Calling the external system for employee "
+ execution.getVariable("employee"));
}
}
6. 查询并完成任务
在一个实际的应用中,用户通过登录系统可以以界面的方式查看他们的任务列表,并且可以看到流程实例的数据(流程运行状态,流程变量等), 以此来对该任务作相应处理。在本例中,我们通过API调用来模拟这个过程。
现在流程运行到了第一个用户任务, 在上面的XML文件中我们指定的完成该用户的为"managers"这个分组,首先我们可以通过TaskService创建TaskQuery对象来查询managers分组下的任务。如下:
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
System.out.println("You have " + tasks.size() + " tasks:");
for (int i=0; i<tasks.size(); i++) {
System.out.println((i+1) + ") " + tasks.get(i).getName());
}
Task task = tasks.get(0);
Map<String, Object> processVariables = taskService.getVariables(task.getId());
System.out.println(processVariables.get("employee") + " wants " +
processVariables.get("nrOfHolidays") + " of holidays. Do you approve this?");
返回:
You have 1 tasks:
1) Approve or reject request
jack wants 3 of holidays. Do you approve this?
现在managers可以完成这个任务了。同样的在完成任务时,虽然现实中是通过用户界面的表单来完成,在这里通过传递包含approved
变量(这个变量很重要,决定后面流程的路线, 这里我们赋值为true)的map来进行。
variables = new HashMap<String, Object>(); variables.put("approved", true); taskService.complete(task.getId(), variables);
通过执行上面的代码managers完成了该任务,细心的你肯定在打印出的日志中发现了如下红框中的内容。对,没错,这就是自动化任务节点中打印出来的。也就是说,现在自动化任务节点已经完成,流程已经到达第二个用户任务了。
同样的,根据XML中的定义, 我们知道第二个用户应该由jack自己(
${employee}
对就流程变量的值)来完成,通过以下代码即可查出jack需要完成的任务。
List<Task> tasks = taskService.createTaskQuery().taskCandidateOrAssigned("jack").list();
用和managers同样的方法,jack就可以完成任务。至此,流程已经到达结束事件,整个流程也就走完了。
7. 查询历史数据
Flowable为我们存储了丰富的流程实例执行的历史数据。例如我们想看到流程实例每个节点耗时情况,首先通过ProcessEngine获得HistoryService对象并且创建一个查询作如下限制:
- 查询待定流程实例的节点情况
- 只查看已完成的节点
结果会按照结束时间排序,也就是执行顺序:
HistoryService historyService = processEngine.getHistoryService();
List<HistoricActivityInstance> activities =
historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstance.getId())
.finished()
.orderByHistoricActivityInstanceEndTime().asc()
.list();
for (HistoricActivityInstance activity : activities) {
System.out.println(activity.getActivityId() + " took "
+ activity.getDurationInMillis() + " milliseconds");
}
输出:
startEvent took 3 milliseconds
approveTask took 3234 milliseconds
decision took 16 milliseconds
externalSystemCall took 4 milliseconds
holidayApprovedTask took 5888 milliseconds
approveEnd took 2 milliseconds
也就是对应数据库表act_hi_actinst
中的如下记录:
全文完。。。