1. 简介
1.1. 协议
Flowable在Apache V2 协议下发布。
1.3. 源码
Flowable的发布包里包含了大部分源码,以JAR文件方式提供。Flowable的源码也可以通过以下链接获得: https://github.com/flowable/flowable-engine
1.4. 必要的软件
1.4.1. JDK 8+
运行Flowable需要JDK 8或以上版本。可以访问 Oracle Java SE downloads页面 点击“Download JDK”按钮获取。该页面上也有安装指导。安装完成后,可以执行 java -version
。能看到JDK的版本信息就说明安装成功了。
1.4.2. IDE
可以自行选择用于Flowable开发的IDE。如果想要使用Flowable Designer,则需要Eclipse Mars或Neon。
到 Eclipse下载页面选择Eclipse版本并下载。解压下载的文件,
然后执行eclipse
文件夹下的eclipse文件。手册后续有专门一章介绍如何安装我们的Eclipse Designer插件。
1.6. 实验性功能
标记有[实验性]的章节介绍的功能还不够稳定。
.impl.
包下的类都是内部实现类,不保证稳定。但是,在用户手册中作为配置参数介绍的类则是被官方支持的,可以保证稳定。
1.7. 内部实现类
在JAR文件中,所有.impl.
包下的类(比如org.flowable.engine.impl.db
)都是实现类,只应在内部使用。实现类中的所有类或接口都不保证稳定。
1.8. 版本策略
使用三个整数的形式标记版本:MAJOR.MINOR.MICRO。其中 MAJOR版本代表核心引擎的演进。MINOR版本代表新功能与新API。MICRO版本代表bug修复与改进。
总的来说,Flowable希望在MINOR与MICRO版本中,对所有非内部实现类保持“源代码兼容性”,即应用可以正确构建,且不改变语义。Flowable也希望在MINOR与MICRO版本中,保持“二进制兼容性”,即用新版本的Flowable直接替换老版本的Jar文件,仍然可以正常工作。
如果在MINOR版本中修改了API,将保留原有版本,并使用@Deprecated注解。这种废弃的API将在两个MINOR版本之后移除。
1.9. 翻译说明
由 TKJohn 根据 官方userguide源码 翻译至简体中文,工程发布于 GitHub ,任何意见建议欢迎提issue。
页面发布链接为 https://tkjohn.github.io/
2. 开始
2.1. Flowable是什么?
Flowable是一个使用Java编写的轻量级业务流程引擎。Flowable流程引擎可用于部署BPMN 2.0流程定义(用于定义流程的行业XML标准), 创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。这个章节将用一个可以在你自己的开发环境中使用的例子,逐步介绍各种概念与API。
Flowable可以十分灵活地加入你的应用/服务/构架。可以将JAR形式发布的Flowable库加入应用或服务,来嵌入引擎。 以JAR形式发布使Flowable可以轻易加入任何Java环境:Java SE;Tomcat、Jetty或Spring之类的servlet容器;JBoss或WebSphere之类的Java EE服务器,等等。 另外,也可以使用Flowable REST API进行HTTP调用。也有许多Flowable应用(Flowable Modeler, Flowable Admin, Flowable IDM 与 Flowable Task),提供了直接可用的UI示例,可以使用流程与任务。
所有使用Flowable方法的共同点是核心引擎。核心引擎是一组服务的集合,并提供管理与执行业务流程的API。 下面的教程从设置与使用核心引擎的介绍开始。后续章节都建立在之前章节中获取的知识之上。
-
第一节展示了以最简单的方式运行Flowable的方法:只使用Java SE的标准Java main方法。这里也会介绍许多核心概念与API。
-
Flowable REST API章节展示了如何通过REST运行及使用相同的API。
-
Flowable APP章节将介绍直接可用的Flowable UI示例的基本方法。
2.2. Flowable与Activiti
Flowable是Activiti(Alfresco持有的注册商标)的fork。在下面的章节中,你会注意到包名,配置文件等等,都使用flowable。
2.3. 构建命令行程序
2.3.1. 创建流程引擎
在这个初步教程中,将构建一个简单的例子,以展示如何创建一个Flowable流程引擎,介绍一些核心概念,并展示如何使用API。 截图时使用的是Eclipse,但实际上可以使用任何IDE。我们使用Maven获取Flowable依赖及管理构建,但是类似的任何其它方法也都可以使用(Gradle,Ivy,等等)。
我们将构建的例子是一个简单的请假(holiday request)流程:
-
雇员(employee)申请几天的假期
-
经理(manager)批准或驳回申请
-
我们会模拟将申请注册到某个外部系统,并给雇员发送结果邮件
首先,通过File → New → Other → Maven Project创建一个新的Maven项目
在下一界面中,选中'create a simple project (skip archetype selection)'
填入'Group Id'与'Artifact id':
这样就建立了空的Maven项目,然后添加两个依赖:
-
Flowable流程引擎。使我们可以创建一个ProcessEngine流程引擎对象,并访问Flowable API。
-
一个内存数据库。本例中为H2,因为Flowable引擎在运行流程实例时,需要使用数据库来存储执行与历史数据。 请注意H2依赖包含了数据库及驱动。如果使用其他数据库(例如PostgreSQL,MySQL等),需要添加对应的数据库驱动依赖。
在pom.xml文件中添加下列行:
1
2
3
4
5
6
7
8
9
10
11
12 <dependencies>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-engine</artifactId>
<version>6.3.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.3.176</version>
</dependency>
</dependencies>
如果由于某些原因,依赖JAR无法自动获取,可以右键点击项目,并选择'Maven → Update Project'以强制手动刷新(一般不会需要这么操作)。 在这个项目中的'Maven Dependencies'下,可以看到flowable-engine与许多其他(传递的)依赖。
创建一个新的Java类,并添加标准的Java main方法:
1
2
3
4
5
6
7
8
9 package org.flowable;
public class HolidayRequest {
public static void main(String[] args) {
}
}
首先要做的是初始化ProcessEngine流程引擎实例。这是一个线程安全的对象,因此通常只需要在一个应用中初始化一次。 ProcessEngine由ProcessEngineConfiguration实例创建。该实例可以配置与调整流程引擎的设置。 通常使用一个配置XML文件创建ProcessEngineConfiguration,但是(像在这里做的一样)也可以编程方式创建它。 ProcessEngineConfiguration所需的最小配置,是数据库JDBC连接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.flowable;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;
public class HolidayRequest {
public static void main(String[] args) {
ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
.setJdbcUrl("jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1")
.setJdbcUsername("sa")
.setJdbcPassword("")
.setJdbcDriver("org.h2.Driver")
.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
ProcessEngine processEngine = cfg.buildProcessEngine();
}
}
在上面的代码中,第10行创建了一个独立(standalone)配置对象。这里的'独立'指的是引擎是完全独立创建及使用的(而不是在Spring环境中使用,这时需要使用SpringProcessEngineConfiguration类代替)。第11至14行中,传递了一个内存H2数据库实例的JDBC连接参数。 重要:请注意这样的数据库在JVM重启后会消失。如果需要永久保存数据,需要切换为持久化数据库,并相应切换连接参数。 第15行中,设置了true,确保在JDBC参数连接的数据库中,数据库表结构不存在时,会创建相应的表结构。 另外,Flowable也提供了一组SQL文件,可用于手动创建所有表的数据库表结构。
然后使用这个配置创建ProcessEngine对象(第17行)。
这样就可以运行了。在Eclipse中最简单的方法是右键点击类文件,选择Run As → Java Application :
应用运行没有问题,但也没有在控制台提供有用的信息,只有一条消息提示日志没有正确配置:
Flowable使用SLF4J作为内部日志框架。在这个例子中,我们使用log4j作为SLF4J的实现。因此在pom.xml文件中添加下列依赖:
1
2
3
4
5
6
7
8
9
10 <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.3.2. 部署流程定义
我们要构建的流程是一个非常简单的请假流程。Flowable引擎需要流程定义为BPMN 2.0格式,这是一个业界广泛接受的XML标准。 在Flowable术语中,我们将其称为一个流程定义(process definition)。一个流程定义可以启动多个流程实例(process instance)。流程定义可以看做是重复执行流程的蓝图。 在这个例子中,流程定义定义了请假的各个步骤,而一个流程实例对应某个雇员提出的一个请假申请。
BPMN 2.0存储为XML,并包含可视化的部分:使用标准方式定义了每个步骤类型(人工任务,自动服务调用,等等)如何呈现,以及如何互相连接。这样BPMN 2.0标准使技术人员与业务人员能用双方都能理解的方式交流业务流程。
我们要使用的流程定义为:
这个流程应该已经十分自我解释了。但为了明确起见,说明一下几个要点:
-
我们假定启动流程需要提供一些信息,例如雇员名字、请假时长以及说明。当然,这些可以单独建模为流程中的第一步。 但是如果将它们作为流程的“输入信息”,就能保证只有在实际请求时才会建立一个流程实例。否则(将提交作为流程的第一步),用户可能在提交之前改变主意并取消,但流程实例已经创建了。 在某些场景中,就可能影响重要的指标(例如启动了多少申请,但还未完成),取决于业务目标。
-
左侧的圆圈叫做启动事件(start event)。这是一个流程实例的起点。
-
第一个矩形是一个用户任务(user task)。这是流程中人类用户操作的步骤。在这个例子中,经理需要批准或驳回申请。
-
取决于经理的决定,排他网关(exclusive gateway) (带叉的菱形)会将流程实例路由至批准或驳回路径。
-
如果批准,则需要将申请注册至某个外部系统,并跟着另一个用户任务,将经理的决定通知给申请人。当然也可以改为发送邮件。
-
如果驳回,则为雇员发送一封邮件通知他。
一般来说,这样的流程定义使用可视化建模工具建立,如Flowable Designer(Eclipse)或Flowable Web Modeler(Web应用)。
但在这里我们直接撰写XML,以熟悉BPMN 2.0及其概念。
与上面展示的流程图对应的BPMN 2.0 XML在下面显示。请注意这只包含了“流程部分”。如果使用图形化建模工具,实际的XML文件还将包含“可视化部分”,用于描述图形信息,如流程定义中各个元素的坐标(所有的图形化信息包含在XML的BPMNDiagram标签中,作为definitions标签的子元素)。
将下面的XML保存在src/main/resources文件夹下名为holiday-request.bpmn20.xml的文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 <?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"/>
<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="org.flowable.CallExternalSystemDelegate"/>
<sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>
<userTask id="holidayApprovedTask" name="Holiday approved"/>
<sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>
<serviceTask id="sendRejectionMail" name="Send out rejection email"
flowable:class="org.flowable.SendRejectionMail"/>
<sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>
<endEvent id="approveEnd"/>
<endEvent id="rejectEnd"/>
</process>
</definitions>
第2至11行看起来挺吓人,但其实在大多数的流程定义中都是一样的。这是一种样板文件,需要与BPMN 2.0标准规范完全一致。
每一个步骤(在BPMN 2.0术语中称作活动(activity))都有一个id属性,为其提供一个在XML文件中唯一的标识符。所有的活动都可以设置一个名字,以提高流程图的可读性。
活动之间通过顺序流(sequence flow)连接,在流程图中是一个有向箭头。在执行流程实例时,执行(execution)会从启动事件沿着顺序流流向下一个活动。
离开排他网关(带有X的菱形)的顺序流很特别:都以表达式(expression)的形式定义了条件(condition) (见第25至32行)。当流程实例的执行到达这个网关时,会计算条件,并使用第一个计算为true的顺序流。这就是排他的含义:只选择一个。当然如果需要不同的路由策略,可以使用其他类型的网关。
这里用作条件的表达式为${approved},这是${approved == true}的简写。变量’approved’被称作流程变量(process variable)。流程变量是持久化的数据,与流程实例存储在一起,并可以在流程实例的生命周期中使用。在这个例子里,我们需要在特定的地方(当经理用户任务提交时,或者以Flowable的术语来说,完成(complete)时)设置这个流程变量,因为这不是流程实例启动时就能获取的数据。
现在我们已经有了流程BPMN 2.0 XML文件,下来需要将它部署(deploy)到引擎中。部署一个流程定义意味着:
-
流程引擎会将XML文件存储在数据库中,这样可以在需要的时候获取它。
-
流程定义转换为内部的、可执行的对象模型,这样使用它就可以启动流程实例。
将流程定义部署至Flowable引擎,需要使用RepositoryService,其可以从ProcessEngine对象获取。使用RepositoryService,可以通过XML文件的路径创建一个新的部署(Deployment),并调用deploy()方法实际执行:
1
2
3
4 RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("holiday-request.bpmn20.xml")
.deploy();
我们现在可以通过API查询验证流程定义已经部署在引擎中(并学习一些API)。通过RepositoryService创建的ProcessDefinitionQuery对象实现。
1
2
3
4 ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.singleResult();
System.out.println("Found process definition : " + processDefinition.getName());
2.3.3. 启动流程实例
现在已经在流程引擎中部署了流程定义,因此可以使用这个流程定义作为“蓝图”启动流程实例。
要启动流程实例,需要提供一些初始化流程变量。一般来说,可以通过呈现给用户的表单,或者在流程由其他系统自动触发时通过REST API,来获取这些变量。在这个例子里,我们简化为使用java.util.Scanner类在命令行输入一些数据:
1
2
3
4
5
6
7
8
9
10 Scanner scanner= new Scanner(System.in);
System.out.println("Who are you?");
String employee = scanner.nextLine();
System.out.println("How many holidays do you want to request?");
Integer nrOfHolidays = Integer.valueOf(scanner.nextLine());
System.out.println("Why do you need them?");
String description = scanner.nextLine();
截下来,我们使用RuntimeService启动一个流程实例。收集的数据作为一个java.util.Map实例传递,其中的键就是之后用于获取变量的标识符。这个流程实例使用key启动。这个key就是BPMN 2.0 XML文件中设置的id属性,在这个例子里是holidayRequest。
(请注意:除了使用key之外,在后面你还会看到有很多其他方式启动一个流程实例)
<process id="holidayRequest" name="Holiday Request" isExecutable="true">
1
2
3
4
5
6
7
8 RuntimeService runtimeService = processEngine.getRuntimeService();
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employee", employee);
variables.put("nrOfHolidays", nrOfHolidays);
variables.put("description", description);
ProcessInstance processInstance =
runtimeService.startProcessInstanceByKey("holidayRequest", variables);
在流程实例启动后,会创建一个执行(execution),并将其放在启动事件上。从这里开始,这个执行沿着顺序流移动到经理审批的用户任务,并执行用户任务行为。这个行为将在数据库中创建一个任务,该任务可以之后使用查询找到。用户任务是一个等待状态(wait state),引擎会停止执行,返回API调用处。
2.3.4. 另一个话题:事务
在Flowable中,数据库事务扮演了关键角色,用于保证数据一致性,并解决并发问题。当调用Flowable API时,默认情况下,所有操作都是同步的,并处于同一个事务下。这意味着,当方法调用返回时,会启动并提交一个事务。
流程启动后,会有一个数据库事务从流程实例启动时持续到下一个等待状态。在这个例子里,指的是第一个用户任务。当引擎到达这个用户任务时,状态会持久化至数据库,提交事务,并返回API调用处。
在Flowable中,当一个流程实例运行时,总会有一个数据库事务从前一个等待状态持续到下一个等待状态。数据持久化之后,可能在数据库中保存很长时间,甚至几年,直到某个API调用使流程实例继续执行。请注意当流程处在等待状态时,不会消耗任何计算或内存资源,直到下一次APi调用。
在这个例子中,当第一个用户任务完成时,会启动一个数据库事务,从用户任务开始,经过排他网关(自动逻辑),直到第二个用户任务。或通过另一条路径直接到达结束。
2.3.5. 查询与完成任务
在更实际的应用中,会为雇员及经理提供用户界面,让他们可以登录并查看任务列表。其中可以看到作为流程变量存储的流程实例数据,并决定如何操作任务。在这个例子中,我们通过执行API调用来模拟任务列表,通常这些API都是由UI驱动的服务在后台调用的。
我们还没有为用户任务配置办理人。我们想将第一个任务指派给"经理(managers)"组,而第二个用户任务指派给请假申请的提交人。因此需要为第一个任务添加candidateGroups属性:
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>
并如下所示为第二个任务添加assignee属性。请注意我们没有像上面的’managers’一样使用静态值,而是使用一个流程变量动态指派。这个流程变量是在流程实例启动时传递的:
<userTask id="holidayApprovedTask" name="Holiday approved" flowable:assignee="${employee}"/>
要获得实际的任务列表,需要通过TaskService创建一个TaskQuery。我们配置这个查询只返回’managers’组的任务:
1
2
3
4
5
6 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());
}
可以使用任务Id获取特定流程实例的变量,并在屏幕上显示实际的申请:
1
2
3
4
5
6 System.out.println("Which task would you like to complete?");
int taskIndex = Integer.valueOf(scanner.nextLine());
Task task = tasks.get(taskIndex - 1);
Map<String, Object> processVariables = taskService.getVariables(task.getId());
System.out.println(processVariables.get("employee") + " wants " +
processVariables.get("nrOfHolidays") + " of holidays. Do you approve this?");
运行结果像下面这样:
经理现在就可以完成任务了。在现实中,这通常意味着由用户提交一个表单。表单中的数据作为流程变量传递。在这里,我们在完成任务时传递带有’approved’变量(这个名字很重要,因为之后会在顺序流的条件中使用!)的map来模拟:
1
2
3
4 boolean approved = scanner.nextLine().toLowerCase().equals("y");
variables = new HashMap<String, Object>();
variables.put("approved", approved);
taskService.complete(task.getId(), variables);
现在任务完成,并会在离开排他网关的两条路径中,基于’approved’流程变量选择一条。
2.3.6. 实现JavaDelegate
拼图还缺了一块:我们还没有实现申请通过后执行的自动逻辑。在BPMN 2.0 XML中,这是一个服务任务(service task):
<serviceTask id="externalSystemCall" name="Enter holidays in external system"
flowable:class="org.flowable.CallExternalSystemDelegate"/>
在现实中,这个逻辑可以做任何事情:向某个系统发起一个HTTP REST服务调用,或调用某个使用了好几十年的系统中的遗留代码。我们不会在这里实现实际的逻辑,而只是简单的日志记录流程。
创建一个新的类(在Eclipse里File → New → Class),填入org.flowable作为包名,CallExternalSystemDelegate作为类名。让这个类实现org.flowable.engine.delegate.JavaDelegate接口,并实现execute方法:
1
2
3
4
5
6
7
8
9
10
11
12
13 package org.flowable;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
public class CallExternalSystemDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) {
System.out.println("Calling the external system for employee "
+ execution.getVariable("employee"));
}
}
当执行到达服务任务时,会初始化并调用BPMN 2.0 XML中所引用的类。
现在执行这个例子的时候,就会显示出日志信息,说明已经执行了自定义逻辑:
2.3.7. 使用历史数据
选择使用Flowable这样的流程引擎的原因之一,是它可以自动存储所有流程实例的审计数据或历史数据。这些数据可以用于创建报告,深入展现组织运行的情况,瓶颈在哪里,等等。
例如,如果希望显示流程实例已经执行的时间,就可以从ProcessEngine获取HistoryService,并创建历史活动(historical activities)的查询。在下面的代码片段中,可以看到我们添加了一些额外的过滤条件:
-
只选择一个特定流程实例的活动
-
只选择已完成的活动
结果按照结束时间排序,代表其执行顺序。
1
2
3
4
5
6
7
8
9
10
11
12 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 1 milliseconds approveTask took 2638 milliseconds decision took 3 milliseconds externalSystemCall took 1 milliseconds
2.3.8. 小结
这个教程介绍了很多Flowable与BPMN 2.0的概念与术语,也展示了如何编程使用Flowable API。
当然,这只是个开始。下面的章节会更深入介绍许多Flowable引擎支持的选项与特性。其他章节介绍安装与使用Flowable引擎的不同方法,并详细介绍了所有可用的BPMN 2.0结构。
2.4. 开始使用Flowable REST API
这个章节展示了与上一章节相同的例子:部署一个流程定义,启动一个流程实例,获取任务列表并完成一个任务。最好先快速浏览上一章节以了解所做的事情。
这一次,将使用Flowable REST API而不是Java API。你很快就会意识到REST API与Java API紧密关联。只要了解一个,就能很快学会另一个。
可以在REST API章节找到Flowable REST API的完整细节。
2.4.1. 安装REST应用
使用Tomcat的步骤如下:
-
下载并解压缩最新的Tomcat zip文件(在Tomcat网站中选择’Core’发行版)。
-
将flowable-rest.war文件从解压的Flowable发行版的wars文件夹中复制到解压的Tomcat文件夹下的webapps文件夹下。
-
使用命令行,转到Tomcat文件夹下的bin文件夹。
-
执行'./catalina run'启动Tomcat服务器。
在服务启动过程中,会显示一些Flowable日志信息。在最后显示的一条类似'INFO [main] org.apache.catalina.startup.Catalina.start Server startup in xyz ms'的消息标志着服务器已经启动,可以接受请求。请注意默认情况下,使用H2内存数据库,这意味着数据在服务器重启后会丢失。
在下面的章节中,我们使用cURL展示各种REST调用。所有的REST调用默认都使用基本认证保护,所有的调用的用户都是 rest-admin,密码为’test'。
在启动后,通过执行下列命令验证应用运行正常:
curl --user rest-admin:test http://localhost:8080/flowable-rest/service/management/engine
如果能获得正确的json响应,则说明REST API已经启动并在工作。
2.4.2. 部署流程定义
第一步是部署一个流程定义。使用REST API时,需要将一个.bpmn20.xml文件(或对于多个流程引擎,一个.zip文件)作为’multipart/formdata’上传:
curl --user rest-admin:test -F "file=@holiday-request.bpmn20.xml" http://localhost:8080/flowable-rest/service/repository/deployments
要验证流程定义已经正确部署,可以请求流程定义的列表:
curl --user rest-admin:test http://localhost:8080/flowable-rest/service/repository/process-definitions
这将返回当前引擎中部署的所有流程定义的列表。
2.4.3. 启动流程实例
使用REST API启动一个流程实例与使用Java API很像:提供key作为流程定义的标识,并使用一个map作为初始化流程变量:
curl --user rest-admin:test -H "Content-Type: application/json" -X POST -d '{ "processDefinitionKey":"holidayRequest", "variables": [ { "name":"employee", "value": "John Doe" }, { "name":"nrOfHolidays", "value": 7 }]}' http://localhost:8080/flowable-rest/service/runtime/process-instances
将返回:
{"id":"43","url":"http://localhost:8080/flowable-rest/service/runtime/process-instances/43","businessKey":null,"suspended":false,"ended":false,"processDefinitionId":"holidayRequest:1:42","processDefinitionUrl":"http://localhost:8080/flowable-rest/service/repository/process-definitions/holidayRequest:1:42","activityId":null,"variables":[],"tenantId":"","completed":false}
2.4.4. 任务列表与完成任务
当流程实例启动后,第一个任务会指派给’managers’组。要获取这个组的所有任务,可以通过REST API进行任务查询:
curl --user rest-admin:test -H "Content-Type: application/json" -X POST -d '{ "candidateGroup" : "managers" }' http://localhost:8080/flowable-rest/service/query/tasks
这将返回’manager’组的所有任务的列表。
可以这样完成任务:
curl --user rest-admin:test -H "Content-Type: application/json" -X POST -d '{ "action" : "complete", "variables" : [ { "name" : "approved", "value" : true} ] }' http://localhost:8080/flowable-rest/service/runtime/tasks/25
然而,很可能会产生如下的错误:
{"message":"Internal server error","exception":"couldn't instantiate class org.flowable.CallExternalSystemDelegate"}
这意味着引擎无法找到服务任务引用的CallExternalSystemDelegate类。要解决这个错误,需要将该类放在应用的classpath下(并需要重启应用)。按照上一章节的介绍创建该类,并将其打包为JAR,放在Tomcat的webapps目录下的flowable-rest目录下的WEB-INF/lib目录下。
3. 配置
3.1. 创建ProcessEngine
Flowable流程引擎通过名为flowable.cfg.xml
的XML文件进行配置。请注意这种方式与使用Spring创建流程引擎不一样。
获取ProcessEngine
,最简单的方式是使用org.flowable.engine.ProcessEngines
类:
1 ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine()
这样会从classpath寻找flowable.cfg.xml
,并用这个文件中的配置构造引擎。下面的代码展示了一个配置的例子。后续章节会对配置参数进行详细介绍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<property name="jdbcUrl" value="jdbc:h2:mem:flowable;DB_CLOSE_DELAY=1000" />
<property name="jdbcDriver" value="org.h2.Driver" />
<property name="jdbcUsername" value="sa" />
<property name="jdbcPassword" value="" />
<property name="databaseSchemaUpdate" value="true" />
<property name="asyncExecutorActivate" value="false" />
<property name="mailServerHost" value="mail.my-corp.com" />
<property name="mailServerPort" value="5025" />
</bean>
</beans>
请注意这个配置XML文件实际上是一个Spring配置文件。但这并不意味着Flowable只能用于Spring环境!我们只是利用Spring内部的解析与依赖注入功能来简化引擎的构建过程。
也可以通过编程方式使用配置文件,来构造ProcessEngineConfiguration对象。也可以使用不同的bean id(例如第3行)。
1
2
3
4
5
6 ProcessEngineConfiguration.
createProcessEngineConfigurationFromResourceDefault();
createProcessEngineConfigurationFromResource(String resource);
createProcessEngineConfigurationFromResource(String resource, String beanName);
createProcessEngineConfigurationFromInputStream(InputStream inputStream);
createProcessEngineConfigurationFromInputStream(InputStream inputStream, String beanName);
也可以不使用配置文件,使用默认配置(参考不同的支持类获得更多信息)。
1
2 ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration();
ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration();
所有的ProcessEngineConfiguration.createXXX()
方法都返回ProcessEngineConfiguration
,并可以继续按需调整。调用buildProcessEngine()
后,生成一个ProcessEngine
:
1
2
3
4
5 ProcessEngine processEngine = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration()
.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_FALSE)
.setJdbcUrl("jdbc:h2:mem:my-own-db;DB_CLOSE_DELAY=1000")
.setAsyncExecutorActivate(false)
.buildProcessEngine();
3.2. ProcessEngineConfiguration bean
flowable.cfg.xml
文件中必须包含一个id为'processEngineConfiguration'的bean。
1 <bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
这个bean用于构建ProcessEngine
。有多个类可以用于定义processEngineConfiguration
。这些类用于不同的环境,并各自设置一些默认值。最佳实践是选择最匹配你环境的类,以便减少配置引擎需要的参数。目前可以使用的类为:
-
org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration:流程引擎独立运行。Flowable自行处理事务。在默认情况下,数据库检查只在引擎启动时进行(如果Flowable表结构不存在或表结构版本不对,会抛出异常)。
-
org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration:这是一个便于使用单元测试的类。Flowable自行处理事务。默认使用H2内存数据库。数据库会在引擎启动时创建,并在引擎关闭时删除。使用这个类时,很可能不需要更多的配置(除了使用任务执行器或邮件等功能时)。
-
org.flowable.spring.SpringProcessEngineConfiguration:在流程引擎处于Spring环境时使用。查看Spring集成章节了解更多信息。
-
org.flowable.engine.impl.cfg.JtaProcessEngineConfiguration:用于引擎独立运行,并使用JTA事务的情况。
3.3. 配置数据库
有两种方式配置Flowable引擎使用的数据库。第一种方式是定义数据库的JDBC参数:
-
jdbcUrl: 数据库的JDBC URL。
-
jdbcDriver: 对应数据库类型的驱动。
-
jdbcUsername: 用于连接数据库的用户名。
-
jdbcPassword: 用于连接数据库的密码。
通过提供的JDBC参数构造的数据源,使用默认的MyBatis连接池设置。可用下列属性调整这个连接池(来自MyBatis文档):
-
jdbcMaxActiveConnections: 连接池能够容纳的最大活动连接数量。默认值为10.
-
jdbcMaxIdleConnections: 连接池能够容纳的最大空闲连接数量。
-
jdbcMaxCheckoutTime: 连接从连接池“取出”后,被强制返回前的最大时间间隔,单位为毫秒。默认值为20000(20秒)。
-
jdbcMaxWaitTime: 这是一个底层设置,在连接池获取连接的时间异常长时,打印日志并尝试重新获取连接(避免连接池配置错误,导致没有异常提示)。默认值为20000(20秒)。
数据库配置示例:
1
2
3
4 <property name="jdbcUrl" value="jdbc:h2:mem:flowable;DB_CLOSE_DELAY=1000" />
<property name="jdbcDriver" value="org.h2.Driver" />
<property name="jdbcUsername" value="sa" />
<property name="jdbcPassword" value="" />
我们的跑分显示MyBatis连接池在处理大量并发请求时,并不是最经济或最具弹性的。因此,建议使用javax.sql.DataSource
的实现,并将其注入到流程引擎配置中(如Hikari、Tomcat JDBC连接池,等等):
1
2
3
4
5
6
7
8
9
10
11
12 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" >
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/flowable" />
<property name="username" value="flowable" />
<property name="password" value="flowable" />
<property name="defaultAutoCommit" value="false" />
</bean>
<bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<property name="dataSource" ref="dataSource" />
...
请注意Flowable发布时不包括用于定义数据源的库。需要自行把库放在classpath中。
无论使用JDBC还是数据源方式配置,都可以使用下列参数:
-
databaseType: 通常不需要专门设置这个参数,因为它可以从数据库连接信息中自动检测得出。只有在自动检测失败时才需要设置。可用值:{h2, mysql, oracle, postgres, mssql, db2}。这个选项会决定创建、删除与查询时使用的脚本。查看“支持的数据库”章节了解我们支持哪些类型的数据库。
-
databaseSchemaUpdate: 用于设置流程引擎启动关闭时使用的数据库表结构控制策略。
-
false
(默认): 当引擎启动时,检查数据库表结构的版本是否匹配库文件版本。版本不匹配时抛出异常。 -
true
: 构建引擎时,检查并在需要时更新表结构。表结构不存在则会创建。 -
create-drop
: 引擎创建时创建表结构,并在引擎关闭时删除表结构。
-
3.4. 配置JNDI数据源
默认情况下,Flowable的数据库配置保存在每个web应用WEB-INF/classes文件夹下的db.properties文件中。有时这样并不合适,因为这需要用户修改Flowable源码中的db.properties文件并重新编译war包,或者在部署后解开war包并修改db.properties文件。
通过使用JNDI(Java Naming and Directory Interface,Java命名和目录接口)获取数据库连接,连接就完全交由Servlet容器管理,并可以在WAR部署之外管理配置。同时也提供了比db.properties中更多的控制连接的参数。
3.4.1. 配置
根据你使用的servlet容器应用不同,配置JNDI数据源的方式也不同。下面的介绍用于Tomcat,对于其他容器应用,请参考对应的文档。
Tomcat的JNDI资源配置在$CATALINA_BASE/conf/[enginename]/[hostname]/[warname].xml (对于Flowable UI通常会是$CATALINA_BASE/conf/Catalina/localhost/flowable-app.xml)。当应用第一次部署时,默认会从Flowable war包中复制context.xml。所以如果存在这个文件则需要替换。例如,如果需要将JNDI资源修改为应用连接MySQL而不是H2,需要如下修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 <?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/flowable-app">
<Resource auth="Container"
name="jdbc/flowableDB"
type="javax.sql.DataSource"
description="JDBC DataSource"
url="jdbc:mysql://localhost:3306/flowable"
driverClassName="com.mysql.jdbc.Driver"
username="sa"
password=""
defaultAutoCommit="false"
initialSize="5"
maxWait="5000"
maxActive="120"
maxIdle="5"/>
</Context>
3.4.2. JNDI参数
在Flowable UI的配置文件中使用下列参数配置JNDI数据源:
-
spring.datasource.jndi-name=: 数据源的JNDI名
-
datasource.jndi.resourceRef: 设置是否在J2EE容器中查找。也就是说,如果JNDI名中没有包含"java:comp/env/"前缀,是否需要添加它。默认为"true"。
3.5. 支持的数据库
下面列出Flowable用于引用数据库的类型(区分大小写!)。
Flowable数据库类型 | 示例JDBC URL | 备注 |
---|---|---|
h2 |
jdbc:h2:tcp://localhost/flowable |
默认配置的数据库 |
mysql |
jdbc:mysql://localhost:3306/flowable?autoReconnect=true |
已使用mysql-connector-java数据库驱动测试 |
oracle |
jdbc:oracle:thin:@localhost:1521:xe |
|
postgres |
jdbc:postgresql://localhost:5432/flowable |
|
db2 |
jdbc:db2://localhost:50000/flowable |
|
mssql |
jdbc:sqlserver://localhost:1433;databaseName=flowable (jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver) OR jdbc:jtds:sqlserver://localhost:1433/flowable (jdbc.driver=net.sourceforge.jtds.jdbc.Driver) |
已使用Microsoft JDBC Driver 4.0 (sqljdbc4.jar)与JTDS Driver测试 |
3.6. 创建数据库表
在你的数据库中创建数据库表,最简单的方法是:
-
在classpath中添加flowable-engine JAR
-
添加合适的数据库驱动
-
在classpath中添加Flowable配置文件(flowable.cfg.xml),指向你的数据库(参考数据库配置)
-
执行DbSchemaCreate类的main方法
然而,通常只有数据库管理员可以在数据库中执行DDL语句,在生产环境中这也是最明智的选择。DDL的SQL脚本可以在Flowable下载页面或Flowable发布文件夹中找到,位于database
子文件夹。引擎JAR (flowable-engine-x.jar)的org/flowable/db/create包中也有一份(drop文件夹存放删除脚本)。SQL文件的格式为:
flowable.{db}.{create|drop}.{type}.sql
其中db为支持的数据库,而type为:
-
engine: 引擎执行所需的表,必需。
-
history: 存储历史与审计信息的表。当历史级别设置为none时不需要。请注意不使用这些表会导致部分使用历史数据的功能失效(如任务备注)。
MySQL用户请注意:低于5.6.4的MySQL版本不支持timestamps或包含毫秒精度的日期。更糟的是部分版本会在创建类似的列时抛出异常,而另一些版本则不会。当使用自动创建/升级时,引擎在执行时会自动修改DDL语句。当使用DDL文件方式建表时,可以使用通用版本,或使用文件名包含mysql55的特殊版本(用于5.6.4以下的任何版本)。特殊版本的文件中不会使用毫秒精度的列类型。
具体地说,对于MySQL的版本:
-
<5.6: 不支持毫秒精度。可以使用DDL文件(使用包含mysql55的文件)。可以使用自动创建/升级。
-
5.6.0 - 5.6.3: 不支持毫秒精度。不可以使用自动创建/升级。建议升级为较新版本的数据库。如果确实需要,可以使用包含mysql55的DDL文件。
-
5.6.4+: 支持毫秒精度。可以使用DDL文件(默认的包含mysql的文件)。可以使用自动创建/升级。
请注意如果在Flowable表已经创建/升级后,再升级MySQL数据库,则需要手工修改列类型!
3.7. 数据库表名说明
Flowable的所有数据库表都以ACT_开头。第二部分是说明表用途的两字符标示符。服务API的命名也大略符合这个规则。
-
ACT_RE_*: 'RE’代表
repository
。带有这个前缀的表包含“静态”信息,例如流程定义与流程资源(图片、规则等)。 -
ACT_RU_*: 'RU’代表
runtime
。这些表存储运行时信息,例如流程实例(process instance)、用户任务(user task)、变量(variable)、作业(job)等。Flowable只在流程实例运行中保存运行时数据,并在流程实例结束时删除记录。这样保证运行时表小和快。 -
ACT_HI_*: 'HI’代表
history
。这些表存储历史数据,例如已完成的流程实例、变量、任务等。 -
ACT_GE_*: 通用数据。在多处使用。
3.8. 数据库升级
在升级前,请确保你已经(使用数据库的备份功能)备份了数据库。
默认情况下,每次流程引擎创建时会进行版本检查,通常是在你的应用或者Flowable web应用启动的时候。如果Flowable发现库版本与Flowable数据库表版本不同,会抛出异常。
要进行升级,首先需要将下列配置参数放入你的flowable.cfg.xml配置文件:
1
2
3
4
5
6
7
8
9
10 <beans >
<bean id="processEngineConfiguration"
class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- ... -->
<property name="databaseSchemaUpdate" value="true" />
<!-- ... -->
</bean>
</beans>
同时,在classpath中加上合适的数据库驱动。升级应用中的Flowable库,或者启动一个新版本的Flowable,并将它指向包含旧版本数据的数据库。将databaseSchemaUpdate
设置为true
。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
也可以直接运行升级DDL语句。也可以从Flowable下载页面获取升级数据库脚本并运行。
3.9. 作业执行器(从6.0.1版本起)
在Flowable V6中唯一可用的作业执行器,是Flowable V5中的异步执行器(async executor)。因为它为Flowable引擎提供了性能更好,对数据库也更友好的执行异步作业的方式。 Flowable V5中的作业执行器(job executor)在V6中不再可用。可以在用户手册的高级章节找到更多信息。
此外,如果在Java EE 7下运行,容器还可以使用符合JSR-236标准的ManagedAsyncJobExecutor
来管理线程。要启用这个功能,需要在配置中如下加入线程工厂:
1
2
3
4
5
6
7
8
9 <bean id="threadFactory" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:jboss/ee/concurrency/factory/default" />
</bean>
<bean id="customJobExecutor" class="org.flowable.engine.impl.jobexecutor.ManagedAsyncJobExecutor">
<!-- ... -->
<property name="threadFactory" ref="threadFactory" />
<!-- ... -->
</bean>
如果没有设置线程工厂,ManagedAsyncJobExecutor实现会退化为默认实现(AsyncJobExecutor)。
3.10. 启用作业执行器
AsyncExecutor
是管理线程池的组件,用于触发定时器与其他异步任务。也可以使用其他实现(如使用消息队列,参见用户手册的高级章节)。
默认情况下,AsyncExecutor
并未启用,也不会启动。如下配置使异步执行器与Flowable引擎一同启动:
1 <property name="asyncExecutorActivate" value="true" />
asyncExecutorActivate这个参数使Flowable引擎在启动同时启动异步执行器。
3.11. 配置邮件服务器
配置邮件服务器是可选的。Flowable支持在业务流程中发送电子邮件。发送电子邮件需要配置有效的SMTP邮件服务器。查看电子邮件任务了解配置选项。
3.13. 配置异步历史
[实验性] 从Flowable 6.1.0起,添加了异步历史功能。当启用异步历史时,历史数据将由历史任务执行器负责持久化,而不是与运行时执行持久化同步保存。 查看配置异步历史章节了解更多细节。
1 <property name="asyncHistoryEnabled" value="true" />
3.14. 配置在表达式与脚本中可用的bean
默认情况下,所有通过flowable.cfg.xml
或你自己的Spring配置文件声明的bean,都可以在表达式与脚本中使用。如果你希望限制配置文件中bean的可见性,可以使用流程引擎配置的beans
参数。ProcessEngineConfiguration
中的beans
参数是一个map。当你配置这个参数时,只有在这个map中声明的bean可以在表达式与脚本中使用。bean会使用你在map中指定的名字暴露。
3.15. 配置部署缓存
鉴于流程定义信息不会改变,为了避免每次使用流程定义时都读取数据库,所有的流程定义都会(在解析后)被缓存。默认情况下,这个缓存没有限制。要限制流程定义缓存,加上如下的参数
1 <property name="processDefinitionCacheLimit" value="10" />
设置这个参数,会将默认的hashmap缓存替换为LRU缓存,以进行限制。当然,参数的“最佳”取值,取决于总的流程定义数量,以及实际使用的流程定义数量。
也可以注入自己的缓存实现。必须是一个实现了org.flowable.engine.impl.persistence.deploy.DeploymentCache
接口的bean:
1
2
3 <property name="processDefinitionCache">
<bean class="org.flowable.MyCache" />
</property>
类似的,可以使用名为knowledgeBaseCacheLimit
与knowledgeBaseCache
的参数配置规则缓存(rules cache)。只有在流程中使用规则任务(rules task)时才需要设置。
3.16. 日志
所有的日志(Flowable、Spring、MyBatis等)都通过SLF4J路由,并允许你自行选择日志实现。
默认情况下,Flowable引擎依赖中不提供SFL4J绑定JAR。你需要自行将其加入你的项目,以便使用所选的日志框架。如果没有加入实现JAR,SLF4J会使用NOP-logger。这时除了一条警告外,不会记录任何日志。可以从http://www.slf4j.org/codes.html#StaticLoggerBinder了解关于绑定的更多信息。
可以像这样(这里使用Log4j)使用Maven添加依赖,请注意你还需要加上版本:
1
2
3
4 <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
Flowable-UI与Flowable-rest web应用配置为使用Log4j绑定。运行所有flowable-*模块的测试时也会使用Log4j。
重要提示:当使用classpath中带有commons-logging的容器时:为了将spring的日志路由至SLF4j,需要使用桥接(参考http://www.slf4j.org/legacy.html#jclOverSLF4J)。如果你的容器提供了commons-logging实现,请按照http://www.slf4j.org/codes.html#release页面的指示调整。
使用Maven的示例(省略了版本):
1
2
3
4 <dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
3.17. 映射诊断上下文
Flowable支持SLF4J的映射诊断上下文特性。下列基本信息会与需要日志记录的信息一起,传递给底层日志实现:
-
processDefinition Id 作为 mdcProcessDefinitionID
-
processInstance Id 作为 mdcProcessInstanceID
-
execution Id 作为 mdcExecutionId
默认情况下这些信息都不会被日志记录,但可以通过配置logger,按照你想要的格式,与其他日志信息一起显示。例如在Log4j中进行如下简单的格式定义,就可以让logger显示上述信息:
1
2 log4j.appender.consoleAppender.layout.ConversionPattern=ProcessDefinitionId=%X{mdcProcessDefinitionID}
executionId=%X{mdcExecutionId} mdcProcessInstanceID=%X{mdcProcessInstanceID} mdcBusinessKey=%X{mdcBusinessKey} %m%n
如果需要使用日志中包含的信息进行实时监测(如使用日志分析器),就会很有帮助。
3.18. 事件处理器
Flowable引擎中的事件机制可以让你在引擎中发生多种事件的时候得到通知。查看所有支持的事件类型了解可用的事件。
可以只为特定种类的事件注册监听器,而不是在任何类型的事件发送时都被通知。可以通过配置添加引擎全局的事件监听器,在运行时通过API添加引擎全局的事件监听器,也可以在BPMN XML文件为个别流程定义添加事件监听器。
所有被分发的事件都是org.flowable.engine.common.api.delegate.event.FlowableEvent
的子类。事件(在可用时)提供type
, executionId
, processInstanceId
与processDefinitionId
。部分事件含有关于发生事件的上下文信息。关于事件包含的附加信息,请参阅所有支持的事件类型。
3.18.1. 实现事件监听器
对事件监听器的唯一要求,是要实现org.flowable.engine.delegate.event.FlowableEventListener
接口。下面是一个监听器实现的例子,它将接收的所有事件打印至标准输出,并对作业执行相关的事件特别处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 public class MyEventListener implements FlowableEventListener {
@Override
public void onEvent(FlowableEvent event) {
switch (event.getType()) {
case JOB_EXECUTION_SUCCESS:
System.out.println("A job well done!");
break;
case JOB_EXECUTION_FAILURE:
System.out.println("A job has failed...");
break;
default:
System.out.println("Event received: " + event.getType());
}
}
@Override
public boolean isFailOnException() {
// onEvent方法中的逻辑并不重要,可以忽略日志失败异常……
return false;
}
}
isFailOnException()
方法决定了当事件分发后,onEvent(..)
方法抛出异常时的行为。若返回false
,忽略异常;若返回true
,异常不会被忽略而会被上抛,使当前执行的命令失败。如果事件是API调用(或其他事务操作,例如作业执行)的一部分,事务将被回滚。如果事件监听器中并不是重要的业务操作,建议返回false
。
Flowable提供了少量基础实现,以简化常用的事件监听器使用场景。它们可以被用作监听器的示例或基类:
-
org.flowable.engine.delegate.event.BaseEntityEventListener: 事件监听器基类,可用来监听实体(entity)相关事件,特定或所有实体的事件都可以。它隐藏了类型检测,提供了4个需要覆盖的方法:
onCreate(..)
,onUpdate(..)
与onDelete(..)
在实体创建、更新及删除时调用;对所有其他实体相关事件,onEntityEvent(..)
会被调用。
3.18.2. 配置与使用
在流程引擎中配置的事件监听器会在流程引擎启动时生效,引擎重启后也会保持有效。
eventListeners
参数为org.flowable.engine.delegate.event.FlowableEventListener
类实例的列表(list)。与其他地方一样,你可以声明内联bean定义,也可以用ref
指向已有的bean。下面的代码片段在配置中添加了一个事件监听器,无论任何类型的事件分发时,都会得到通知:
1
2
3
4
5
6
7
8
9 <bean id="processEngineConfiguration"
class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
...
<property name="eventListeners">
<list>
<bean class="org.flowable.engine.example.MyEventListener" />
</list>
</property>
</bean>
要在特定类型的事件分发时得到通知,使用typedEventListeners
参数,值为map。map的key为逗号分隔的事件名字列表(或者一个事件的名字),取值为org.flowable.engine.delegate.event.FlowableEventListener
实例的列表。下面的代码片段在配置中添加了一个事件监听器,它会在作业执行成功或失败时得到通知:
1
2
3
4
5
6
7
8
9
10
11
12
13 <bean id="processEngineConfiguration"
class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
...
<property name="typedEventListeners">
<map>
<entry key="JOB_EXECUTION_SUCCESS,JOB_EXECUTION_FAILURE" >
<list>
<bean class="org.flowable.engine.example.MyJobEventListener" />
</list>
</entry>
</map>
</property>
</bean>
事件分发的顺序由加入监听器的顺序决定。首先,所有普通(eventListeners
参数定义的)事件监听器按照在list
里的顺序被调用;之后,如果分发的是某类型的事件,则(typedEventListeners
参数定义的)该类型监听器被调用。
3.18.3. 在运行时添加监听器
可以使用API(RuntimeService
)为引擎添加或删除事件监听器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 新增一个监听器,会在所有事件发生时被通知。
* @param listenerToAdd 要新增的监听器
*/
void addEventListener(FlowableEventListener listenerToAdd);
/**
* 新增一个监听器,在给定类型的事件发生时被通知。
* @param listenerToAdd 要新增的监听器
* @param types 监听器需要监听的事件类型
*/
void addEventListener(FlowableEventListener listenerToAdd, FlowableEventType... types);
/**
* 从分发器中移除指定监听器。该监听器将不再被通知,无论该监听器注册为监听何种类型。
* @param listenerToRemove 要移除的监听器
*/
void removeEventListener(FlowableEventListener listenerToRemove);
请注意,运行时新增的监听器在引擎重启后不会保持。
3.18.4. 为流程定义增加监听器
可以为某一流程定义增加监听器。只有与该流程定义相关,或使用该流程定义启动的流程实例相关的事件,才会调用这个监听器。监听器实现可以用完全限定类名(fully qualified classname)定义;也可以定义为表达式,该表达式需要能被解析为实现监听器接口的bean;也可以配置为抛出消息(message)/信号(signal)/错误(error)的BPMN事件。
执行用户定义逻辑的监听器
下面的代码片段为流程定义增加了2个监听器。第一个监听器接收任何类型的事件,使用完全限定类名定义。第二个监听器只在作业成功执行或失败时被通知,使用流程引擎配置中beans
参数定义的bean作为监听器。
1
2
3
4
5
6
7
8
9 <process id="testEventListeners">
<extensionElements>
<flowable:eventListener class="org.flowable.engine.test.MyEventListener" />
<flowable:eventListener delegateExpression="${testEventListener}" events="JOB_EXECUTION_SUCCESS,JOB_EXECUTION_FAILURE" />
</extensionElements>
...
</process>
实体相关的事件也可以在流程定义中增加监听器,只有在特定实体类型的事件发生时得到通知。下面的代码片段展示了如何设置。可以响应实体的所有事件(第一个例子),或只响应实体的特定类型事件(第二个例子)。
1
2
3
4
5
6
7
8
9 <process id="testEventListeners">
<extensionElements>
<flowable:eventListener class="org.flowable.engine.test.MyEventListener" entityType="task" />
<flowable:eventListener delegateExpression="${testEventListener}" events="ENTITY_CREATED" entityType="task" />
</extensionElements>
...
</process>
entityType
可用的值有:attachment
(附件), comment
(备注), execution
(执行), identity-link
(身份关联), job
(作业), process-instance
(流程实例), process-definition
(流程定义), task
(任务)。
抛出BPMN事件的监听器
处理分发的事件的另一个方法,是抛出BPMN事件。请牢记在心,只有特定种类的Flowable事件类型,抛出BPMN事件才合理。例如,在流程实例被删除时抛出BPMN事件,会导致错误。下面的代码片段展示了如何在流程实例中抛出信号,向外部流程(全局)抛出信号,在流程实例中抛出消息事件,以及在流程实例中抛出错误事件。这里不使用class
或delegateExpression
,而要使用throwEvent
属性,以及一个附加属性,用于指定需要抛出的事件类型。
1
2
3
4
5 <process id="testEventListeners">
<extensionElements>
<flowable:eventListener throwEvent="signal" signalName="My signal" events="TASK_ASSIGNED" />
</extensionElements>
</process>
1
2
3
4
5 <process id="testEventListeners">
<extensionElements>
<flowable:eventListener throwEvent="globalSignal" signalName="My signal" events="TASK_ASSIGNED" />
</extensionElements>
</process>
1
2
3
4
5 <process id="testEventListeners">
<extensionElements>
<flowable:eventListener throwEvent="message" messageName="My message" events="TASK_ASSIGNED" />
</extensionElements>
</process>
1
2
3
4
5 <process id="testEventListeners">
<extensionElements>
<flowable:eventListener throwEvent="error" errorCode="123" events="TASK_ASSIGNED" />
</extensionElements>
</process>
如果需要使用额外的逻辑判断是否需要抛出BPMN事件,可以扩展Flowable提供的监听器类。通过在你的子类中覆盖isValidEvent(FlowableEvent event)
,可以阻止抛出BPMN事件。相关的类为org.flowable.engine.test.api.event.SignalThrowingEventListenerTest
, org.flowable.engine.impl.bpmn.helper.MessageThrowingEventListener
与org.flowable.engine.impl.bpmn.helper.ErrorThrowingEventListener
.
关于流程定义监听器的说明
-
事件监听器只能作为
extensionElements
的子元素,声明在process
元素上。不能在个别节点(activity)上定义(事件)监听器。 -
delegateExpression
中的表达式,与其他表达式(例如在网关中的)不一样,不可以访问执行上下文。只能够引用在流程引擎配置中beans
参数定义的bean;或是在使用spring(且没有定义beans参数)时,引用任何实现了监听器接口的spring bean。 -
使用监听器的
class
属性时,只会创建唯一一个该类的实例。请确保监听器实现不依赖于成员变量,或确保多线程/上下文的使用安全。 -
如果
events
属性使用了不合法的事件类型,或者使用了不合法的throwEvent
值,会在流程定义部署时抛出异常(导致部署失败)。如果class
或delegateExecution
指定了不合法的值(不存在的类,不存在的bean引用,或者代理类没有实现监听器接口),在流程启动(或该流程定义的第一个有效事件分发给这个监听器)时,会抛出异常。请确保引用的类在classpath中,并且保证表达式能够解析为有效的实例。
3.18.5. 通过API分发事件
可以通过API提供事件分发机制,向任何在引擎中注册的监听器分发自定义事件。建议(但不强制)只分发CUSTOM
类型的FlowableEvents
。使用RuntimeService
分发事件:
1
2
3
4
5
6
7
8
9
/**
* 将给定事件分发给所有注册监听器。
* @param event 要分发的事件。
*
* @throws FlowableException 当分发事件发生异常,或者{@link FlowableEventDispatcher}被禁用。
* @throws FlowableIllegalArgumentException 当给定事件不可分发
*/
void dispatchEvent(FlowableEvent event);
3.18.6. 支持的事件类型
下表列出引擎中的所有事件类型。每种类型对应org.flowable.engine.common.api.delegate.event.FlowableEventType
中的一个枚举值。
事件名称 | 说明 | 事件类 |
---|---|---|
ENGINE_CREATED |
本监听器所属的流程引擎已经创建,并可以响应API调用。 |
|
ENGINE_CLOSED |
本监听器所属的流程引擎已经关闭,不能再对该引擎进行API调用。 |
|
ENTITY_CREATED |
新的实体已经创建。该实体包含在本事件里。 |
|
ENTITY_INITIALIZED |
新的实体已经创建并完全初始化。如果任何子实体作为该实体的一部分被创建,本事件会在子实体创建/初始化后触发,与 |
|
ENTITY_UPDATED |
实体已经更新。该实体包含在本事件里。 |
|
ENTITY_DELETED |
实体已经删除。该实体包含在本事件里。 |
|
ENTITY_SUSPENDED |
实体已经暂停。该实体包含在本事件里。ProcessDefinitions(流程定义), ProcessInstances(流程实例)与Tasks(任务)会分发本事件。 |
|
ENTITY_ACTIVATED |
实体已经激活。该实体包含在本事件里。ProcessDefinitions, ProcessInstances与Tasks会分发本事件。 |
|
JOB_EXECUTION_SUCCESS |
作业已经成功执行。该作业包含在本事件里。 |
|
JOB_EXECUTION_FAILURE |
作业执行失败。该作业与异常包含在本事件里。 |
|
JOB_RETRIES_DECREMENTED |
作业重试次数已经由于执行失败而减少。该作业包含在本事件里。 |
|
TIMER_SCHEDULED |
已创建一个定时作业,并预计在未来时间点执行。 |
|
TIMER_FIRED |
定时器已经触发。 |
|
JOB_CANCELED |
作业已经取消。该作业包含在本事件里。作业会由于API调用取消,任务完成导致关联的边界定时器取消,也会由于新流程定义的部署而取消。 |
|
ACTIVITY_STARTED |
节点开始执行 |
|
ACTIVITY_COMPLETED |
节点成功完成 |
|
ACTIVITY_CANCELLED |
节点将要取消。节点的取消有三个原因(MessageEventSubscriptionEntity, SignalEventSubscriptionEntity, TimerEntity)。 |
|
ACTIVITY_SIGNALED |
节点收到了一个信号 |
|
ACTIVITY_MESSAGE_RECEIVED |
节点收到了一个消息。事件在节点接收消息前分发。节点接收消息后,会为该节点分发 |
|
ACTIVITY_MESSAGE_WAITING |
一个节点已经创建了一个消息事件订阅,并正在等待接收消息。 |
|
ACTIVITY_MESSAGE_CANCELLED |
一个节点已经取消了一个消息事件订阅,因此接收这个消息不会再触发该节点。 |
|
ACTIVITY_ERROR_RECEIVED |
节点收到了错误事件。在节点实际处理错误前分发。该事件的 |
|
UNCAUGHT_BPMN_ERROR |
抛出了未捕获的BPMN错误。流程没有该错误的处理器。该事件的 |
|
ACTIVITY_COMPENSATE |
节点将要被补偿(compensate)。该事件包含将要执行补偿的节点id。 |
|
MULTI_INSTANCE_ACTIVITY_STARTED |
多实例节点开始执行 |
|
MULTI_INSTANCE_ACTIVITY_COMPLETED |
多实例节点成功完成 |
|
MULTI_INSTANCE_ACTIVITY_CANCELLED |
多实例节点将要取消。多实例节点的取消有三个原因(MessageEventSubscriptionEntity, SignalEventSubscriptionEntity, TimerEntity)。 |
|
VARIABLE_CREATED |
流程变量已经创建。本事件包含变量名、取值,及关联的执行和任务(若有)。 |
|
VARIABLE_UPDATED |
变量已经更新。本事件包含变量名、取值,及关联的执行和任务(若有)。 |
|
VARIABLE_DELETED |
变量已经删除。本事件包含变量名、最后取值,及关联的执行和任务(若有)。 |
|
TASK_ASSIGNED |
任务已经分派给了用户。该任务包含在本事件里。 |
|
TASK_CREATED |
任务已经创建。本事件在 |
|
TASK_COMPLETED |
任务已经完成。本事件在 |
|
PROCESS_CREATED |
流程实例已经创建。已经设置所有的基础参数,但还未设置变量。 |
|
PROCESS_STARTED |
流程实例已经启动。在启动之前创建的流程时分发。PROCESS_STARTED事件在相关的ENTITY_INITIALIZED事件,以及设置变量之后分发。 |
|
PROCESS_COMPLETED |
流程实例已经完成。在最后一个节点的 |
|
PROCESS_COMPLETED_WITH_TERMINATE_END_EVENT |
流程已经到达终止结束事件(terminate end event)并结束。 |
|
PROCESS_CANCELLED |
流程已经被取消。在流程实例从运行时中删除前分发。流程实例由API调用 |
|
MEMBERSHIP_CREATED |
用户已经加入组。本事件包含了相关的用户和组的id。 |
|
MEMBERSHIP_DELETED |
用户已经从组中移出。本事件包含了相关的用户和组的id。 |
|
MEMBERSHIPS_DELETED |
组的所有用户将被移出。本事件在用户移出前抛出,因此关联关系仍然可以访问。因为性能原因,不会再为每个被移出的用户抛出 |
|
引擎中所有的 ENTITY_\*
事件都与实体关联。下表列出每个实体分发的实体事件:
-
ENTITY_CREATED, ENTITY_INITIALIZED, ENTITY_DELETED
: 附件(Attachment),备注(Comment),部署(Deployment),执行(Execution),组(Group),身份关联(IdentityLink),作业(Job),模型(Model),流程定义(ProcessDefinition),流程实例(ProcessInstance),任务(Task),用户(User)。 -
ENTITY_UPDATED
: 附件,部署,执行,组,身份关联,作业,模型,流程定义,流程实例,任务,用户。 -
ENTITY_SUSPENDED, ENTITY_ACTIVATED
: 流程定义,流程实例/执行,任务。
3.18.7. 附加信息
监听器只会响应其所在引擎分发的事件。因此如果在同一个数据库上运行不同的引擎,则只有该监听器注册的引擎生成的事件,才会分发给该监听器。其他引擎生成的事件不会分发给这个监听器,而不论这些引擎是否运行在同一个JVM下。
某些事件类型(与实体相关)暴露了目标实体。按照事件类型的不同,有些实体不能被更新(如实体删除事件中的实体)。如果可能的话,请使用事件暴露的EngineServices
来安全地操作引擎。即使这样,更新、操作事件中暴露的实体仍然需要小心。
历史不会分发实体事件,因为它们都有对应的运行时实体分发事件。
4. Flowable API
4.1. 流程引擎API与服务
引擎API是与Flowable交互的最常用手段。总入口点是ProcessEngine
。像配置章节中介绍的一样,ProcessEngine可以使用多种方式创建。使用ProcessEngine,可以获得各种提供工作流/BPM方法的服务。ProcessEngine与服务对象都是线程安全的,因此可以在服务器中保存并共用同一个引用。
1
2
3
4
5
6
7
8
9
10 ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
TaskService taskService = processEngine.getTaskService();
ManagementService managementService = processEngine.getManagementService();
IdentityService identityService = processEngine.getIdentityService();
HistoryService historyService = processEngine.getHistoryService();
FormService formService = processEngine.getFormService();
DynamicBpmnService dynamicBpmnService = processEngine.getDynamicBpmnService();
在ProcessEngines.getDefaultProcessEngine()
第一次被调用时,将初始化并构建流程引擎,之后的重复调用都会返回同一个流程引擎。可以通过ProcessEngines.init()
创建流程引擎,并由ProcessEngines.destroy()
关闭流程引擎。
ProcessEngines会扫描flowable.cfg.xml
与flowable-context.xml
文件。对于flowable.cfg.xml
文件,流程引擎会以标准Flowable方式构建引擎:ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(inputStream).buildProcessEngine()
。对于flowable-context.xml
文件,流程引擎会以Spring的方式构建:首先构建Spring应用上下文,然后从该上下文中获取流程引擎。
所有的服务都是无状态的。这意味着你可以很容易的在集群环境的多个节点上运行Flowable,使用同一个数据库,而不用担心上一次调用实际在哪台机器上执行。不论在哪个节点执行,对任何服务的任何调用都是幂等(idempotent)的。
RepositoryService很可能是使用Flowable引擎要用的第一个服务。这个服务提供了管理与控制部署(deployments)
与流程定义(process definitions)
的操作。在这里简单说明一下,流程定义是BPMN 2.0流程对应的Java对象,体现流程中每一步的结构与行为。部署
是Flowable引擎中的包装单元,一个部署中可以包含多个BPMN 2.0 XML文件及其他资源。开发者可以决定在一个部署中包含的内容,可以是单个流程的BPMN 2.0 XML文件,也可以包含多个流程及其相关资源(如’hr-processes’部署可以包含所有与人力资源流程相关的的东西)。RepositoryService
可用于部署
这样的包。部署意味着将它上传至引擎,引擎将在储存至数据库之前检查与分析所有的流程。在部署操作后,可以在系统中使用这个部署包,部署包中的所有流程都可以启动。
此外,这个服务还可以:
-
查询引擎现有的部署与流程定义。
-
暂停或激活部署中的某些流程,或整个部署。暂停意味着不能再对它进行操作,激活刚好相反,重新使它可以操作。
-
获取各种资源,比如部署中保存的文件,或者引擎自动生成的流程图。
-
获取POJO版本的流程定义。它可以用Java而不是XML的方式查看流程。
与提供静态信息(也就是不会改变,至少不会经常改变的信息)的RepositoryService
相反,RuntimeService用于启动流程定义的新流程实例。前面介绍过,流程定义
中定义了流程中不同步骤的结构与行为。流程实例则是流程定义的实际执行过程。同一时刻,一个流程定义通常有多个运行中的实例。RuntimeService
也用于读取与存储流程变量
。流程变量是流程实例中的数据,可以在流程的许多地方使用(例如排他网关经常使用流程变量判断流程下一步要走的路径)。RuntimeService
还可以用于查询流程实例与执行(Execution)。执行也就是BPMN 2.0中 'token'
的概念。通常执行是指向流程实例当前位置的指针。最后,还可以在流程实例等待外部触发时使用RuntimeService
,使流程可以继续运行。流程有许多等待状态(wait states)
,RuntimeService
服务提供了许多操作用于“通知”流程实例:已经接收到外部触发,流程实例可以继续运行。
对于像Flowable这样的BPM引擎来说,核心是需要人类用户操作的任务。所有任务相关的东西都组织在TaskService中,例如:
-
查询分派给用户或组的任务
-
创建独立运行(standalone)任务。这是一种没有关联到流程实例的任务。
-
决定任务的执行用户(assignee),或者将用户通过某种方式与任务关联。
-
认领(claim)与完成(complete)任务。认领是指某人决定成为任务的执行用户,也即他将会完成这个任务。完成任务是指“做这个任务要求的工作”,通常是填写某个表单。
IdentityService很简单。它用于管理(创建,更新,删除,查询……)组与用户。请注意,Flowable实际上在运行时并不做任何用户检查。例如任务可以分派给任何用户,而引擎并不会验证系统中是否存在该用户。这是因为Flowable有时要与LDAP、Active Directory等服务结合使用。
FormService是可选服务。也就是说Flowable没有它也能很好地运行,而不必牺牲任何功能。这个服务引入了开始表单(start form)与任务表单(task form)的概念。 开始表单是在流程实例启动前显示的表单,而任务表单是用户完成任务时显示的表单。Flowable可以在BPMN 2.0流程定义中定义这些表单。表单服务通过简单的方式暴露这些数据。再次重申,表单不一定要嵌入流程定义,因此这个服务是可选的。
HistoryService暴露Flowable引擎收集的所有历史数据。当执行流程时,引擎会保存许多数据(可配置),例如流程实例启动时间、谁在执行哪个任务、完成任务花费的事件、每个流程实例的执行路径,等等。这个服务主要提供查询这些数据的能力。
ManagementService通常在用Flowable编写用户应用时不需要使用。它可以读取数据库表与表原始数据的信息,也提供了对作业(job)的查询与管理操作。Flowable中很多地方都使用作业,例如定时器(timer),异步操作(asynchronous continuation),延时暂停/激活(delayed suspension/activation)等等。后续会详细介绍这些内容。
DynamicBpmnService可用于修改流程定义中的部分内容,而不需要重新部署它。例如可以修改流程定义中一个用户任务的办理人设置,或者修改一个服务任务中的类名。
参考javadocs了解服务操作与引擎API的更多信息。
4.2. 异常策略
Flowable的异常基类是org.flowable.engine.FlowableException
,这是一个非受检异常(unchecked exception)。在任何API操作时都可能会抛出这个异常,javadoc提供了每个方法可能抛出的异常。例如,从TaskService
中摘录:
1
2
3
4
5
6 /**
* 当任务成功执行时调用。
* @param taskId 需要完成的任务id,不能为null。
* @throws FlowableObjectNotFoundException 若给定id找不到任务。
*/
void complete(String taskId);
在上例中,如果所用的id找不到任务,就会抛出异常。并且,由于javadoc中明确要求taskId不能为null,因此如果传递了null
值,会抛出FlowableIllegalArgumentException
异常。
尽管我们想避免过大的异常层次结构,但在特定情况下仍然会抛出下述异常子类。所有流程执行与API调用中发生的错误,如果不符合下面列出的异常,会统一抛出FlowableExceptions
。
-
FlowableWrongDbException
: 当Flowable引擎检测到数据库表结构版本与引擎版本不匹配时抛出。 -
FlowableOptimisticLockingException
: 当对同一数据实体的并发访问导致数据存储发生乐观锁异常时抛出。 -
FlowableClassLoadingException
: 当需要载入的类(如JavaDelegate, TaskListener, …)无法找到,或载入发生错误时抛出。 -
FlowableObjectNotFoundException
: 当请求或要操作的对象不存在时抛出。 -
FlowableIllegalArgumentException
: 当调用Flowable API时使用了不合法的参数时抛出。可能是引擎配置中的不合法值,或者是API调用传递的不合法参数,也可能是流程定义中的不合法值。 -
FlowableTaskAlreadyClaimedException
: 当对已被认领的任务调用taskService.claim(…)
时抛出。
4.3. 查询API
从引擎中查询数据有两种方式:查询API与原生(native)查询。查询API可以使用链式API,通过编程方式进行类型安全的查询。你可以在查询中增加各种条件(所有条件都用做AND逻辑),也可以明确指定排序方式。下面是示例代码:
1
2
3
4
5 List<Task> tasks = taskService.createTaskQuery()
.taskAssignee("kermit")
.processVariableValueEquals("orderId", "0815")
.orderByDueDate().asc()
.list();
有时需要更复杂的查询,例如使用OR操作符查询,或者使用查询API不能满足查询条件要求。我们为这种需求提供了可以自己写SQL查询的原生查询。返回类型由使用的查询对象决定,数据会映射到正确的对象中(Task、ProcessInstance、Execution,等等)。查询在数据库中进行,因此需要使用数据库中定义的表名与列名。这需要了解内部数据结构,因此建议小心使用原生查询。数据库表名可以通过API读取,这样可以将依赖关系减到最小。
1
2
3
4
5
6
7
8
9
10 List<Task> tasks = taskService.createNativeTaskQuery()
.sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) +
" T WHERE T.NAME_ = #{taskName}")
.parameter("taskName", "gonzoTask")
.list();
long count = taskService.createNativeTaskQuery()
.sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T1, " +
managementService.getTableName(VariableInstanceEntity.class) + " V1 WHERE V1.TASK_ID_ = T1.ID_")
.count();
4.4. 变量
流程实例按步骤执行时,需要使用一些数据。在Flowable中,这些数据称作变量(variable),并会存储在数据库中。变量可以用在表达式中(例如在排他网关中用于选择正确的出口路径),也可以在Java服务任务(service task)中用于调用外部服务(例如为服务调用提供输入或结果存储),等等。
流程实例可以持有变量(称作流程变量 process variables);用户任务以及执行(executions)——流程当前活动节点的指针——也可以持有变量。流程实例可以持有任意数量的变量,每个变量存储为ACT_RU_VARIABLE数据库表的一行。
所有的startProcessInstanceXXX方法都有一个可选参数,用于在流程实例创建及启动时设置变量。例如,在RuntimeService中:
1 ProcessInstance startProcessInstanceByKey(String processDefinitionKey, Map<String, Object> variables);
也可以在流程执行中加入变量。例如,(RuntimeService):
1
2
3
4 void setVariable(String executionId, String variableName, Object value);
void setVariableLocal(String executionId, String variableName, Object value);
void setVariables(String executionId, Map<String, ? extends Object> variables);
void setVariablesLocal(String executionId, Map<String, ? extends Object> variables);
请注意可以为给定执行(请记住,流程实例由一颗执行的树(tree of executions)组成)设置局部(local)变量。局部变量将只在该执行中可见,对执行树的上层则不可见。这可以用于 数据不应该暴露给流程实例的其他执行,或者变量在流程实例的不同路径中有不同的值(例如使用并行路径时)的情况。
可以用下列方法读取变量。请注意TaskService中有类似的方法。这意味着任务与执行一样,可以持有局部变量,其生存期为任务持续的时间。
1
2
3
4
5
6 Map<String, Object> getVariables(String executionId);
Map<String, Object> getVariablesLocal(String executionId);
Map<String, Object> getVariables(String executionId, Collection<String> variableNames);
Map<String, Object> getVariablesLocal(String executionId, Collection<String> variableNames);
Object getVariable(String executionId, String variableName);
<T> T getVariable(String executionId, String variableName, Class<T> variableClass);
变量通常用于Java代理(Java delegates)、表达式(expressions)、执行(execution)、任务监听器(tasklisteners)、脚本(scripts)等等。在这些结构中,提供了当前的execution或task对象,可用于变量的设置、读取。简单示例如下:
1
2
3
4
5
6 execution.getVariables();
execution.getVariables(Collection<String> variableNames);
execution.getVariable(String variableName);
execution.setVariables(Map<String, object> variables);
execution.setVariable(String variableName, Object value);
请注意也可以使用上例中方法的局部变量版本。
由于历史(与向后兼容)原因,当调用上述任何方法时,引擎会从数据库中取出所有变量。也就是说,如果你有10个变量,使用getVariable("myVariable")获取其中的一个,实际上其他9个变量也会从数据库取出并缓存。这并不坏,因为后续的调用可以不必再读取数据库。比如,如果流程定义包含三个连续的服务任务(因此它们在同一个数据库事务里),在第一个服务任务里通过一次调用获取全部变量,也许比在每个服务任务里分别获取需要的变量要好。请注意对读取与设置变量都是这样。
当然,如果使用大量变量,或者你希望精细控制数据库查询与流量,上述的做法就不合适了。我们引入了可以更精细控制的方法。这个方法有一个可选的参数,告诉引擎是否需要读取并缓存所有变量:
1
2
3 Map<String, Object> getVariables(Collection<String> variableNames, boolean fetchAllVariables);
Object getVariable(String variableName, boolean fetchAllVariables);
void setVariable(String variableName, Object value, boolean fetchAllVariables);
当fetchAllVariables参数为true时,行为与上面描述的完全一样:读取或设置一个变量时,所有的变量都将被读取并缓存。
而当参数值为false时,会使用明确的查询,其他变量不会被读取或缓存。只有指定的变量的值会被缓存并用于后续使用。
4.5. 瞬时变量
瞬时变量(Transient variable)类似普通变量,只是不会被持久化。通常来说,瞬时变量用于高级使用场景。如果不明确,还是使用普通流程变量为好。
瞬时变量具有下列特性:
-
瞬时变量完全不存储历史。
-
与普通变量类似,设置瞬时变量时会存入最上层父中。这意味着在一个执行中设置一个变量时,瞬时变量实际上会存储在流程实例执行中。与普通变量类似,可以使用局部(local)的对应方法,将变量设置为某个执行或任务的局部变量。
-
瞬时变量只能在下一个“等待状态”之前访问。之后该变量即消失。等待状态意味着流程实例会持久化至数据存储中。请注意在这个定义中,异步活动也是“等待状态”!
-
只能使用setTransientVariable(name, value)设置瞬时变量,但是调用getVariable(name)也会返回瞬时变量(也有getTransientVariable(name)方法,它只会返回瞬时变量)。这是为了简化表达式的撰写,并保证已有逻辑可以使用这两种类型的变量。
-
瞬时变量屏蔽(shadow)同名的持久化变量。也就是说当一个流程实例中设置了同名的持久化变量与瞬时变量时,getVariable("someVariable")会返回瞬时变量的值。
在大多数可以使用普通变量的地方,都可以获取、设置瞬时变量:
-
在JavaDelegate实现中的DelegateExecution内
-
在ExecutionListener实现中的DelegateExecution内,以及在TaskListener实现中的DelegateTask内
-
通过execution对象在脚本任务内
-
通过RuntimeService启动流程实例时
-
完成任务时
-
调用runtimeService.trigger方法时
瞬时变量相关的方法遵循普通流程变量方法的命名约定:
1
2
3
4
5
6
7
8
9
10
11
12
13 void setTransientVariable(String variableName, Object variableValue);
void setTransientVariableLocal(String variableName, Object variableValue);
void setTransientVariables(Map<String, Object> transientVariables);
void setTransientVariablesLocal(Map<String, Object> transientVariables);
Object getTransientVariable(String variableName);
Object getTransientVariableLocal(String variableName);
Map<String, Object> getTransientVariables();
Map<String, Object> getTransientVariablesLocal();
void removeTransientVariable(String variableName);
void removeTransientVariableLocal(String variableName);
下面的BPMN流程图展示了一个典型例子:
假设’Fetch Data(获取数据)'服务任务调用某个远程服务(例如使用REST)。也假设需要其需要一些配置参数,并需要在启动流程实例时提供。同时,这些配置参数对于历史审计并不重要,因此我们将它们作为瞬时变量传递:
1
2
3
4
5
6 ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
.processDefinitionKey("someKey")
.transientVariable("configParam01", "A")
.transientVariable("configParam02", "B")
.transientVariable("configParam03", "C")
.start();
请注意在到达用户任务并持久化之前,都可以使用这些瞬时变量。例如,在’Additional Work(额外工作)'用户任务中它们就不再可用。也请注意如果’Fetch Data’是异步的,则瞬时变量在该步骤之后也不再可用。
Fetch Data(的简化版本)可以像是:
1
2
3
4
5
6
7
8
9
10 public static class FetchDataServiceTask implements JavaDelegate {
public void execute(DelegateExecution execution) {
String configParam01 = (String) execution.getVariable(configParam01);
// ...
RestReponse restResponse = executeRestCall();
execution.setTransientVariable("response", restResponse.getBody());
execution.setTransientVariable("status", restResponse.getStatus());
}
}
'Process Data(处理数据)'可以获取response瞬时变量,解析并将其相关数据存储在实际流程变量中,因为之后还需要使用它们。
离开排他网关的顺序流上的条件表达式,不关注使用的是持久化还是瞬时变量(在这个例子中status是瞬时变量):
1 <conditionExpression xsi:type="tFormalExpression">${status == 200}</conditionExpression>
4.6. 表达式
Flowable使用UEL进行表达式解析。UEL代表Unified Expression Language,是EE6规范的一部分(查看EE6规范了解更多信息)。
表达式可以用于Java服务任务(Java Service task)、执行监听器(Execution Listener)、任务监听器(Task Listener) 与 条件顺序流(Conditional sequence flow)等。尽管有值表达式与方法表达式这两种不同的表达式,Flowable通过抽象,使它们都可以在需要表达式
的地方使用。
-
值表达式 Value expression: 解析为一个值。默认情况下,所有流程变量都可以使用。(若使用Spring)所有的Spring bean也可以用在表达式里。例如:
${myVar} ${myBean.myProperty}
-
方法表达式 Method expression: 调用一个方法,可以带或不带参数。当调用不带参数的方法时,要确保在方法名后添加空括号(以避免与值表达式混淆)。传递的参数可以是字面值(literal value),也可以是表达式,它们会被自动解析。例如:
${printer.print()} ${myBean.addNewOrder('orderName')} ${myBean.doSomething(myVar, execution)}
请注意,表达式支持解析(及比较)原始类型(primitive)、bean、list、array与map。 Note that these expressions support resolving primitives (including comparing them), beans, lists, arrays and maps.
除了所有流程变量外,还有一些默认对象可在表达式中使用:
-
execution
:DelegateExecution
+,持有正在运行的执行的额外信息。 -
task
:DelegateTask
持有当前任务的额外信息。请注意:只在任务监听器的表达式中可用。 -
authenticatedUserId
: 当前已验证的用户id。如果没有已验证的用户,该变量不可用。
更多实际使用例子,请查看Spring中的表达式、Java服务任务、执行监听器、任务监听器或者条件顺序流等章节。
4.7. 单元测试
业务流程是软件项目的必要组成部分,也需要使用测试一般应用逻辑的方法——单元测试——测试它们。Flowable是嵌入式的Java引擎,因此为业务流程编写单元测试就同编写一般的单元测试一样简单。
Flowable支持JUnit 3及4的单元测试风格。按照JUnit 3的风格,必须扩展(extended)org.flowable.engine.test.FlowableTestCase
。它通过保护(protected)成员变量提供对ProcessEngine与服务的访问。在测试的setup()
中,processEngine会默认使用classpath中的flowable.cfg.xml
资源初始化。如果要指定不同的配置文件,请覆盖getConfigurationResource()方法。当使用相同的配置资源时,流程引擎会静态缓存,用于多个单元测试。
通过扩展FlowableTestCase
,可以使用org.flowable.engine.test.Deployment
注解测试方法。在测试运行前,会部署与测试类在同一个包下的格式为testClassName.testMethod.bpmn20.xml
的资源文件。在测试结束时,会删除这个部署,包括所有相关的流程实例、任务,等等。也可以使用Deployment
注解显式指定资源位置。查看该类以获得更多信息。
综上所述,JUnit 3风格的测试看起来类似:
1
2
3
4
5
6
7
8
9
10
11
12
13 public class MyBusinessProcessTest extends FlowableTestCase {
@Deployment
public void testSimpleProcess() {
runtimeService.startProcessInstanceByKey("simpleProcess");
Task task = taskService.createTaskQuery().singleResult();
assertEquals("My Task", task.getName());
taskService.complete(task.getId());
assertEquals(0, runtimeService.createProcessInstanceQuery().count());
}
}
要使用JUnit 4的风格书写单元测试并达成同样的功能,必须使用org.flowable.engine.test.FlowableRule
Rule。这样能够通过它的getter获得流程引擎与服务。对于FlowableTestCase
(上例),包含@Rule
就可以使用org.flowable.engine.test.Deployment
注解(参见上例解释其用途及配置),并且会自动在classpath中寻找默认配置文件。当使用相同的配置资源时,流程引擎会静态缓存,以用于多个单元测试。
下面的代码片段展示了JUnit 4风格的测试与FlowableRule
的用法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 public class MyBusinessProcessTest {
@Rule
public FlowableRule FlowableRule = new FlowableRule();
@Test
@Deployment
public void ruleUsageExample() {
RuntimeService runtimeService = FlowableRule.getRuntimeService();
runtimeService.startProcessInstanceByKey("ruleUsage");
TaskService taskService = FlowableRule.getTaskService();
Task task = taskService.createTaskQuery().singleResult();
assertEquals("My Task", task.getName());
taskService.complete(task.getId());
assertEquals(0, runtimeService.createProcessInstanceQuery().count());
}
}
4.8. 调试单元测试
当使用H2内存数据库进行单元测试时,下面的方法可以让你在调试过程中方便地检查Flowable数据库中的数据。截图来自Eclipse,但其他IDE方式相似。
假设我们的单元测试的某处放置了断点(breakpoint)(在Eclipse里可以通过在代码左侧条上双击实现):
如果我们在debug模式(在测试类中右键,选择“Run as”,然后选择“JUnit test”)下运行单元测试,测试进程会在断点处暂停,这样我们就可以在右上窗口中查看测试中的变量。
要检查Flowable的数据,打开Display窗口(如果没有找到这个窗口,打开 Window→Show View→Other,然后选择Display),并键入(可以使用代码补全)org.h2.tools.Server.createWebServer("-web").start()
选中刚键入的行并右键点击。然后选择’Display'(或者用快捷键执行)
现在打开浏览器并访问http://localhost:8082,填入内存数据库的JDBC URL(默认为jdbc:h2:mem:flowable
),然后点击connect按钮。
这样就可以看到Flowable的数据。便于理解单元测试执行流程的方式。
4.9. Web应用中的流程引擎
ProcessEngine
是线程安全的类,可以很容易地在多个线程间共享。在web应用中,这意味着可以在容器启动时创建引擎,并在容器关闭时关闭引擎。
下面的代码片段展示了如何在Servlet环境中,通过ServletContextListener
初始化与销毁流程引擎。
1
2
3
4
5
6
7
8
9
10
11 public class ProcessEnginesServletContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent servletContextEvent) {
ProcessEngines.init();
}
public void contextDestroyed(ServletContextEvent servletContextEvent) {
ProcessEngines.destroy();
}
}
contextInitialized
方法会调用ProcessEngines.init()
。它会在classpath中查找flowable.cfg.xml
资源文件,并为每个文件分别创建ProcessEngine
(如果多个JAR都包含配置文件)。如果在classpath中有多个这样的资源文件,请确保它们使用不同的引擎名。需要使用流程引擎时,可以这样获取:
1 ProcessEngines.getDefaultProcessEngine()
或者
1 ProcessEngines.getProcessEngine("myName");
当然,就像配置章节中介绍的,还可以使用各种不同的方式创建流程引擎。
context-listener的contextDestroyed
方法会调用ProcessEngines.destroy()
。它会妥善关闭所有已初始化的流程引擎。
5. 集成Spring
尽管完全可以脱离Spring使用Flowable,我们仍提供了很多非常好的集成特性,并将在这一章节介绍。
5.1. ProcessEngineFactoryBean
可以将ProcessEngine
配置为普通的Spring bean。入口是org.flowable.spring.ProcessEngineFactoryBean
类。这个bean处理流程引擎配置,并创建流程引擎。所以在Spring中创建与设置的参数与配置章节中介绍的相同。Spring集成所用的配置与引擎bean为:
1
2
3
4
5
6
7 <bean id="processEngineConfiguration" class="org.flowable.spring.SpringProcessEngineConfiguration">
...
</bean>
<bean id="processEngine" class="org.flowable.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration" />
</bean>
请注意processEngineConfiguration
bean现在使用org.flowable.spring.SpringProcessEngineConfiguration
类。
5.2. 事务
我们会逐步说明(Flowable)发行版里,Spring示例中的SpringTransactionIntegrationTest
。下面是我们示例中使用的Spring配置文件(SpringTransactionIntegrationTest-context.xml)。下面的小节包含了dataSource(数据源),transactionManager(事务管理器),processEngine(流程引擎)以及Flowable引擎服务。
将DataSource传递给SpringProcessEngineConfiguration
(使用“dataSource”参数)时,Flowable会在内部使用org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
对得到的数据源进行包装(wrap)。这是为了保证从数据源获取的SQL连接与Spring的事务可以协同工作。这样也就不需要在Spring配置中对数据源进行代理(proxy)。但仍然可以将代理TransactionAwareDataSourceProxy
传递给SpringProcessEngineConfiguration
——在这种情况下,不会再进行包装。
请确保如果自行在Spring配置中声明了TransactionAwareDataSourceProxy
,则不要将它用在已经配置Spring事务的资源上(例如DataSourceTransactionManager与JPATransactionManager。它们需要未经代理的数据源)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="org.h2.Driver" />
<property name="url" value="jdbc:h2:mem:flowable;DB_CLOSE_DELAY=1000" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="processEngineConfiguration" class="org.flowable.spring.SpringProcessEngineConfiguration">
<property name="dataSource" ref="dataSource" />
<property name="transactionManager" ref="transactionManager" />
<property name="databaseSchemaUpdate" value="true" />
<property name="asyncExecutorActivate" value="false" />
</bean>
<bean id="processEngine" class="org.flowable.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration" />
</bean>
<bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService" />
<bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService" />
<bean id="taskService" factory-bean="processEngine" factory-method="getTaskService" />
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService" />
<bean id="managementService" factory-bean="processEngine" factory-method="getManagementService" />
...
这个Spring配置文件也包含了在这个示例中要用到的bean与配置:
1
2
3
4
5
6
7
8
9
10
11 <beans>
...
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="userBean" class="org.flowable.spring.test.UserBean">
<property name="runtimeService" ref="runtimeService" />
</bean>
<bean id="printer" class="org.flowable.spring.test.Printer" />
</beans>
可以使用任何Spring支持的方式创建应用上下文(application context)。在这个例子中,可以使用classpath中的XML资源配置来创建Spring应用上下文:
1
2 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(
"org/flowable/examples/spring/SpringTransactionIntegrationTest-context.xml");
或者在单元测试中:
1
2 @ContextConfiguration(
"classpath:org/flowable/spring/test/transaction/SpringTransactionIntegrationTest-context.xml")
然后就可以获取服务bean,并调用它们的方法。ProcessEngineFactoryBean会为服务加上额外的拦截器(interceptor),并为Flowable服务方法设置Propagation.REQUIRED事务级别。这样,我们就可以使用repositoryService部署流程:
1
2
3
4
5
6
7 RepositoryService repositoryService =
(RepositoryService) applicationContext.getBean("repositoryService");
String deploymentId = repositoryService
.createDeployment()
.addClasspathResource("org/flowable/spring/test/hello.bpmn20.xml")
.deploy()
.getId();
还有另一种方法也可以使用。如果userBean.hello()方法在Spring事务中,Flowable服务方法调用就会加入这个事务。
1
2 UserBean userBean = (UserBean) applicationContext.getBean("userBean");
userBean.hello();
UserBean看起来像下面这样。请记着在上面的Spring bean配置中,我们已经将repositoryService注入了userBean。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class UserBean {
/** 已经由Spring注入 */
private RuntimeService runtimeService;
@Transactional
public void hello() {
// 可以在你的领域模型(domain model)中进行事务操作,
// 它会与Flowable RuntimeService的startProcessInstanceByKey
// 合并在同一个事务里
runtimeService.startProcessInstanceByKey("helloProcess");
}
public void setRuntimeService(RuntimeService runtimeService) {
this.runtimeService = runtimeService;
}
}
5.3. 表达式
当使用ProcessEngineFactoryBean时,默认BPMN流程中所有的表达式都可以“看见”所有的Spring bean。可以通过配置的map,限制表达式能使用的bean,甚至可以完全禁止表达式使用bean。下面的例子只暴露了一个bean(printer),可以使用“printer”作为key访问。要完全禁止表达式使用bean,可以将SpringProcessEngineConfiguration的‘beans’参数设为空list。如果不设置‘beans’参数,则上下文中的所有bean都将可以使用。
1
2
3
4
5
6
7
8
9
10 <bean id="processEngineConfiguration" class="org.flowable.spring.SpringProcessEngineConfiguration">
...
<property name="beans">
<map>
<entry key="printer" value-ref="printer" />
</map>
</property>
</bean>
<bean id="printer" class="org.flowable.examples.spring.Printer" />
这样就可以在表达式中使用这个bean了。例如,SpringTransactionIntegrationTest hello.bpmn20.xml
展示了如何通过UEL方法表达式(method expression)调用Spring bean:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <definitions id="definitions">
<process id="helloProcess">
<startEvent id="start" />
<sequenceFlow id="flow1" sourceRef="start" targetRef="print" />
<serviceTask id="print" flowable:expression="#{printer.printMessage()}" />
<sequenceFlow id="flow2" sourceRef="print" targetRef="end" />
<endEvent id="end" />
</process>
</definitions>
其中Printer
为:
1
2
3
4
5
6 public class Printer {
public void printMessage() {
System.out.println("hello world");
}
}
Spring bean配置(上面已经展示过)为:
1
2
3
4
5
6 <beans>
...
<bean id="printer" class="org.flowable.examples.spring.Printer" />
</beans>
5.4. 自动部署资源
集成Spring也提供了部署资源的特殊方式。在流程引擎配置中,可以指定一组资源。当创建流程引擎时,会扫描并部署这些资源。可以用过滤器阻止重复部署:只有当资源确实发生变化时,才会重新部署至Flowable数据库。在Spring容器经常重启(例如测试时)的时候,这很有用。
这里有个例子:
1
2
3
4
5
6
7
8
9 <bean id="processEngineConfiguration" class="org.flowable.spring.SpringProcessEngineConfiguration">
...
<property name="deploymentResources"
value="classpath*:/org/flowable/spring/test/autodeployment/autodeploy.*.bpmn20.xml" />
</bean>
<bean id="processEngine" class="org.flowable.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration" />
</bean>
默认情况下,上面的配置方式会将符合这个过滤器的所有资源组织在一起,作为Flowable引擎的一个部署。重复检测过滤器将作用于整个部署,避免重复地部署未改变资源。有时这不是你想要的。例如,如果用这种方式部署了一组资源,即使只有其中的一个资源发生了改变,整个部署都会被视作已改变,因此这个部署中所有的所有流程定义都会被重新部署。这将导致每个流程定义都会刷新版本号(流程定义id会变化),即使实际上只有一个流程发生了变化。
可以使用SpringProcessEngineConfiguration
中的额外参数+deploymentMode+,定制部署的方式。这个参数定义了对于一组符合过滤器的资源,组织部署的方式。默认这个参数有3个可用值:
-
default
: 将所有资源组织在一个部署中,整体用于重复检测过滤。这是默认值,在未设置这个参数时也会用这个值。 -
single-resource
: 为每个资源创建一个单独的部署,并用于重复检测过滤。如果希望单独部署每一个流程定义,并且只有在它发生变化时才创建新的流程定义版本,就应该使用这个值。 -
resource-parent-folder
: 为同一个目录下的资源创建一个单独的部署,并用于重复检测过滤。这个参数值可以为大多数资源创建独立的部署。同时仍可以通过将部分资源放在同一个目录下,将它们组织在一起。这里有一个将deploymentMode
设置为single-resource
的例子:
1
2
3
4
5
6 <bean id="processEngineConfiguration"
class="org.flowable.spring.SpringProcessEngineConfiguration">
...
<property name="deploymentResources" value="classpath*:/flowable/*.bpmn" />
<property name="deploymentMode" value="single-resource" />
</bean>
如果上述deploymentMode
的参数值不能满足要求,还可以自定义组织部署的行为。创建SpringProcessEngineConfiguration
的子类,并覆盖getAutoDeploymentStrategy(String deploymentMode)
方法。这个方法用于确定对给定的deploymentMode
参数值,应使用何种部署策略。
5.5. 单元测试
与Spring集成后,业务流程可以非常简单地使用标准的 Flowable测试工具进行测试。下面的例子展示了如何通过典型的基于Spring的单元测试,对业务流程进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:org/flowable/spring/test/junit4/springTypicalUsageTest-context.xml")
public class MyBusinessProcessTest {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
@Rule
public FlowableRule flowableSpringRule;
@Test
@Deployment
public void simpleProcessTest() {
runtimeService.startProcessInstanceByKey("simpleProcess");
Task task = taskService.createTaskQuery().singleResult();
assertEquals("My Task", task.getName());
taskService.complete(task.getId());
assertEquals(0, runtimeService.createProcessInstanceQuery().count());
}
}
请注意要让这个例子可以正常工作,需要在Spring配置中定义org.flowable.engine.test.FlowableRule bean(在上面的例子中通过@Autowire注入)。
1
2
3 <bean id="flowableRule" class="org.flowable.engine.test.Flowable">
<property name="processEngine" ref="processEngine" />
</bean>
5.6. 通过Hibernate 4.2.x使用JPA
要在Flowable引擎的服务任务或者监听器逻辑中使用Hibernate 4.2.x JPA,需要添加Spring ORM的额外依赖。对Hibernate 4.1.x或更低则不需要。需要添加的依赖为:
1
2
3
4
5 <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${org.springframework.version}</version>
</dependency>
5.7. Spring Boot
Spring Boot是一个应用框架。按照其官网的介绍,可以轻松地创建独立运行的,生产级别的,基于Spring的应用,并且可以“直接运行”。它基于约定大于配置的原则使用Spring框架与第三方库,使你可以轻松地开始使用。大多数Spring Boot应用只需要很少的Spring配置。
要获得更多关于Spring Boot的信息,请查阅http://projects.spring.io/spring-boot/
Flowable与Spring Boot的集成是我们与Spring的提交者共同开发的。
5.7.1. 兼容性
Flowable使用同一个starter支持Spring Boot 2.0及1.5。主要支持Spring Boot 2.0。所以监控(actuator) endpoint只支持2.0。Flowable的starter直接引用Spring Boot starter,所以如果需要使用1.5版的Spring Boot starter,需要自行定义。
5.7.2. 开始
Spring Boot提倡约定大于配置。要开始工作,只需在项目中添加flowable-spring-boot-starter或flowable-spring-boot-starter-rest依赖。如果不需要引入所有的引擎,可以查看其它的Flowable starter。 如使用Maven:
1
2
3
4
5 <dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>${flowable.version}</version>
</dependency>
就这么简单。这个依赖会自动向classpath添加正确的Flowable与Spring依赖。现在可以编写Spring Boot应用了:
1
2
3
4
5
6
7
8
9
10
11 import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Flowable需要数据库来存储数据。运行上面的代码会得到异常提示,指出需要在classpath中添加数据库驱动依赖。现在添加H2数据库依赖:
1
2
3
4
5 <dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>
应用这次可以启动了。你会看到类似这样的输出:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.0.0.RELEASE) MyApplication : Starting MyApplication on ... MyApplication : No active profile set, falling back to default profiles: default ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@4fdfa676: startup date [Wed Mar 28 12:04:00 CEST 2018]; root of context hierarchy o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) o.apache.catalina.core.StandardService : Starting service [Tomcat] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.28 o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 3085 ms o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/] o.s.b.w.servlet.ServletRegistrationBean : Servlet Flowable IDM Rest API mapped to [/idm-api/*] o.s.b.w.servlet.ServletRegistrationBean : Servlet Flowable Form Rest API mapped to [/form-api/*] o.s.b.w.servlet.ServletRegistrationBean : Servlet Flowable DMN Rest API mapped to [/dmn-api/*] o.s.b.w.servlet.ServletRegistrationBean : Servlet Flowable Content Rest API mapped to [/content-api/*] o.s.b.w.servlet.ServletRegistrationBean : Servlet Flowable CMMN Rest API mapped to [/cmmn-api/*] o.s.b.w.servlet.ServletRegistrationBean : Servlet Flowable BPMN Rest API mapped to [/process-api/*] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*] uration$$EnhancerBySpringCGLIB$$3d0c70ac : No deployment resources were found for autodeployment uration$$EnhancerBySpringCGLIB$$8131eb1a : No deployment resources were found for autodeployment o.f.e.i.c.ProcessEngineConfigurationImpl : Found 5 Engine Configurators in total: o.f.e.i.c.ProcessEngineConfigurationImpl : class org.flowable.spring.configurator.SpringIdmEngineConfigurator (priority:100000) o.f.e.i.c.ProcessEngineConfigurationImpl : class org.flowable.dmn.spring.configurator.SpringDmnEngineConfigurator (priority:200000) o.f.e.i.c.ProcessEngineConfigurationImpl : class org.flowable.form.spring.configurator.SpringFormEngineConfigurator (priority:300000) o.f.e.i.c.ProcessEngineConfigurationImpl : class org.flowable.content.spring.configurator.SpringContentEngineConfigurator (priority:400000) o.f.e.i.c.ProcessEngineConfigurationImpl : class org.flowable.cmmn.spring.configurator.SpringCmmnEngineConfigurator (priority:500000) o.f.e.i.c.ProcessEngineConfigurationImpl : Executing beforeInit() of class org.flowable.spring.configurator.SpringIdmEngineConfigurator (priority:100000) o.f.e.i.c.ProcessEngineConfigurationImpl : Executing beforeInit() of class org.flowable.dmn.spring.configurator.SpringDmnEngineConfigurator (priority:200000) o.f.e.i.c.ProcessEngineConfigurationImpl : Executing beforeInit() of class org.flowable.form.spring.configurator.SpringFormEngineConfigurator (priority:300000) o.f.e.i.c.ProcessEngineConfigurationImpl : Executing beforeInit() of class org.flowable.content.spring.configurator.SpringContentEngineConfigurator (priority:400000) o.f.e.i.c.ProcessEngineConfigurationImpl : Executing beforeInit() of class org.flowable.cmmn.spring.configurator.SpringCmmnEngineConfigurator (priority:500000) com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. o.f.e.i.c.ProcessEngineConfigurationImpl : Executing configure() of class org.flowable.spring.configurator.SpringIdmEngineConfigurator (priority:100000) .d.AbstractSqlScriptBasedDbSchemaManager : performing create on identity with resource org/flowable/idm/db/create/flowable.h2.create.identity.sql o.f.idm.engine.impl.IdmEngineImpl : IdmEngine default created o.f.e.i.c.ProcessEngineConfigurationImpl : Executing configure() of class org.flowable.dmn.spring.configurator.SpringDmnEngineConfigurator (priority:200000) o.f.dmn.engine.impl.DmnEngineImpl : DmnEngine default created o.f.e.i.c.ProcessEngineConfigurationImpl : Executing configure() of class org.flowable.form.spring.configurator.SpringFormEngineConfigurator (priority:300000) o.f.form.engine.impl.FormEngineImpl : FormEngine default created o.f.e.i.c.ProcessEngineConfigurationImpl : Executing configure() of class org.flowable.content.spring.configurator.SpringContentEngineConfigurator (priority:400000) o.f.c.engine.ContentEngineConfiguration : Content file system root : ... o.f.c.engine.impl.ContentEngineImpl : ContentEngine default created o.f.e.i.c.ProcessEngineConfigurationImpl : Executing configure() of class org.flowable.cmmn.spring.configurator.SpringCmmnEngineConfigurator (priority:500000) o.f.cmmn.engine.CmmnEngineConfiguration : Found 1 Engine Configurators in total: o.f.cmmn.engine.CmmnEngineConfiguration : class org.flowable.cmmn.engine.impl.cfg.IdmEngineConfigurator (priority:100000) o.f.cmmn.engine.CmmnEngineConfiguration : Executing beforeInit() of class org.flowable.cmmn.engine.impl.cfg.IdmEngineConfigurator (priority:100000) o.f.cmmn.engine.CmmnEngineConfiguration : Executing configure() of class org.flowable.cmmn.engine.impl.cfg.IdmEngineConfigurator (priority:100000) o.f.idm.engine.impl.IdmEngineImpl : IdmEngine default created o.f.cmmn.engine.impl.CmmnEngineImpl : CmmnEngine default created o.f.engine.impl.ProcessEngineImpl : ProcessEngine default created o.f.j.s.i.a.AbstractAsyncExecutor : Starting up the async job executor [org.flowable.spring.job.service.SpringAsyncExecutor]. o.f.j.s.i.a.AcquireAsyncJobsDueRunnable : starting to acquire async jobs due o.f.j.s.i.a.AcquireTimerJobsRunnable : starting to acquire async jobs due o.f.j.s.i.a.ResetExpiredJobsRunnable : starting to reset expired jobs o.f.e.impl.cmd.ValidateV5EntitiesCmd : Total of v5 deployments found: 0 s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@4fdfa676: startup date [Wed Mar 28 12:04:00 CEST 2018]; root of context hierarchy s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest) s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase -20 o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 20 o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' flowable.Application : Started Application in 18.235 seconds (JVM running for 19.661)
只是在classpath中添加依赖,并使用@SpringBootAplication注解,就会在幕后发生很多事情:
-
自动创建了内存数据库(因为classpath中有H2驱动),并传递给Flowable流程引擎配置
-
创建并暴露了Flowable的ProcessEngine、CmmnEngine、DmnEngine、FormEngine、ContentEngine及IdmEngine bean
-
所有的Flowable服务都暴露为Spring bean
-
创建了Spring Job Executor
并且:
-
processes目录下的任何BPMN 2.0流程定义都会被自动部署。创建processes目录,并在其中创建示例流程定义(命名为one-task-process.bpmn20.xml)。
-
cases目录下的任何CMMN 1.1事例都会被自动部署。
-
forms目录下的任何Form定义都会被自动部署。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <?xml version="1.0" encoding="UTF-8"?>
<definitions
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples">
<process id="oneTaskProcess" name="The One Task Process">
<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
<userTask id="theTask" name="my task" />
<sequenceFlow id="flow2" sourceRef="theTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
</definitions>
然后添加下列代码,以测试部署是否生效。CommandLineRunner是一个特殊的Spring bean,在应用启动时执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 @SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
@Bean
public CommandLineRunner init(final RepositoryService repositoryService,
final RuntimeService runtimeService,
final TaskService taskService) {
return new CommandLineRunner() {
@Override
public void run(String... strings) throws Exception {
System.out.println("Number of process definitions : "
+ repositoryService.createProcessDefinitionQuery().count());
System.out.println("Number of tasks : " + taskService.createTaskQuery().count());
runtimeService.startProcessInstanceByKey("oneTaskProcess");
System.out.println("Number of tasks after process start: "
+ taskService.createTaskQuery().count());
}
};
}
}
会得到这样的输出:
Number of process definitions : 1 Number of tasks : 0 Number of tasks after process start : 1
5.7.3. 更换数据源与连接池
上面也提到过,Spring Boot的约定大于配置。默认情况下,如果classpath中只有H2,就会创建内存数据库,并传递给Flowable流程引擎配置。
只要添加一个数据库驱动的依赖并提供数据库URL,就可以更换数据源。例如,要切换到MySQL数据库:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/flowable-spring-boot?characterEncoding=UTF-8
spring.datasource.username=flowable
spring.datasource.password=flowable
从Maven依赖中移除H2,并在classpath中添加MySQL驱动:
1
2
3
4
5 <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
这次应用启动后,可以看到使用了MySQL作为数据库(并使用HikariCP连接池框架):
org.flowable.engine.impl.db.DbSqlSession : performing create on engine with resource org/flowable/db/create/flowable.mysql.create.engine.sql org.flowable.engine.impl.db.DbSqlSession : performing create on history with resource org/flowable/db/create/flowable.mysql.create.history.sql org.flowable.engine.impl.db.DbSqlSession : performing create on identity with resource org/flowable/db/create/flowable.mysql.create.identity.sql
多次重启应用,会发现任务的数量增加了(H2内存数据库在关闭后会丢失,而MySQL不会)。
关于配置数据源的更多信息,可以在Spring Boot的参考手册中Configure a DataSource(配置数据源)章节查看。
5.7.4. REST 支持
通常会在嵌入的Flowable引擎之上,使用REST API(用于与公司的不同服务交互)。Spring Boot让这变得很容易。在classpath中添加下列依赖:
1
2
3
4
5 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
创建一个新的Spring服务类,并创建两个方法:一个用于启动流程,另一个用于获得给定任务办理人的任务列表。在这里只是简单地包装了Flowable调用,但在实际使用场景中会比这复杂得多。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 @Service
public class MyService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Transactional
public void startProcess() {
runtimeService.startProcessInstanceByKey("oneTaskProcess");
}
@Transactional
public List<Task> getTasks(String assignee) {
return taskService.createTaskQuery().taskAssignee(assignee).list();
}
}
现在可以用@RestController来注解类,以创建REST endpoint。在这里我们简单地调用上面定义的服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 @RestController
public class MyRestController {
@Autowired
private MyService myService;
@RequestMapping(value="/process", method= RequestMethod.POST)
public void startProcessInstance() {
myService.startProcess();
}
@RequestMapping(value="/tasks", method= RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE)
public List<TaskRepresentation> getTasks(@RequestParam String assignee) {
List<Task> tasks = myService.getTasks(assignee);
List<TaskRepresentation> dtos = new ArrayList<TaskRepresentation>();
for (Task task : tasks) {
dtos.add(new TaskRepresentation(task.getId(), task.getName()));
}
return dtos;
}
static class TaskRepresentation {
private String id;
private String name;
public TaskRepresentation(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
Spring Boot会自动扫描组件,并找到我们添加在应用类上的@Service与@RestController。再次运行应用,现在可以与REST API交互了。例如使用cURL:
curl http://localhost:8080/tasks?assignee=kermit [] curl -X POST http://localhost:8080/process curl http://localhost:8080/tasks?assignee=kermit [{"id":"10004","name":"my task"}]
5.7.5. JPA 支持
要为Spring Boot中的Flowable添加JPA支持,增加下列依赖:
1
2
3
4
5
6
7
8
9
10 <dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version</version>
</dependency>
这会加入JPA用的Spring配置以及bean。默认使用Hibernate作为JPA提供者。
创建一个简单的实体类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 @Entity
class Person {
@Id
@GeneratedValue
private Long id;
private String username;
private String firstName;
private String lastName;
private Date birthDate;
public Person() {
}
public Person(String username, String firstName, String lastName, Date birthDate) {
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.birthDate = birthDate;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Date getBirthDate() {
return birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
}
默认情况下,如果没有使用内存数据库则不会自动创建数据库表。在classpath中创建application.properties文件并加入下列参数:
spring.jpa.hibernate.ddl-auto=update
添加下列类:
1
2
3
4
5 @Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
Person findByUsername(String username);
}
这是一个Spring repository,提供了直接可用的增删改查。我们添加了通过username查找Person的方法。Spring会基于约定自动实现它(也就是使用names属性)。
现在进一步增强我们的服务:
-
在类上添加@Transactional。请注意,通过上面添加的JPA依赖,之前使用的DataSourceTransactionManager会自动替换为JpaTransactionManager。
-
startProcess增加了任务办理人入参,用于查找Person,并将Person JPA对象作为流程变量存入流程实例。
-
添加了创建示例用户的方法。CommandLineRunner使用它打桩数据库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 @Service
@Transactional
public class MyService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private PersonRepository personRepository;
public void startProcess(String assignee) {
Person person = personRepository.findByUsername(assignee);
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("person", person);
runtimeService.startProcessInstanceByKey("oneTaskProcess", variables);
}
public List<Task> getTasks(String assignee) {
return taskService.createTaskQuery().taskAssignee(assignee).list();
}
public void createDemoUsers() {
if (personRepository.findAll().size() == 0) {
personRepository.save(new Person("jbarrez", "Joram", "Barrez", new Date()));
personRepository.save(new Person("trademakers", "Tijs", "Rademakers", new Date()));
}
}
}
CommandLineRunner现在为:
1
2
3
4
5
6
7
8
9 @Bean
public CommandLineRunner init(final MyService myService) {
return new CommandLineRunner() {
public void run(String... strings) throws Exception {
myService.createDemoUsers();
}
};
}
RestController也有小改动(只展示新方法),以配合上面的修改。HTTP POST使用body传递办理人用户名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 @RestController
public class MyRestController {
@Autowired
private MyService myService;
@RequestMapping(value="/process", method= RequestMethod.POST)
public void startProcessInstance(@RequestBody StartProcessRepresentation startProcessRepresentation) {
myService.startProcess(startProcessRepresentation.getAssignee());
}
...
static class StartProcessRepresentation {
private String assignee;
public String getAssignee() {
return assignee;
}
public void setAssignee(String assignee) {
this.assignee = assignee;
}
}
最后,为了试用Spring-JPA-Flowable集成,我们在流程定义中,将Person JPA对象的ID指派为任务办理人:
1 <userTask id="theTask" name="my task" flowable:assignee="${person.id}"/>
现在可以在POST body中提供用户名,启动一个新的流程实例:
curl -H "Content-Type: application/json" -d '{"assignee" : "jbarrez"}' http://localhost:8080/process
可以使用Person id获取任务列表:
curl http://localhost:8080/tasks?assignee=1 [{"id":"12505","name":"my task"}]
5.7.6. Flowable Actuator Endpoint
Flowable提供了Spring Boot Actuator Endpoint,以提供正在运行的流程的信息。
默认情况下 flowable
端点映射至 /actuator/flowable
。
Spring Boot默认只提供 info
与 health
端点。要启用 flowable
端点,需要在 application.properties
中添加 management.endpoint.flowable.enabled=true
。
curl http://localhost:8080/actuator/flowable
{
"completedTaskCountToday": 0,
"deployedProcessDefinitions": [
"oneTaskProcess (v1)"
],
"processDefinitionCount": 1,
"cachedProcessDefinitionCount": 0,
"runningProcessInstanceCount": {
"oneTaskProcess (v1)": 0
},
"completedTaskCount": 2,
"completedActivities": 3,
"completedProcessInstanceCount": {
"oneTaskProcess (v1)": 0
},
"openTaskCount": 0
}
要了解Spring Boot Actuator的更多信息,可以在Spring Boot参考手册中查看Production Ready Endpoint(生产可用的端点)。
5.7.7. Flowable Info Contributor
Flowable也提供了Spring Boot的 InfoContributor
:
curl http://localhost:8080/actuator/info
{
"flowable": {
"version": "6.3.0.1"
}
}
5.7.8. 配置Flowable应用
Flowable会自动配置用于控制Spring Boot的参数与配置。参见Spring Boot参考手册中的Properties and Configuration(参数与配置)。
下面是Flowable Spring Boot支持的配置参数列表。
# ===================================================================
# Common Flowable Spring Boot Properties
# 通用Flowable Spring Boot参数
#
# This sample file is provided as a guideline. Do NOT copy it in its
# entirety to your own application. ^^^
# 本示例文件只作为指导。请不要直接拷贝至你自己的应用中。
# ===================================================================
# Core (Process) FlowableProperties
# 核心(流程)
flowable.check-process-definitions=true # 是否需要自动部署流程定义。
flowable.custom-mybatis-mappers= # 需要添加至引擎的自定义Mybatis映射的FQN。
flowable.custom-mybatis-x-m-l-mappers= # 需要添加至引擎的自定义Mybatis XML映射的路径。
flowable.database-schema= # 如果数据库返回的元数据不正确,可以在这里设置schema用于检测/生成表。
flowable.database-schema-update=true # 数据库schema更新策略。
flowable.db-history-used=true # 是否要使用db历史。
flowable.deployment-name=SpringBootAutoDeployment # 自动部署的名称。
flowable.history-level= # 要使用的历史级别。
flowable.process-definition-location-prefix=classpath*:/processes/ # 自动部署时查找流程的目录。
flowable.process-definition-location-suffixes=**.bpmn20.xml,**.bpmn # 'processDefinitionLocationPrefix'路径下需要部署的文件的后缀(扩展名)。
# Process FlowableProcessProperties
# 流程
flowable.process.definition-cache-limit=-1 # 流程定义缓存中保存流程定义的最大数量。默认值为-1(缓存所有流程定义)。
flowable.process.enable-safe-xml=true # 在解析BPMN XML文件时进行额外检查。参见 https://www.flowable.org/docs/userguide/index.html#advanced.safe.bpmn.xml 。不幸的是,部分平台(JDK 6,JBoss)上无法使用这个功能,因此如果你所用的平台在XML解析时不支持StaxSource,需要禁用这个功能。
flowable.process.servlet.load-on-startup=-1 # 启动时加载Process servlet。
flowable.process.servlet.name=Flowable BPMN Rest API # Process servlet的名字。
flowable.process.servlet.path=/process-api # Process servelet的context path。
# Process Async Executor
# 流程异步执行器
flowable.process.async-executor-activate=true # 是否启用异步执行器。
flowable.process.async.executor.async-job-lock-time-in-millis=300000 # 异步作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。
flowable.process.async.executor.default-async-job-acquire-wait-time-in-millis=10000 # 异步作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的异步作业,或者只取到很少的异步作业时生效。默认值 = 10秒。
flowable.process.async.executor.default-queue-size-full-wait-time-in-millis=0 # 异步作业(包括定时器作业与异步执行)获取线程在队列满时,等待执行下次查询的等待时间(以毫秒计)。默认值为0(以向后兼容)
flowable.process.async.executor.default-timer-job-acquire-wait-time-in-millis=10000 # 定时器作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的定时器作业,或者只取到很少的定时器作业时生效。默认值 = 10秒。
flowable.process.async.executor.max-async-jobs-due-per-acquisition=1 # (译者补)单次查询的异步作业数量。默认值为1,以降低乐观锁异常的可能性。除非你知道自己在做什么,否则请不要修改这个值。
flowable.process.async.executor.retry-wait-time-in-millis=500 # ???(译者补不了了)
flowable.process.async.executor.timer-lock-time-in-millis=300000 # 定时器作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。
# CMMN FlowableCmmnProperties
flowable.cmmn.deploy-resources=true # 是否部署资源。默认值为'true'。
flowable.cmmn.deployment-name=SpringBootAutoDeployment # CMMN资源部署的名字。
flowable.cmmn.enable-safe-xml=true # 在解析CMMN XML文件时进行额外检查。参见 https://www.flowable.org/docs/userguide/index.html#advanced.safe.bpmn.xml 。不幸的是,部分平台(JDK 6,JBoss)上无法使用这个功能,因此如果你所用的平台在XML解析时不支持StaxSource,需要禁用这个功能。
flowable.cmmn.enabled=true # 是否启用CMMN引擎。
flowable.cmmn.resource-location=classpath*:/cases/ # CMMN资源的路径。
flowable.cmmn.resource-suffixes=**.cmmn,**.cmmn11,**.cmmn.xml,**.cmmn11.xml # 需要扫描的资源后缀名。
flowable.cmmn.servlet.load-on-startup=-1 # 启动时加载CMMN servlet。
flowable.cmmn.servlet.name=Flowable CMMN Rest API # CMMN servlet的名字。
flowable.cmmn.servlet.path=/cmmn-api # CMMN servlet的context path。
# CMMN Async Executor
# CMMN异步执行器
flowable.cmmn.async-executor-activate=true # 是否启用异步执行器。
flowable.cmmn.async.executor.async-job-lock-time-in-millis=300000 # 异步作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。
flowable.cmmn.async.executor.default-async-job-acquire-wait-time-in-millis=10000 # 异步作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的异步作业,或者只取到很少的异步作业时生效。默认值 = 10秒。
flowable.cmmn.async.executor.default-queue-size-full-wait-time-in-millis=0 # 异步作业(包括定时器作业与异步执行)获取线程在队列满时,等待执行下次查询的等待时间(以毫秒计)。默认值为0(以向后兼容)
flowable.cmmn.async.executor.default-timer-job-acquire-wait-time-in-millis=1000 # 定时器作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的定时器作业,或者只取到很少的定时器作业时生效。默认值 = 10秒。
flowable.cmmn.async.executor.max-async-jobs-due-per-acquisition=1 # (译者补)单次查询的异步作业数量。默认值为1,以降低乐观锁异常的可能性。除非你知道自己在做什么,否则请不要修改这个值。
flowable.cmmn.async.executor.retry-wait-time-in-millis=500 #(译者补不了了)
flowable.cmmn.async.executor.timer-lock-time-in-millis=300000 # 定时器作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。
# Content FlowableContentProperties
flowable.content.enabled=true # 是否启动Content引擎。
flowable.content.servlet.load-on-startup=-1 # 启动时加载Content servlet。
flowable.content.servlet.name=Flowable Content Rest API # Content servlet的名字。
flowable.content.servlet.path=/content-api # Content servlet的context path。
flowable.content.storage.create-root=true # 如果根路径不存在,是否需要创建?
flowable.content.storage.root-folder= # 存储content文件(如上传的任务附件,或表单文件)的根路径。
# DMN FlowableDmnProperties
flowable.dmn.deploy-resources=true # 是否部署资源。默认为'true'。
flowable.dmn.deployment-name=SpringBootAutoDeployment # DMN资源部署的名字。
flowable.dmn.enable-safe-xml=true # 在解析DMN XML文件时进行额外检查。参见 https://www.flowable.org/docs/userguide/index.html#advanced.safe.bpmn.xml 。不幸的是,部分平台(JDK 6,JBoss)上无法使用这个功能,因此如果你所用的平台在XML解析时不支持StaxSource,需要禁用这个功能。
flowable.dmn.enabled=true # 是否启用DMN引擎。
flowable.dmn.history-enabled=true # 是否启用DMN引擎的历史。
flowable.dmn.resource-location=classpath*:/dmn/ # DMN资源的路径。
flowable.dmn.resource-suffixes=**.dmn,**.dmn.xml,**.dmn11,**.dmn11.xml # 需要扫描的资源后缀名。
flowable.dmn.servlet.load-on-startup=-1 # 启动时加载DMN servlet。
flowable.dmn.servlet.name=Flowable DMN Rest API # DMN servlet的名字。
flowable.dmn.servlet.path=/dmn-api # DMN servlet的context path。
flowable.dmn.strict-mode=true # 如果希望避免抉择表命中策略检查导致失败,可以将本参数设置为false。如果检查发现了错误,会直接返回错误前一刻的中间结果。
# Form FlowableFormProperties
flowable.form.deploy-resources=true # 是否部署资源。默认为'true'。
flowable.form.deployment-name=SpringBootAutoDeployment # Form资源部署的名字。
flowable.form.enabled=true # 是否启用Form引擎。
flowable.form.resource-location=classpath*:/forms/ # Form资源的路径。
flowable.form.resource-suffixes=**.form # 需要扫描的资源后缀名。
flowable.form.servlet.load-on-startup=-1 # 启动时加载Form servlet。
flowable.form.servlet.name=Flowable Form Rest API # Form servlet的名字。
flowable.form.servlet.path=/form-api # Form servlet的context path。
# IDM FlowableIdmProperties
flowable.idm.enabled=true # 是否启用IDM引擎。
flowable.idm.password-encoder= # 使用的密码编码类型。
flowable.idm.servlet.load-on-startup=-1 # 启动时加载IDM servlet。
flowable.idm.servlet.name=Flowable IDM Rest API # IDM servlet的名字。
flowable.idm.servlet.path=/idm-api # IDM servlet的context path。
# IDM Ldap FlowableLdapProperties
flowable.idm.ldap.attribute.email= # 用户email的属性名。
flowable.idm.ldap.attribute.first-name= # 用户名字的属性名。
flowable.idm.ldap.attribute.group-id= # 用户组ID的属性名。
flowable.idm.ldap.attribute.group-name= # 用户组名的属性名。
flowable.idm.ldap.attribute.group-type= # 用户组类型的属性名。
flowable.idm.ldap.attribute.last-name= # 用户姓的属性名。
flowable.idm.ldap.attribute.user-id= # 用户ID的属性名。
flowable.idm.ldap.base-dn= # 查找用户与组的DN(标志名称 distinguished name)。
flowable.idm.ldap.cache.group-size=-1 # 设置{@link org.flowable.ldap.LDAPGroupCache}的大小。这是LRU缓存,用于缓存用户及组,以避免每次都查询LDAP系统。
flowable.idm.ldap.custom-connection-parameters= # 用于设置所有没有专用setter的LDAP连接参数。查看 http://docs.oracle.com/javase/tutorial/jndi/ldap/jndi.html 介绍的自定义参数。参数包括配置链接池,安全设置,等等。
flowable.idm.ldap.enabled=false # 是否启用LDAP IDM 服务。
flowable.idm.ldap.group-base-dn= # 组查找的DN。
flowable.idm.ldap.initial-context-factory=com.sun.jndi.ldap.LdapCtxFactory # 初始化上下文工厂的类名。
flowable.idm.ldap.password= # 连接LDAP系统的密码。
flowable.idm.ldap.port=-1 # LDAP系统的端口。
flowable.idm.ldap.query.all-groups= # 查询所有组所用的语句。
flowable.idm.ldap.query.all-users= # 查询所有用户所用的语句。
flowable.idm.ldap.query.groups-for-user= # 按照指定用户查询所属组所用的语句
flowable.idm.ldap.query.user-by-full-name-like= # 按照给定全名查找用户所用的语句。
flowable.idm.ldap.query.user-by-id= # 按照userId查找用户所用的语句。
flowable.idm.ldap.search-time-limit=0 # 查询LDAP的超时时间(以毫秒计)。默认值为'0',即“一直等待”。
flowable.idm.ldap.security-authentication=simple # 连接LDAP系统所用的'java.naming.security.authentication'参数的值。
flowable.idm.ldap.server= # LDAP系统的主机名。如'ldap://localhost'。
flowable.idm.ldap.user= # 连接LDAP系统的用户ID。
flowable.idm.ldap.user-base-dn= # 查找用户的DN。
# Flowable Mail FlowableMailProperties
flowable.mail.server.default-from=flowable@localhost # 发送邮件时使用的默认发信人地址。
flowable.mail.server.host=localhost # 邮件服务器。
flowable.mail.server.password= # 邮件服务器的登录密码。
flowable.mail.server.port=1025 # 邮件服务器的端口号。
flowable.mail.server.use-ssl=false # 是否使用SSL/TLS加密SMTP传输连接(即SMTPS/POPS)。
flowable.mail.server.use-tls=false # 使用或禁用STARTTLS加密。
flowable.mail.server.username= # 邮件服务器的登录用户名。如果为空,则不需要登录。
# Actuator
management.endpoint.flowable.cache.time-to-live=0ms # 缓存响应的最大时间。
management.endpoint.flowable.enabled=true # 是否启用flowable端点。
现参数 | 原参数 | 默认值 | 描述 |
---|---|---|---|
flowable.process.servlet.name |
flowable.rest-api-servlet-name |
Flowable BPMN Rest API |
Process servlet的名字。 |
flowable.process.servlet.path |
flowable.rest-api-mapping |
/process-api |
Process servlet的context path。 |
flowable.mail.server.host |
flowable.mail-server-host |
localhost |
邮件服务器。 |
flowable.mail.server.password |
flowable.mail-server-password |
- |
邮件服务器的密码。 |
flowable.mail.server.port |
flowable.mail-server-port |
1025 |
邮件服务器的端口号。 |
flowable.mail.server.use-ssl |
flowable.mail-server-use-ssl |
false |
是否使用SSL/TLS加密SMTP传输连接(即SMTPS/POPS)。 |
flowable.mail.server.use-tls |
flowable.mail-server-use-tls |
false |
使用或禁用STARTTLS加密。 |
flowable.mail.server.username |
flowable.mail-server-user-name |
- |
邮件服务器的登录用户名。如果为空,则不需要登录。 |
flowable.process.definition-cache-limit |
flowable.process-definitions.cache.max |
-1 |
流程定义缓存中保存流程定义的最大数量。默认值为-1(缓存所有流程定义)。 |
5.7.9. Flowable自动配置类
这是Flowable提供的所有自动配置类的列表,并包括了文档及源码的连接。记得查看你的应用的conditions报告,以确认具体启用了哪些功能。(使用—debug或-Ddebug或在Actuator应用中启动应用,并使用 conditions
端点)。
配置类 |
---|
5.7.10. Flowable Starter
这是Flowable Spring Boot stater的列表。
Starter | 描述 |
---|---|
提供以独立运行模式启动CMMN引擎的依赖 |
|
提供以独立运行模式启动CMMN引擎,并提供其REST API的依赖。 |
|
提供以独立运行模式启动DMN引擎的依赖。 |
|
提供以独立运行模式启动DMN引擎,并提供其REST API的依赖。 |
|
提供以独立运行模式启动流程引擎的依赖。 |
|
提供以独立运行模式启动流程引擎,并提供其REST API的依赖。 |
|
提供启动所有Flowable引擎(流程,CMMN,DMN,Form,Content及IDM)的依赖。 |
|
提供启动所有Flowable引擎,并提供其REST API的依赖。 |
|
提供Spring Boot Actuator所需的依赖。 |
5.7.11. 使用Liquibase
Flowable引擎使用Liquibase管理数据库版本。
因此Spring Boot的 LiquibaseAutoConfiguration
会自动启用。
然而,如果你并未使用Liquibase,则应用将无法启动,并抛出异常。
因此Flowable将 spring.liquibase.enabled
设置为 false
,也即如果需要使用Liquibase,则需手动启用它。
5.7.12. 扩展阅读
很明显还有很多Spring Boot相关的内容还没有提及,如非常简单的JTA集成、构建能在主流应用服务器上运行的WAR文件。也还有很多Spring Boot集成:
-
Actuator支持
-
Spring Integration支持
-
Rest API集成:启动Spring应用中嵌入的Flowable Rest API
-
Spring Security支持
5.7.13. 高级配置
自定义引擎配置
实现org.flowable.spring.boot.EngineConfigurationConfigurer<T>接口,可以获取引擎配置对象。其中T是具体引擎配置的Spring类型。 这样可以在参数尚未公开时,进行高级配置,或简化配置。 例如:
1
2
3
4
5
6
7 public class MyConfigurer implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
public void configure(SpringProcessEngineConfiguration processEngineConfiguration) {
// advanced configuration
}
}
在Spring Boot配置中使用@Bean发布该类的实例,这样配置类会在流程引擎创建前调用。
可以用这种方法实现自定义的Flowable服务。参见 FlowableLdapAutoConfiguration |
整合starter
如果需要一组引擎,则只能依次添加依赖。 比如要使用流程、CMMN、Form与IDM引擎,并使用LDAP,则需要添加这些依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-cmmn</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-content-spring-configurator</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ldap</artifactId>
<version>${flowable.version}</version>
</dependency>
配置异步执行器
流程及CMMN引擎使用专门的 AsyncExecutor
,并可使用 flowable.{engine}.async.executor
参数组进行配置。
其中 engine
代表 process
或 cmmn
。
默认情况下, AsyncExecutor
共享同一个Spring TaskExecutor
及 SpringRejectedJobsHandler
。
如果需要为引擎提供专门的执行器,则需要使用 @Process
及 @Cmmn
定义的bean。
可以使用如下方法配置自定义的执行器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 @Configuration
public class MyConfiguration {
@Process (1)
@Bean
public TaskExecutor processTaskExecutor() {
return new SimpleAsyncTaskExecutor();
}
@Cmmn (2)
@Bean
public TaskExecutor cmmnTaskExecutor() {
return new SyncTaskExecutor();
}
}
1 | 流程引擎将使用 SimpleAsyncTaskExecutor 作为异步执行器 |
2 | CMMN引擎将使用 SyncTaskExecutor 作为异步执行器 |
如果使用了自定义的 |
6. 部署
6.1. 业务存档
要部署流程,需要将它们包装在业务存档(BAR, Business archive)里。业务存档是Flowable引擎的部署单元,也就是一个ZIP文件。可以包含BPMN 2.0流程、表单定义、DMN规则,与其他类型的文件。总的来说,业务存档包含一组具名资源。
当部署业务存档时,会扫描具有.bpmn20.xml
或.bpmn
扩展名的BPMN文件。每一个这种文件都会被处理,并可以包含多个流程定义。
当启用DMN引擎时,也会解析.dmn
。当启用表单引擎时,会处理.form
文件。
业务存档中的Java类不会添加至classpath。业务存档中,所有流程定义使用的自定义类(例如Java服务任务或者事件监听器),都需要放在运行流程的flowable引擎的classpath内。 |
6.1.1. 编程方式部署
从ZIP文件部署业务存档,可以这样做:
1
2
3
4
5
6
7 String barFileName = "path/to/process-one.bar";
ZipInputStream inputStream = new ZipInputStream(new FileInputStream(barFileName));
repositoryService.createDeployment()
.name("process-one.bar")
.addZipInputStream(inputStream)
.deploy();
也可以为单个资源构建部署。查看javadoc以获取更多信息。
6.2. 外部资源
流程定义保存在Flowable数据库中。这些流程定义在使用服务任务、执行监听器,或执行Flowable配置文件中定义的Spring bean时,可以引用委托类。这些类及Spring配置文件都需要在可能运行这个流程定义的流程引擎中可用。
6.2.1. Java类
在流程启动时,引擎的classpath中需要有所有流程中用到的自定义类(例如服务任务、事件监听器、任务监听器等中用到的JavaDelegate)。
但是在部署业务存档时,classpath中可以没有这些类。这意味着,比如使用Ant部署新业务存档时,代理类不必提前放在classpath中。
当使用演示配置且希望添加自定义类时,需要在flowable-task或flowable-rest的webapp lib中,添加包含有你的自定义类的JAR。别忘了也要添加你的自定义类的依赖(若有)。或者,也可以将你的依赖添加到Tomcat的库文件夹${tomcat.home}/lib
中。
6.2.2. 在流程中使用Spring bean
在表达式或脚本中使用Spring bean时,执行该流程定义的引擎需要可以使用这些bean。可以简单直接地自行构建web应用,并按照集成Spring章节的介绍在上下文中配置流程引擎。但也请牢记在心,如果使用Flowable task和rest web应用,就需要更新它的上下文配置。
6.2.3. 创建单独应用
如果不想费心在所有流程引擎的classpath中都包含所有需要的代理类,及保证它们都使用了正确的Spring配置,也可以考虑将Flowable rest web应用嵌入你自己的web应用。也就是说只使用一个单独的ProcessEngine
。
6.3. 流程定义的版本
BPMN并没有版本的概念。这其实很好,因为可执行的BPMN流程文件很可能已经作为开发项目的一部分,保存在版本管理系统仓库中了(例如Subversion,Git,或者Mercurial)。但是,作为部署过程的一部分,引擎会创建流程定义的版本。在部署时,Flowable会在保存至Flowable数据库前,为ProcessDefinition
指定版本。
对于业务存档中的每个流程定义,为了初始化key
、version
、name
与id
参数,会执行下列步骤:
-
XML文件中的流程定义
id
属性用作流程定义的key
参数。 -
XML文件中的流程定义
name
属性用作流程定义的name
参数。如果未给定name
属性,会使用id作为name。 -
当每个key的流程第一次部署时,指定版本为1。对其后所有使用相同key的流程定义,部署时版本会在该key当前已部署的最高版本号基础上加1。key参数用于区分流程定义。
-
id参数设置为{processDefinitionKey}:{processDefinitionVersion}:{generated-id},其中
generated-id
是一个唯一数字,用以保证在集群环境下,流程定义缓存中,流程id的唯一性。
以下面的流程为例
1
2
3 <definitions id="myDefinitions" >
<process id="myProcess" name="My important process" >
...
当部署这个流程定义时,数据库中的流程定义会是这个样子:
id | key | name | version |
---|---|---|---|
myProcess:1:676 |
myProcess |
My important process |
1 |
如果我们现在部署同一个流程的更新版本(例如修改部分用户任务),且保持流程定义的id
不变,那么流程定义表中会包含下面的记录:
id | key | name | version |
---|---|---|---|
myProcess:1:676 |
myProcess |
My important process |
1 |
myProcess:2:870 |
myProcess |
My important process |
2 |
当调用runtimeService.startProcessInstanceByKey("myProcess")
时,会使用版本2
的流程定义,因为这是这个流程定义的最新版本。
如果再创建第二个流程,如下定义并部署至Flowable,表中会增加第三行。
1
2
3 <definitions id="myNewDefinitions" >
<process id="myNewProcess" name="My important process" >
...
表数据类似:
id | key | name | version |
---|---|---|---|
myProcess:1:676 |
myProcess |
My important process |
1 |
myProcess:2:870 |
myProcess |
My important process |
2 |
myNewProcess:1:1033 |
myNewProcess |
My important process |
1 |
请注意新流程的key与第一个流程的key不同。即使name是相同的(我们也可以修改它),Flowable也只用id
属性来区分流程。因此新的流程部署时版本为1.
6.4. 提供流程图
可以在部署中添加流程图片。这个图片将存储在Flowable数据库中,并可以使用API访问。图片可以用在Flowable应用中,使流程可视化。
如果在classpath中,有一个org/flowable/expenseProcess.bpmn20.xml
流程,key为’expense'。则流程图片会使用下列命名约定(按顺序):
-
如果部署中有图片资源,并且文件名包含BPMN 2.0 XML流程定义文件名以及流程key,并具有图形格式,则使用这个图片。在我们的例子中,就是
org/flowable/expenseProcess.expense.png
(或者.jpg/gif)。使用key的原因是,一个BPMN 2.0 XML文件中可以有多个流程定义。因此使用流程key区分每一个流程图的文件。 -
如果没有这种图片,就会寻找部署中匹配BPMN 2.0 XML文件名的图片资源。在我们的例子中,就是
org/flowable/expenseProcess.png
。请注意,这意味着同一个BPMN 2.0文件中的每一个流程定义,都会使用同一个流程图图片。当然,如果每个BPMN 2.0 XML文件中都只有一个流程定义,就没有问题。
用编程方式部署的例子:
1
2
3
4
5
6
repositoryService.createDeployment()
.name("expense-process.bar")
.addClasspathResource("org/flowable/expenseProcess.bpmn20.xml")
.addClasspathResource("org/flowable/expenseProcess.png")
.deploy();
图片资源可用下面的API获取:
1
2
3
4
5
6
7 ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("expense")
.singleResult();
String diagramResourceName = processDefinition.getDiagramResourceName();
InputStream imageStream = repositoryService.getResourceAsStream(
processDefinition.getDeploymentId(), diagramResourceName);
6.5. 生成流程图
如果部署时没有按上小节介绍的方式提供图片,且流程定义中包含必要的“图形交换(diagram interchange)”信息,Flowable引擎会生成流程图。
可以用与部署时提供图片完全相同的方法获取图片资源。
如果不需要或不希望在部署时生成流程图,可以在流程引擎配置中设置isCreateDiagramOnDeploy
参数:
1 <property name="createDiagramOnDeploy" value="false" />
这样就不会生成流程图了。
6.6. 类别
部署与流程定义都可以自定义类别。流程定义的类别使用BPMN文件中targetNamespace的值设置:<definitions … targetNamespace="yourCategory" …/>
。
部署的类别也可用API如此设定:
1
2
3
4
5 repositoryService
.createDeployment()
.category("yourCategory")
...
.deploy();
7. BPMN 2.0介绍
7.1. BPMN是什么?
BPMN是一个广泛接受与支持的,展现流程的注记方法。OMG BPMN标准.
7.2. 定义流程
本篇文档假设你使用Eclipse IDE创建与编辑文件。但其实文档中只有少数几处使用了Eclipse的特性。你可以使用喜欢的任何其他工具创建BPMN 2.0 XML文件。 |
创建一个新的XML文件(在任意项目上右击,选择New→Other→XML-XML File)并命名。确保该文件名以.bpmn20.xml或.bpmn结尾,否则引擎不会在部署时使用这个文件。
BPMN 2.0概要(schema)的根元素(root element)是definitions
元素。在这个元素中,可以定义多个流程定义(然而我们建议在每个文件中,只有一个流程定义。这样可以简化之后的部署过程)。下面给出的是一个空流程定义。请注意definitions
元素最少需要包含xmlns
与targetNamespace
声明。targetNamespace
可以为空,它用于对流程定义进行分类。
1
2
3
4
5
6
7
8
9
10 <definitions
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples">
<process id="myProcess" name="My First Process">
..
</process>
</definitions>
除了使用Eclipse中的XML分类选项,也可以使用在线概要作为BPMN 2.0 XML概要。
1
2
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL
http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd
process
元素有两个属性:
-
id: 必填属性,将映射为Flowable
ProcessDefinition
对象的key参数。可以使用RuntimeService
中的startProcessInstanceByKey
方法,使用id
来启动这个流程定义的新流程实例。这个方法总会使用流程定义的最新部署版本。
1 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess");
-
请注意这与调用
startProcessInstanceById
方法不同。startProcessInstanceById
方法的参数为Flowable引擎在部署时生成的字符串ID(可以通过调用processDefinition.getId()
方法获取)。生成ID的格式为key:version,长度限制为64字符。请注意限制流程key的长度,否则会抛出FlowableException
异常,提示生成的ID过长。 -
name: 可选属性,将映射为
ProcessDefinition
的name参数。引擎本身不会使用这个参数。可以用于在用户界面上显示更友好的名字。
7.3. 开始:十分钟教程
这个章节包含了一个很简单的业务流程,用于介绍一些基本的Flowable概念以及Flowable API。
7.3.1. 必要条件
这个教程需要你已经运行了Flowable演示配置,并使用独立的H2服务器。编辑db.properties
并设置jdbc.url=jdbc:h2:tcp://localhost/flowable
,然后按照H2文档的介绍运行独立服务器。
7.3.2. 目标
这个教程的目标是学习Flowable以及BPMN 2.0的一些基础概念。最后成果是一个简单的Java SE程序,部署了一个流程定义,并可以通过Flowable引擎API与流程进行交互。当然,在这个教程里学到的东西,也可以按照你的业务流程用于构建你自己的web应用程序。
7.3.3. 用例
用例很简单:有一个公司,叫做BPMCorp。在BPMCorp中,由会计部门负责,每月需要为投资人撰写一份报告。在报告完成后,需要高层经理中的一人进行审核,然后才能发给所有投资人。
7.3.5. XML格式
这个业务流程的XML版本(FinancialReportProcess.bpmn20.xml)在下面展示。很容易认出流程的主要元素(点击链接可以跳转到BPMN 2.0结构的详细章节):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 <definitions id="definitions"
targetNamespace="http://flowable.org/bpmn20"
xmlns:flowable="http://flowable.org/bpmn"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">
<process id="financialReport" name="Monthly financial report reminder process">
<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="writeReportTask" />
<userTask id="writeReportTask" name="Write monthly financial report" >
<documentation>
Write monthly financial report for publication to shareholders.
</documentation>
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>accountancy</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>
<sequenceFlow id="flow2" sourceRef="writeReportTask" targetRef="verifyReportTask" />
<userTask id="verifyReportTask" name="Verify monthly financial report" >
<documentation>
Verify monthly financial report composed by the accountancy department.
This financial report is going to be sent to all the company shareholders.
</documentation>
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>management</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>
<sequenceFlow id="flow3" sourceRef="verifyReportTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
</definitions>
7.3.6. 启动流程实例
现在我们已经创建了业务流程的流程定义。使用这样的流程定义,可以创建流程实例。在这个例子中,一个流程实例将对应某一月份的财经报告创建与审核工作。所有月份的流程实例共享相同的流程定义。
要用给定的流程定义创建流程实例,需要首先部署(deploy)流程定义。部署流程定义意味着两件事:
-
流程定义将会存储在Flowable引擎配置的持久化数据库中。因此部署业务流程保证了引擎在重启后也能找到流程定义。
-
BPMN 2.0流程XML会解析为内存中的对象模型,供Flowable API使用。
更多关于部署的信息可以在部署章节中找到。
与该章节的描述一样,部署有很多种方式。其中一种是通过下面展示的API。请注意所有与Flowable引擎的交互都要通过它的服务(services)进行。
1
2
3 Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("FinancialReportProcess.bpmn20.xml")
.deploy();
现在可以使用在流程定义中定义的id
(参见XML中的process元素)启动新流程实例。请注意这个id
在Flowable术语中被称作key。
1 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("financialReport");
这会创建流程实例,并首先通过开始事件。在开始事件后,会沿着所有出口顺序流(在这个例子中只有一个)继续执行,并到达第一个任务(write monthly financial report 撰写月度财务报告)。这时,Flowable引擎会在持久化数据库中存储一个任务。同时也会解析并保存这个任务附加的分配用户或组。请注意,Flowable引擎会持续执行流程,直到到达等待状态(wait state),例如用户任务。在等待状态,流程实例的当前状态会存储在数据库中并保持,直到用户决定完成任务。这时,引擎会继续执行,直到遇到新的等待状态,或者流程结束。如果在这期间引擎重启或崩溃,流程的状态也仍在数据库中安全的保存。
用户任务活动是一个等待状态,因此startProcessInstanceByKey
方法会在任务创建后返回。在这个例子里,这个任务分配给一个组。这意味着这个组的每一个成员都是处理这个任务的候选人(candidate)。
现在可以将前面这些东西整合起来,构造一个简单的Java程序。创建一个新的Eclipse项目,在它的classpath中添加Flowable JAR与依赖(可以在Flowable发行版的libs目录下找到)。在调用Flowable服务前,需要首先构建ProcessEngine
,用于访问服务。这里我们使用'独立(standalone)'配置,这个配置会构建ProcessEngine
,并使用与演示配置中相同的数据库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 public static void main(String[] args) {
// 创建Flowable流程引擎
ProcessEngine processEngine = ProcessEngineConfiguration
.createStandaloneProcessEngineConfiguration()
.buildProcessEngine();
// 获取Flowable服务
RepositoryService repositoryService = processEngine.getRepositoryService();
RuntimeService runtimeService = processEngine.getRuntimeService();
// 部署流程定义
repositoryService.createDeployment()
.addClasspathResource("FinancialReportProcess.bpmn20.xml")
.deploy();
// 启动流程实例
runtimeService.startProcessInstanceByKey("financialReport");
}
7.3.7. 任务列表
现在可以通过如下代码获取这个任务:
1 List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit").list();
请注意传递给这个操作的用户需要是accountancy组的成员,因为在流程定义中是这么声明的:
1
2
3
4
5 <potentialOwner>
<resourceAssignmentExpression>
<formalExpression>accountancy</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
也可以使用任务查询API,用组名查得相同结果。可以在代码中添加下列逻辑:
1
2 TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("accountancy").list();
因为我们使用与演示配置中相同的数据库配置ProcessEngine
,因此可以直接登录Flowable IDM。使用admin/test登录,创建两个新用户kermit与fozzie,并将Access the workflow application(访问工作流应用)权限授予他们。然后创建两个组,命名为accountancy与management,并将fozzie添加至accountancy组,将kermit添加至management组。
然后以fozzie登录Flowable task应用。选择Task应用,再选择其Processes页面,选择'Monthly financial report (月度财务报告)',这样就可以启动我们的业务流程。
前面已经解释过,流程会执行直到第一个用户任务。因为登录为fozzie,所以可以看到在启动流程实例后,他有一个新的候选任务(candidate task)。选择Task页面来查看这个新任务。请注意即使流程是由其他人启动的,accountancy组中的每一个人仍然都能看到这个候选任务。
7.3.8. 申领任务
会计师(accountancy组的成员)现在需要申领任务(claim)。申领任务后,这个用户会成为任务的执行人(assignee),这个任务也会从accountancy组的其他成员的任务列表中消失。可以通过如下代码实现申领任务:
1 taskService.claim(task.getId(), "fozzie");
这个任务现在在申领任务者的个人任务列表中。
1 List<Task> tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
在Flowable Task应用中,点击claim按钮会执行相同操作。这个任务会转移到登录用户的个人任务列表中。也可以看到任务执行人变更为当前登录用户。
7.3.9. 完成任务
会计师(accountancy组的成员)现在需要开始撰写财务报告了。完成报告后,他就可以完成任务(complete),代表任务的所有工作都已完成。
1 taskService.complete(task.getId());
对于Flowable引擎来说,这是个外部信号,指示流程实例可以继续执行。Flowable会从运行时数据中移除任务,并沿着这个任务唯一的出口转移线(outgoing transition),将执行移至第二个任务('verification of the report 审核报告')。为第二个任务分配执行人的机制,与上面介绍的第一个任务使用的机制相同。唯一的区别是这个任务会分配给management组。
在演示设置中,完成任务可以通过点击任务列表中的complete按钮。因为Fozzie不是经理,我们需要登出Flowable Task应用,并用kermit(他是经理)登录。这样就可以在未分配任务列表中看到第二个任务。
7.3.10. 结束流程
可以使用与之前完全相同的方式获取并申领审核任务。完成这个第二个任务会将流程执行移至结束事件,并结束流程实例。这个流程实例,及所有相关的运行时执行数据都会从数据库中移除。
也可以通过编程方式,使用historyService
验证流程已经结束
1
2
3
4 HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult();
System.out.println("Process instance end time: " + historicProcessInstance.getEndTime());
7.3.11. 代码总结
将之前章节的所有代码片段整合起来,会得到类似这样的代码。这段代码考虑到了你可能已经使用Flowable UI应用启动了一些流程实例。代码中总是获取任务列表而不是一个任务,因此可以正确执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 public class TenMinuteTutorial {
public static void main(String[] args) {
// 创建Flowable流程引擎
ProcessEngine processEngine = ProcessEngineConfiguration
.createStandaloneProcessEngineConfiguration()
.buildProcessEngine();
// 获取Flowable服务
RepositoryService repositoryService = processEngine.getRepositoryService();
RuntimeService runtimeService = processEngine.getRuntimeService();
// 部署流程定义
repositoryService.createDeployment()
.addClasspathResource("FinancialReportProcess.bpmn20.xml")
.deploy();
// 启动流程实例
String procId = runtimeService.startProcessInstanceByKey("financialReport").getId();
// 获取第一个任务
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("accountancy").list();
for (Task task : tasks) {
System.out.println("Following task is available for accountancy group: " + task.getName());
// 申领任务
taskService.claim(task.getId(), "fozzie");
}
// 验证Fozzie获取了任务
tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
for (Task task : tasks) {
System.out.println("Task for fozzie: " + task.getName());
// 完成任务
taskService.complete(task.getId());
}
System.out.println("Number of tasks for fozzie: "
+ taskService.createTaskQuery().taskAssignee("fozzie").count());
// 获取并申领第二个任务
tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
for (Task task : tasks) {
System.out.println("Following task is available for management group: " + task.getName());
taskService.claim(task.getId(), "kermit");
}
// 完成第二个任务并结束流程
for (Task task : tasks) {
taskService.complete(task.getId());
}
// 验证流程已经结束
HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult();
System.out.println("Process instance end time: " + historicProcessInstance.getEndTime());
}
}
7.3.12. 后续增强
可以看出这个业务流程太简单了,不能实际使用。但只要继续学习Flowable中可用的BPMN 2.0结构,就可以通过以下元素增强业务流程:
-
定义网关(gateway)使经理可以选择:驳回财务报告,并重新为会计师创建任务;或者接受报告。
-
定义并使用变量(variables)存储或引用报告,并可以在表单中显示它。
-
在流程结束处定义服务任务(service task),将报告发送给每一个投资人。
-
等等。
8. BPMN 2.0结构
本章节介绍Flowable支持的BPMN 2.0结构,以及Flowable对BPMN标准的自定义扩展。
8.1. 自定义扩展
BPMN 2.0标准对流程的所有的参与者都很有用。最终用户不会因为依赖专有解决方案,而被供应商“绑架”。Flowable之类的开源框架,也可以提供与大型供应商的解决方案相同(经常是更好;-)的实现。有了BPMN 2.0标准,从大型供应商解决方案向Flowable的迁移,可以十分简单平滑。
缺点则是标准通常是不同公司(不同观点)大量讨论与妥协的结果。作为阅读BPMN 2.0 XML流程定义的开发者,有时会觉得某些结构或方法十分笨重。Flowable将开发者的感受放在最高优先,因此引入了一些Flowable BPMN扩展(extensions)。这些“扩展”并不在BPMN 2.0规格中,有些是新结构,有些是对特定结构的简化。
尽管BPMN 2.0规格明确指出可以支持自定义扩展,我们仍做了如下保证:
-
自定义扩展保证是在标准方式的基础上进行简化。因此当你决定使用自定义扩展时,不用担心无路可退(仍然可以用标准方式)。
-
使用自定义扩展时,总是通过flowable:命名空间前缀,明确标识出XML元素、属性等。请注意Flowable引擎也支持activiti:命名空间前缀。
因此是否使用自定义扩展,完全取决于你自己。有些其他因素会影响选择(图形化编辑器的使用,公司策略,等等)。我们提供扩展,只是因为相信标准中的某些地方可以用更简单或效率更高的方式处理。请不要吝啬给我们反馈你对扩展的评价(正面的或负面的),也可以给我们提供关于自定义扩展的新想法。说不定某一天,你的想法会成为标准的一部分!
8.2. 事件
事件(event)通常用于为流程生命周期中发生的事情建模。事件总是图形化为圆圈。在BPMN 2.0中,有两种主要的事件分类:捕获(catching)与抛出(throwing)事件。
-
捕获: 当流程执行到达这个事件时,会等待直到触发器动作。触发器的类型由其中的图标,或者说XML中的类型声明而定义。捕获事件与抛出事件显示上的区别,是其内部的图标没有填充(即是白色的)。
-
抛出: 当流程执行到达这个事件时,会触发一个触发器。触发器的类型,由其中的图标,或者说XML中的类型声明而定义。抛出事件与捕获事件显示上的区别,是其内部的图标填充为黑色。
8.2.1. 事件定义
事件定义(event definition),用于定义事件的语义。没有事件定义的话,事件就“不做什么特别的事情”。例如,一个没有事件定义的开始事件,并不限定具体是什么启动了流程。如果为这个开始事件添加事件定义(例如定时器事件定义),就声明了启动流程的“类型”(例如对于定时器事件定义,就是到达了特定的时间点)。
8.2.2. 定时器事件定义
定时器事件(timer event definition),是由定时器所触发的事件。可以用于开始事件,中间事件,或边界事件。定时器事件的行为取决于所使用的业务日历(business calendar)。定时器事件有默认的业务日历,但也可以为每个定时器事件定义,单独定义业务日历。
1
2
3 <timerEventDefinition flowable:businessCalendarName="custom">
...
</timerEventDefinition>
其中businessCalendarName指向流程引擎配置中的业务日历。如果省略业务日历定义,则使用默认业务日历。
定时器定义必须且只能包含下列的一种元素:
-
timeDate。这个元素指定了ISO 8601格式的固定时间。在这个时间就会触发触发器。例如:
1
2
3 <timerEventDefinition>
<timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
-
timeDuration。要定义定时器需要等待多长时间再触发,可以用timerEventDefinition的子元素timeDuration。使用ISO 8601格式(BPMN 2.0规范要求)。例如(等待10天):
1
2
3 <timerEventDefinition>
<timeDuration>P10D</timeDuration>
</timerEventDefinition>
1
2
3 <timerEventDefinition>
<timeCycle flowable:endDate="2015-02-25T16:42:11+00:00">R3/PT10H</timeCycle>
</timerEventDefinition>
1
2
3 <timerEventDefinition>
<timeCycle>R3/PT10H/${EndDate}</timeCycle>
</timerEventDefinition>
如果同时使用了两种格式,则系统会使用以属性方式定义的endDate。
目前只有BoundaryTimerEvents与CatchTimerEvent可以使用EndDate。
另外,也可以使用cron表达式指定定时周期。下面的例子展示了一个整点启动,每5分钟触发的触发器:
0 0/5 * * * ?
请参考这个教程了解如何使用cron表达式。
请注意: 与普通的Unix cron不同,第一个符号代表的是秒,而不是分钟。
重复时间周期更适合使用相对时间,也就是从某个特定时间点开始计算(比如用户任务开始的时间)。而cron表达式可以使用绝对时间,因此更适合用于定时启动事件。
可以在定时事件定义中使用表达式,也就是使用流程变量控制定时。这个流程变量必须是包含合适时间格式的字符串,ISO 8601(或者对于循环类型,cron)。
1
2
3
4
5 <boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
<timerEventDefinition>
<timeDuration>${duration}</timeDuration>
</timerEventDefinition>
</boundaryEvent>
请注意:定时器只有在异步执行器启用时才能触发(需要在flowable.cfg.xml
中,将asyncExecutorActivate设置为true
。因为默认情况下异步执行器都是禁用的)。
8.2.3. 错误事件定义
(error event definition)
重要提示: BPMN错误与Java异常不是一回事。事实上,这两者毫无共同点。BPMN错误事件是建模业务异常(business exceptions)的方式。而Java异常会按它们自己的方式处理。
1
2
3 <endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
8.2.4. 信号事件定义
信号事件(signal event),是引用具名信号的事件。信号是全局范围(广播)的事件,并会被传递给所有激活的处理器(等待中的流程实例/捕获信号事件 catching signal events)。
使用signalEventDefinition
元素声明信号事件定义。其signalRef
属性引用一个signal
元素,该signal
元素需要声明为definitions
根元素的子元素。下面摘录一个流程,使用中间事件(intermediate event)抛出与捕获信号事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <definitions... >
<!-- 声明信号 -->
<signal id="alertSignal" name="alert" />
<process id="catchSignal">
<intermediateThrowEvent id="throwSignalEvent" name="Alert">
<!-- 信号事件定义 -->
<signalEventDefinition signalRef="alertSignal" />
</intermediateThrowEvent>
...
<intermediateCatchEvent id="catchSignalEvent" name="On Alert">
<!-- 信号事件定义 -->
<signalEventDefinition signalRef="alertSignal" />
</intermediateCatchEvent>
...
</process>
</definitions>
两个signalEventDefinition
引用同一个signal
元素。
抛出信号事件
信号可以由流程实例使用BPMN结构抛出(throw),也可以通过编程方式使用Java API抛出。下面org.flowable.engine.RuntimeService
中的方法可以用编程方式抛出信号:
1
2 RuntimeService.signalEventReceived(String signalName);
RuntimeService.signalEventReceived(String signalName, String executionId);
signalEventReceived(String signalName)
与signalEventReceived(String signalName, String executionId)
的区别,是前者在全局范围为所有已订阅处理器抛出信号(广播),而后者只为指定的执行传递信号。
捕获信号事件
可以使用信号捕获中间事件(intermediate catch signal event)或者信号边界事件(signal boundary event)捕获信号事件。
查询信号事件订阅
可以查询订阅了某一信号事件的所有执行:
1
2
3 List<Execution> executions = runtimeService.createExecutionQuery()
.signalEventSubscriptionName("alert")
.list();
可以使用signalEventReceived(String signalName, String executionId)
方法为这些执行传递这个信号。
信号事件的范围
默认情况下,信号事件在流程引擎全局广播。这意味着你可以在一个流程实例中抛出一个信号事件,而不同流程定义的不同流程实例都会响应这个事件。
但有时也会希望只在同一个流程实例中响应信号事件。例如,在流程实例中使用异步机制,而两个或多个活动彼此互斥的时候。
要限制信号事件的范围(scope),在信号事件定义中添加(非BPMN 2.0标准!)scope属性:
1 <signal id="alertSignal" name="alert" flowable:scope="processInstance"/>
这个属性的默认值为"global(全局)"。
信号事件示例
下面是一个不同流程通过信号通信的例子。第一个流程在保险政策更新或变更时启动。在变更由人工审核之后,会抛出信号事件,指出政策已经发生了变更:
这个事件可以被所有感兴趣的流程实例捕获。下面是一个订阅这个事件的流程的例子。
请注意:要理解信号事件会广播给所有激活的处理器,这很重要。这意味着在上面的例子中,所有订阅这个信号的流程实例都会接收这个信号。在这个例子中这就是我们期望的。然而,有的情况下,不希望使用广播方式。考虑下面的流程:
Flowable不支持上面流程中描述的模式。我们的想法是,在执行"do something"任务时抛出的错误,由错误边界事件捕获,并通过信号抛出事件传播至执行的并行分支,最终中断"do something in parallel"任务。到目前为止Flowable会按照预期效果执行。然而,由于信号的广播效应,它也会被传播至所有其他订阅了这个信号事件的流程实例。这可能并非我们希望的效果。
请注意:信号事件与特定的流程实例无关,而是会广播给所有流程实例。如果你需要只为某一特定的流程实例传递信号,则需要使用signalEventReceived(String signalName, String executionId)
手动建立关联,并使用适当的的查询机制。
Flowable提供了解决的方法。可以在信号事件上添加scope属性,并将其设置为processInstance。
8.2.5. 消息事件定义
消息事件(message event),是指引用具名消息的事件。消息具有名字与载荷。与信号不同,消息事件只有一个接收者。
消息事件定义使用messageEventDefinition
元素声明。其messageRef
属性引用一个message
元素,该message
元素需要声明为definitions
根元素的子元素。下面摘录一个流程,声明了两个消息事件,并由开始事件与消息捕获中间事件(intermediate catching message event)引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 <definitions id="definitions"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples"
xmlns:tns="Examples">
<message id="newInvoice" name="newInvoiceMessage" />
<message id="payment" name="paymentMessage" />
<process id="invoiceProcess">
<startEvent id="messageStart" >
<messageEventDefinition messageRef="newInvoice" />
</startEvent>
...
<intermediateCatchEvent id="paymentEvt" >
<messageEventDefinition messageRef="payment" />
</intermediateCatchEvent>
...
</process>
</definitions>
抛出消息事件
作为嵌入式的流程引擎,Flowable并不关心实际如何接收消息。因为这可能与环境相关,或需要进行平台定义的操作。例如连接至JMS(Java Messaging Service,Java消息服务)队列(Queue)/主题(Topic),或者处理Webservice或者REST请求。因此接收消息需要作为应用的一部分,或者是流程引擎所嵌入的基础框架中的一部分,由你自行实现。
在应用中接收到消息后,需要决定如何处理它。如果这个消息需要启动新的流程实例,可以选择一种由runtime服务提供的方法:
1
2
3
4 ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey,
Map<String, Object> processVariables);
这些方法使用消息启动流程实例。
如果需要由已有的流程实例接收消息,需要首先将消息与特定的流程实例关联(查看后续章节),然后触发等待中的执行,让流程继续进行。runtime服务提供了下列方法,可以触发订阅了消息事件的执行:
1
2 void messageEventReceived(String messageName, String executionId);
void messageEventReceived(String messageName, String executionId, HashMap<String, Object> processVariables);
查询消息事件订阅
-
对于消息启动事件(message start event),消息事件的订阅与的流程定义相关。可以使用
ProcessDefinitionQuery
查询这种类型的消息订阅:
1
2
3 ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.messageEventSubscription("newCallCenterBooking")
.singleResult();
因为一个消息只能被一个流程定义订阅,因此这个查询总是返回0或1个结果。如果流程定义更新了,只有该流程定义的最新版本会订阅这个消息事件。
-
对于消息捕获中间事件(intermediate catch message event),消息事件的订阅与执行相关。可以使用
ExecutionQuery
查询这种类型的消息订阅:
1
2
3
4 Execution execution = runtimeService.createExecutionQuery()
.messageEventSubscriptionName("paymentReceived")
.variableValueEquals("orderId", message.getOrderId())
.singleResult();
这种查询通常都会有关联查询,并且通常需要了解流程的情况(在这个例子里,对于给定的orderId,至多只有一个流程实例)。
消息事件示例
下面是一个流程的例子,可以使用两种不同的消息启动:
在流程需要通过不同的方式启动,但是后续使用统一的方式处理时,就可以使用这种方法。
8.2.6. 启动事件
启动事件(start event)是流程的起点。启动事件的类型(流程在消息到达时启动,在指定的时间间隔后启动,等等),定义了流程如何启动,并显示为启动事件中的小图标。在XML中,类型由子元素声明来定义。
启动事件随时捕获:启动事件(保持)等候,直到特定的触发器被触发。
在启动事件中,可以使用下列Flowable自定义参数:
-
initiator: 指明保存认证用户(authenticated user)ID用的变量名。在流程启动时,操作用户的ID会保存在这个变量中。例如:
1 <startEvent id="request" flowable:initiator="initiator" />
认证用户必须在try-finally块中调用IdentityService.setAuthenticatedUserId(String)
方法进行设置。像这样:
1
2
3
4
5
6 try {
identityService.setAuthenticatedUserId("bono");
runtimeService.startProcessInstanceByKey("someProcessKey");
} finally {
identityService.setAuthenticatedUserId(null);
}
这段代码已经集成在Flowable应用中,可以在表单中使用。
8.2.7. 空启动事件
描述
“空”启动事件(none Start Event),指的是未指定启动流程实例触发器的启动事件。引擎将无法预知何时启动流程实例。空启动事件用于流程实例通过调用下列startProcessInstanceByXXX API方法启动的情况。
1 ProcessInstance processInstance = runtimeService.startProcessInstanceByXXX();
请注意:子流程(sub-process)必须有空启动事件。
图示
空启动事件用空心圆圈表示,中间没有图标(也就是说,没有触发器)。
XML表示
空启动事件的XML表示格式,就是普通的启动事件声明,而没有任何子元素(其他种类的启动事件都有用于声明其类型的子元素)。
1 <startEvent id="start" name="my start event" />
空启动事件的自定义扩展
formKey: 引用表单定义,用户需要在启动新流程实例时填写该表单。可以在表单章节找到更多信息。例如:
1 <startEvent id="request" flowable:formKey="request" />
8.2.8. 定时器启动事件
描述
定时器启动事件(timer start event)在指定时间创建流程实例。在流程只需要启动一次,或者流程需要在特定的时间间隔重复启动时,都可以使用。
请注意:子流程不能有定时器启动事件。
请注意:定时器启动事件,在流程部署的同时就开始计时。不需要调用startProcessInstanceByXXX就会在时间启动。调用startProcessInstanceByXXX时会在定时启动之外额外启动一个流程。
请注意:当部署带有定时器启动事件的流程的更新版本时,上一版本的定时器作业会被移除。这是因为通常并不希望旧版本的流程仍然自动启动新的流程实例。
图示
定时器启动事件,用其中有一个钟表图标的圆圈来表示。
XML表示
定时器启动事件的XML表示格式,是普通的启动事件声明加上定时器定义子元素。请参考定时器定义了解详细配置方法。
示例:流程会启动4次,间隔5分钟,从2011年3月11日,12:13开始
1
2
3
4
5 <startEvent id="theStart">
<timerEventDefinition>
<timeCycle>R4/2011-03-11T12:13/PT5M</timeCycle>
</timerEventDefinition>
</startEvent>
示例:流程会在设定的时间启动一次
1
2
3
4
5 <startEvent id="theStart">
<timerEventDefinition>
<timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
</startEvent>
8.2.9. 消息启动事件
描述
消息启动事件(message start event)使用具名消息启动流程实例。消息名用于选择正确的启动事件。
当部署具有一个或多个消息启动事件的流程定义时,会做如下判断:
-
给定流程定义中,消息启动事件的名字必须是唯一的。一个流程定义不得包含多个同名的消息启动事件。如果流程定义中有两个或多个消息启动事件引用同一个消息,或者两个或多个消息启动事件引用了具有相同消息名字的消息,则Flowable会在部署这个流程定义时抛出异常。
-
在所有已部署的流程定义中,消息启动事件的名字必须是唯一的。如果在流程定义中,一个或多个消息启动事件引用了已经部署的另一流程定义中消息启动事件的消息名,则Flowable会在部署这个流程定义时抛出异常。
-
流程版本:在部署流程定义的新版本时,会取消上一版本的消息订阅,即使新版本中并没有这个消息事件)。
在启动流程实例时,可以使用下列RuntimeService
中的方法,触发消息启动事件:
1
2
3
4 ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey,
Map<String, Object< processVariables);
messageName
是由message
元素的name
属性给定的名字。messageEventDefinition
的messageRef
属性会引用message
元素。当启动流程实例时,会做如下判断:
-
只有顶层流程(top-level process)才支持消息启动事件。嵌入式子流程不支持消息启动事件。
-
如果一个流程定义中有多个消息启动事件,可以使用
runtimeService.startProcessInstanceByMessage(…)
选择合适的启动事件。 -
如果一个流程定义中有多个消息启动事件与一个空启动事件,则
runtimeService.startProcessInstanceByKey(…)
与runtimeService.startProcessInstanceById(…)
会使用空启动事件启动流程实例。 -
如果一个流程定义中有多个消息启动事件而没有空启动事件,则
runtimeService.startProcessInstanceByKey(…)
与runtimeService.startProcessInstanceById(…)
会抛出异常。 -
如果一个流程定义中只有一个消息启动事件,则
runtimeService.startProcessInstanceByKey(…)
与runtimeService.startProcessInstanceById(…)
会使用这个消息启动事件启动新流程实例。 -
如果流程由调用活动(call activity)启动,则只有在下列情况下才支持消息启动事件
-
除了消息启动事件之外,流程还有唯一的空启动事件
-
或者流程只有唯一的消息启动事件,而没有其他启动事件。
-
图示
消息启动事件用其中有一个消息事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。
XML表示
消息启动事件的XML表示格式,为普通启动事件声明加上messageEventDefinition子元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <definitions id="definitions"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples"
xmlns:tns="Examples">
<message id="newInvoice" name="newInvoiceMessage" />
<process id="invoiceProcess">
<startEvent id="messageStart" >
<messageEventDefinition messageRef="tns:newInvoice" />
</startEvent>
...
</process>
</definitions>
8.2.10. 信号启动事件
描述
信号启动事件(signal start event),使用具名信号启动流程实例。这个信号可以由流程实例中的信号抛出中间事件(intermediary signal throw event),或者API(runtimeService.signalEventReceivedXXX方法)触发。两种方式都会启动所有拥有相同名字信号启动事件的流程定义。
请注意可以选择异步还是同步启动流程实例。
需要为API传递的signalName
,是由signal
元素的name
属性决定的名字。signal
元素由signalEventDefinition
的signalRef
属性引用。
图示
信号启动事件用其中有一个信号事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。
XML表示
信号启动事件的XML表示格式,为普通启动事件声明,加上signalEventDefinition子元素:
1
2
3
4
5
6
7
8
9
10
11 <signal id="theSignal" name="The Signal" />
<process id="processWithSignalStart1">
<startEvent id="theStart">
<signalEventDefinition id="theSignalEventDefinition" signalRef="theSignal" />
</startEvent>
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
<userTask id="theTask" name="Task in process A" />
<sequenceFlow id="flow2" sourceRef="theTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
8.2.11. 错误启动事件
图示
错误启动事件用其中有一个错误事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。
XML表示
错误启动事件的XML表示格式,为普通启动事件声明加上errorEventDefinition子元素:
1
2
3 <startEvent id="messageStart" >
<errorEventDefinition errorRef="someError" />
</startEvent>
8.2.12. 结束事件
结束事件(end event)标志着流程或子流程中一个分支的结束。结束事件总是抛出(型)事件。这意味着当流程执行到达结束事件时,会抛出一个结果。结果的类型由事件内部的黑色图标表示。在XML表示中,类型由子元素声明给出。
8.2.13. 空结束事件
描述
“空”结束事件(none end event),意味着当到达这个事件时,没有特别指定抛出的结果。因此,引擎除了结束当前执行分支之外,不会多做任何事情。
图示
空结束事件,用其中没有图标(没有结果类型)的粗圆圈表示。
XML表示
空事件的XML表示格式为普通结束事件声明,没有任何子元素(其它种类的结束事件都有子元素,用于声明其类型)。
1 <endEvent id="end" name="my end event" />
8.2.14. 错误结束事件
描述
当流程执行到达错误结束事件(error end event)时,结束执行的当前分支,并抛出错误。这个错误可以由匹配的错误边界中间事件捕获。如果找不到匹配的错误边界事件,将会抛出异常。
图示
错误结束事件事件用内部有一个错误图标的标准结束事件(粗圆圈)表示。错误图标是全黑的,代表抛出的含义。
XML表示
错误结束事件表示为结束事件,加上errorEventDefinition子元素:
1
2
3 <endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
errorRef属性可以引用在流程外定义的error元素:
1
2
3
4 <error id="myError" errorCode="123" />
...
<process id="myProcess">
...
error的errorCode用于查找匹配的错误捕获边界事件。如果errorRef不匹配任何已定义的error,则该errorRef会用做errorCode的快捷方式。这个快捷方式是Flowable特有的。下面的代码片段在功能上是相同的。
1
2
3
4
5
6
7
8 <error id="myError" errorCode="error123" />
...
<process id="myProcess">
...
<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
...
与下面的代码功能相同
1
2
3 <endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="error123" />
</endEvent>
请注意errorRef必须遵从BPMN 2.0概要(schema),且必须是合法的QName。
8.2.15. 终止结束事件
描述
当到达终止结束事件(terminate end event)时,当前的流程实例或子流程会被终止。也就是说,当执行到达终止结束事件时,会判断第一个范围 scope(流程或子流程)并终止它。请注意在BPMN 2.0中,子流程可以是嵌入式子流程,调用活动,事件子流程,或事务子流程。有一条通用规则:当存在多实例的调用过程或嵌入式子流程时,只会终止一个实例,其他的实例与流程实例不会受影响。
可以添加一个可选属性terminateAll。当其为true时,无论该终止结束事件在流程定义中的位置,也无论它是否在子流程(甚至是嵌套子流程)中,都会终止(根)流程实例。
图示
终止结束事件用内部有一个全黑圆的标准结束事件(粗圆圈)表示。
XML表示
终止结束事件,表示为结束事件,加上terminateEventDefinition子元素。
请注意terminateAll属性是可选的(默认为false)。
1
2
3 <endEvent id="myEndEvent >
<terminateEventDefinition flowable:terminateAll="true"></terminateEventDefinition>
</endEvent>
8.2.16. 取消结束事件
描述
取消结束事件(cancel end event)只能与BPMN事务子流程(BPMN transaction subprocess)一起使用。当到达取消结束事件时,会抛出取消事件,且必须由取消边界事件(cancel boundary event)捕获。取消边界事件将取消事务,并触发补偿(compensation)。
图示
取消结束事件用内部有一个取消图标的标准结束事件(粗圆圈)表示。取消图标是全黑的,代表抛出的含义。
XML表示
取消结束事件,表示为结束事件,加上cancelEventDefinition子元素。
1
2
3 <endEvent id="myCancelEndEvent">
<cancelEventDefinition />
</endEvent>
8.2.17. 边界事件
边界事件(boundary event)是捕获型事件,依附在活动(activity)上。边界事件永远不会抛出。这意味着当活动运行时,事件将监听特定类型的触发器。当捕获到事件时,会终止活动,并沿该事件的出口顺序流继续。
所有的边界事件都用相同的方式定义:
1
2
3 <boundaryEvent id="myBoundaryEvent" attachedToRef="theActivity">
<XXXEventDefinition/>
</boundaryEvent>
边界事件由下列元素定义:
-
(流程范围内)唯一的标识符
-
由attachedToRef属性定义的,对该事件所依附的活动的引用。请注意边界事件及其所依附的活动,应定义在相同级别(也就是说,边界事件并不包含在活动内)。
-
定义了边界事件的类型的,形如XXXEventDefinition的XML子元素(例如TimerEventDefinition,ErrorEventDefinition,等等)。查阅特定的边界事件类型,以了解更多细节。
8.2.18. 定时器边界事件
描述
定时器边界事件(timer boundary event)的行为像是跑表与闹钟。当执行到达边界事件所依附的活动时,将启动定时器。当定时器触发时(例如在特定时间间隔后),可以中断活动,并沿着边界事件的出口顺序流继续执行。
图示
定时器边界事件用内部有一个定时器图标的标准边界事件(圆圈)表示。
XML表示
定时器边界事件与一般边界事件一样定义。其中类型子元素为timerEventDefinition元素。
1
2
3
4
5 <boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
<timerEventDefinition>
<timeDuration>PT4H</timeDuration>
</timerEventDefinition>
</boundaryEvent>
请参考定时器事件定义了解定时器配置的细节。
在图示中圆圈画为虚线,如下:
中断与非中断定时器事件是不同的。非中断意味着最初的活动不会被中断,而会保持原样。默认为中断行为。在XML表示中,cancelActivity属性设置为false。
一个典型使用场景,是在一段时间之后发送提醒邮件,但不影响正常的流程流向。
1 <boundaryEvent id="escalationTimer" cancelActivity="false" attachedToRef="firstLineSupport"/>
请注意:定时器边界事件只有在异步执行器(async executor)启用时才能触发(也就是说,需要在flowable.cfg.xml
中,将asyncExecutorActivate设置为true
。因为异步执行器默认情况下是禁用的。)
边界事件的已知问题
所有类型的边界事件,都有一个关于并发的已知问题:不能在边界事件上附加多个出口顺序流。这个问题的解决方案,是使用一条出口顺序流,指向并行网关。
8.2.19. 错误边界事件
描述
在活动边界上的错误捕获中间(事件),或简称错误边界事件(error boundary event),捕获其所依附的活动范围内抛出的错误。
在嵌入式子流程或者调用活动上定义错误边界事件最有意义,因为子流程的范围会包括其中的所有活动。错误可以由错误结束事件抛出。这样的错误会逐层向其上级父范围传播,直到在范围内找到一个匹配错误事件定义的错误边界事件。
当捕获错误事件时,会销毁边界事件定义所在的活动,同时销毁其中所有的当前执行(例如,并行活动,嵌套子流程,等等)。流程执行将沿着边界事件的出口顺序流继续。
图示
错误边界事件用内部有一个错误图标的标准中间事件(两层圆圈)表示。错误图标是白色的,代表捕获的含义。
XML表示
错误边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="catchError" attachedToRef="mySubProcess">
<errorEventDefinition errorRef="myError"/>
</boundaryEvent>
在边界事件中,errorRef引用一个流程元素外定义的错误:
1
2
3
4 <error id="myError" errorCode="123" />
...
<process id="myProcess">
...
errorCode用于匹配捕获的错误:
-
如果省略了errorRef,错误边界事件会捕获所有错误事件,无论error的errorCode是什么。
-
如果提供了errorRef,并且其引用了存在的error,则边界事件只会捕获相同错误代码的错误。
-
如果提供了errorRef,但BPMN 2.0文件中没有定义error,则errorRef会用作errorCode(与错误结束事件类似)。
示例
下面的示例流程展示了如何使用错误结束事件。当'Review profitability (审核盈利能力)'用户任务完成,并指出提供的信息不足时,会抛出错误。当这个错误被子流程边界捕获时,'Review sales lead (审核销售线索)'子流程中的所有运行中活动都会被销毁(即使'Review customer rating 审核客户等级'还没有完成),并会创建'Provide additional details (提供更多信息)'用户任务。
这个流程作为演示配置的示例提供。可以在org.flowable.examples.bpmn.event.error包中找到流程XML与单元测试。
8.2.20. 信号边界事件
描述
依附在活动边界上的信号捕获中间(事件),或简称信号边界事件(signal boundary event),捕获与其信号定义具有相同名称的信号。
请注意:与其他事件例如错误边界事件不同的是,信号边界事件不只是捕获其所依附范围抛出的信号。信号边界事件为全局范围(广播)的,意味着信号可以从任何地方抛出,甚至可以是不同的流程实例。
请注意:与其他事件(如错误事件)不同,信号在被捕获后不会被消耗。如果有两个激活的信号边界事件,捕获相同的信号事件,则两个边界事件都会被触发,哪怕它们不在同一个流程实例里。
图示
信号边界事件,用内部有一个信号图标的标准中间事件(两层圆圈)表示。信号图标是白色的,代表捕获的含义。
XML表示
信号边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<signalEventDefinition signalRef="alertSignal"/>
</boundaryEvent>
示例
参见信号事件定义章节。
8.2.21. 消息边界事件
描述
在活动边界上的消息捕获中间(事件),或简称消息边界事件(message boundary event),捕获与其消息定义具有相同消息名的消息。
图示
消息边界事件,用内部有一个消息图标的标准中间事件(两层圆圈)表示。信号图标是白色的,代表捕获的含义。
请注意消息边界事件既可以是中断型的(右图),也可以是非中断型的(左图)。
XML表示
消息边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<messageEventDefinition messageRef="newCustomerMessage"/>
</boundaryEvent>
示例
参见消息事件定义章节。
8.2.22. 取消边界事件
描述
依附在事务子流程边界上的取消捕获中间事件,或简称取消边界事件(cancel boundary event),在事务取消时触发。当取消边界事件触发时,首先会中断当前范围的所有活动执行。接下来,启动事务范围内所有有效的的补偿边界事件(compensation boundary event)。补偿会同步执行,也就是说在离开事务前,边界事件会等待补偿完成。当补偿完成时,沿取消边界事件的任何出口顺序流离开事务子流程。
请注意:一个事务子流程只允许使用一个取消边界事件。
请注意:如果事务子流程中有嵌套的子流程,只会对成功完成的子流程触发补偿。
请注意:如果取消边界事件放置在具有多实例特性的事务子流程上,如果一个实例触发了取消,则边界事件将取消所有实例。
图示
取消边界事件,用内部有一个取消图标的标准中间事件(两层圆圈)表示。取消图标是白色的(未填充),代表捕获的含义。
XML表示
取消边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="boundary" attachedToRef="transaction" >
<cancelEventDefinition />
</boundaryEvent>
因为取消边界事件总是中断型的,因此没有cancelActivity
属性。
8.2.23. 补偿边界事件
描述
依附在活动边界上的补偿捕获中间(事件),或简称补偿边界事件(compensation boundary event),可以为活动附加补偿处理器。
补偿边界事件必须使用直接关联的方式引用单个的补偿处理器。
补偿边界事件与其它边界事件的活动策略不同。其它边界事件,例如信号边界事件,在其依附的活动启动时激活;当该活动结束时会被解除,并取消相应的事件订阅。而补偿边界事件不是这样。补偿边界事件在其依附的活动成功完成时激活,同时创建补偿事件的相应订阅。当补偿事件被触发,或者相应的流程实例结束时,才会移除订阅。请考虑下列因素:
-
当补偿被触发时,会调用补偿边界事件关联的补偿处理器。调用次数与其依附的活动成功完成的次数相同。
-
如果补偿边界事件依附在具有多实例特性的活动上,则会为每一个实例创建补偿事件订阅。
-
如果补偿边界事件依附在位于循环内部的活动上,则每次该活动执行时,都会创建一个补偿事件订阅。
-
如果流程实例结束,则取消补偿事件的订阅。
请注意:嵌入式子流程不支持补偿边界事件。
图示
补偿边界事件,用内部有一个补偿图标的标准中间事件(两层圆圈)表示。补偿图标是白色的(未填充),代表捕获的含义。另外,补偿边界事件使用单向连接关联补偿处理器,如下图所示:
XML表示
补偿边界事件与标准边界事件一样定义:
1
2
3
4
5
6
7
8 <boundaryEvent id="compensateBookHotelEvt" attachedToRef="bookHotel" >
<compensateEventDefinition />
</boundaryEvent>
<association associationDirection="One" id="a1"
sourceRef="compensateBookHotelEvt" targetRef="undoBookHotel" />
<serviceTask id="undoBookHotel" isForCompensation="true" flowable:class="..." />
补偿边界事件在活动完成后才激活,因此不支持cancelActivity
属性。
8.2.24. 捕获中间事件
所有的捕获中间事件(intermediate catching events)都使用相同方式定义:
1
2
3 <intermediateCatchEvent id="myIntermediateCatchEvent" >
<XXXEventDefinition/>
</intermediateCatchEvent>
捕获中间事件由下列元素定义:
-
(流程范围内)唯一的标识符
-
定义了捕获中间事件类型的,形如XXXEventDefinition的XML子元素(例如TimerEventDefinition等)。查阅特定中间捕获事件类型,以了解更多细节。
8.2.25. 定时器捕获中间事件
描述
定时器捕获中间事件(timer intermediate catching event)的行为像是跑表。当执行到达捕获事件时,启动定时器;当定时器触发时(例如在一段时间间隔后),沿定时器中间事件的出口顺序流继续执行。
图示
定时器中间事件用内部有定时器图标的中间捕获事件表示。
8.2.26. 信号捕获中间事件
描述
信号捕获中间事件(signal intermediate catching event),捕获与其引用的信号定义具有相同信号名称的信号。
请注意:与其他事件如错误事件不同,信号在被捕获后不会被消耗。如果有两个激活的信号中间事件,捕获相同的信号事件,则两个中间事件都会被触发,哪怕它们不在同一个流程实例里。
图示
信号捕获中间事件用内部有信号图标的标准中间事件(两层圆圈)表示。信号图标是白色的(未填充),代表捕获的含义。
XML表示
信号中间事件与捕获中间事件一样定义。子元素为signalEventDefinition。
1
2
3 <intermediateCatchEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateCatchEvent>
示例
参阅信号事件定义章节。
8.2.27. 消息捕获中间事件
描述
消息捕获中间事件(message intermediate catching event),捕获特定名字的消息。
图示
消息捕获中间事件用内部有消息图标的标准中间事件(两层圆圈)表示。消息图标是白色的(未填充),代表捕获的含义。
XML表示
消息中间事件与捕获中间事件一样定义。子元素为messageEventDefinition。
1
2
3 <intermediateCatchEvent id="message">
<messageEventDefinition signalRef="newCustomerMessage" />
</intermediateCatchEvent>
示例
参阅消息事件定义章节。
8.2.28. 抛出中间事件
所有的抛出中间事件(intermediate throwing evnet)都使用相同方式定义:
1
2
3 <intermediateThrowEvent id="myIntermediateThrowEvent" >
<XXXEventDefinition/>
</intermediateThrowEvent>
抛出中间事件由下列元素定义:
-
(流程范围内)唯一的标识符
-
定义了抛出中间事件类型的,形如XXXEventDefinition的XML子元素(例如signalEventDefinition等)。查阅特定中间抛出事件类型,以了解更多细节。
8.2.29. 空抛出中间事件
下面的流程图展示了空抛出中间事件(intermediate throwing none event)的简单例子。其用于指示流程已经到达了某种状态。
添加一个执行监听器后,空中间事件就可以成为很好的监视某些KPI(Key Performance Indicators 关键绩效指标)的钩子。
1
2
3
4
5 <intermediateThrowEvent id="noneEvent">
<extensionElements>
<flowable:executionListener class="org.flowable.engine.test.bpmn.event.IntermediateNoneEventTest$MyExecutionListener" event="start" />
</extensionElements>
</intermediateThrowEvent>
你也可以添加一些自己的代码,将部分事件发送给你的BAM(Business Activity Monitoring 业务活动监控)工具,或者DWH(Data Warehouse 数据仓库)。在这种情况下,引擎本身不会做任何事情,只是从中穿过。
8.2.30. 信号抛出中间事件
描述
信号抛出中间事件(signal intermediate throwing event),抛出所定义信号的信号事件。
在Flowable中,信号会广播至所有的激活的处理器(也就是说,所有的信号捕获事件)。可以同步或异步地发布信号。
-
在默认配置中,信号同步地传递。这意味着抛出信号的流程实例会等待,直到信号传递至所有的捕获信号的流程实例。所有的捕获流程实例也会在与抛出流程实例相同的事务中,也就是说如果收到通知的流程实例中,有一个实例产生了技术错误(抛出异常),则所有相关的实例都会失败。
-
信号也可以异步地传递。这是由到达抛出信号事件时的发送处理器来决定的。对于每个激活的处理器,JobExecutor会为其存储并传递一个异步通知消息(asynchronous notification message),即作业(Job)。
图示
消息抛出中间事件用内部有信号图标的标准中间事件(两层圆圈)表示。信号图标是黑色的(已填充),代表抛出的含义。
XML表示
信号中间事件与抛出中间事件一样定义。子元素为signalEventDefinition。
1
2
3 <intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateThrowEvent>
异步信号事件这样定义:
1
2
3 <intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" flowable:async="true" />
</intermediateThrowEvent>
示例
参阅信号事件定义章节。
8.2.31. 补偿抛出中间事件
描述
补偿抛出中间事件(compensation intermediate throwing event)用于触发补偿。
触发补偿:既可以为设计的活动触发补偿,也可以为补偿事件所在的范围触发补偿。补偿由活动所关联的补偿处理器执行。
-
活动抛出补偿时,活动关联的补偿处理器将执行的次数,为活动成功完成的次数。
-
抛出补偿时,当前范围中所有的活动,包括并行分支上的活动都会被补偿。
-
补偿分层触发:如果将要被补偿的活动是一个子流程,则该子流程中所有的活动都会触发补偿。如果该子流程有嵌套的活动,则会递归地抛出补偿。然而,补偿不会传播至流程的上层:如果子流程中触发了补偿,该补偿不会传播至子流程范围外的活动。BPMN规范指出,对“与子流程在相同级别”的活动触发补偿。
-
在Flowable中,补偿按照执行的相反顺序运行。这意味着最后完成的活动会第一个补偿。
-
可以使用补偿抛出中间事件补偿已经成功完成的事务子流程。
请注意:如果抛出补偿的范围中有一个子流程,而该子流程包含有关联了补偿处理器的活动,则当抛出补偿时,只有该子流程成功完成时,补偿才会传播至该子流程。如果子流程内嵌套的部分活动已经完成,并附加了补偿处理器,但包含这些活动的子流程还没有完成,则这些补偿处理器仍不会执行。参考下面的例子:
在这个流程中,有两个并行的执行:一个执行嵌入子流程,另一个执行“charge credit card(信用卡付款)”活动。假定两个执行都已开始,且第一个执行正等待用户完成“review bookings(检查预定)”任务。第二个执行进行了“charge credit card(信用卡付款)”活动的操作,抛出了错误,导致“cancel reservations(取消预订)”事件触发补偿。这时并行子流程还未完成,意味着补偿不会传播至该子流程,因此不会执行“cancel hotel reservation(取消酒店预订)”补偿处理器。而如果“cancel reservations(取消预订)”运行前,这个用户任务(因此该嵌入式子流程也)已经完成,则补偿会传播至该嵌入式子流程。
流程变量:当补偿嵌入式子流程时,用于执行补偿处理器的执行,可以访问子流程的局部流程变量在子流程完成时的值。为此,会对范围执行(为执行子流程所创建的执行)所关联的流程变量进行快照。意味着:
-
补偿执行器无法访问子流程范围内并行执行所添加的变量。
-
上层执行所关联的流程变量(例如流程实例关联的流程变量)不在该快照中。因为补偿处理器可以直接访问这些流程变量在抛出补偿时的值。
-
只会为嵌入式子流程进行变量快照。其他活动不会进行变量快照。
目前的限制:
-
目前不支持
waitForCompletion="false"
。当补偿抛出中间事件触发补偿时,只有在补偿成功完成时,才会离开该事件。 -
补偿由并行执行运行。并行执行会按照补偿活动完成的逆序启动。
-
补偿不会传播至调用活动(call activity)生成的子流程。
图示
补偿抛出中间事件用内部有补偿图标的标准中间事件(两层圆圈)表示。补偿图标是黑色的(已填充),代表抛出的含义。
XML表示
补偿中间事件与抛出中间事件一样定义。子元素为compensateEventDefinition。
1
2
3 <intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition />
</intermediateThrowEvent>
另外,activityRef
可选项用于为指定的范围或活动触发补偿:
1
2
3 <intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition activityRef="bookHotel" />
</intermediateThrowEvent>
8.3. 顺序流
8.3.1. 描述
顺序流(sequence flow)是流程中两个元素间的连接器。在流程执行过程中,一个元素被访问后,会沿着其所有出口顺序流继续执行。这意味着BPMN 2.0的默认是并行执行的:两个出口顺序流就会创建两个独立的、并行的执行路径。
8.3.2. 图示
顺序流,用从源元素指向目标元素的箭头表示。箭头总是指向目标元素。
8.3.3. XML表示
顺序流需要有流程唯一的id,并引用存在的源与目标元素。
1 <sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
8.3.4. 条件顺序流
描述
在顺序流上可以定义条件(conditional sequence flow)。当离开BPMN 2.0活动时,默认行为是计算其每个出口顺序流上的条件。当条件计算为true时,选择该出口顺序流。如果该方法选择了多条顺序流,则会生成多个执行,流程会以并行方式继续。
请注意:上面的介绍针对BPMN 2.0活动(与事件),但不适用于网关(gateway)。不同类型的网关,会用不同的方式处理带有条件的顺序流。
图示
条件顺序流用起点带有小菱形的顺序流表示。在顺序流旁显示条件表达式。
XML表示
条件顺序流的XML表示格式为含有conditionExpression(条件表达式)子元素的普通顺序流。请注意目前只支持tFormalExpressions。可以省略xsi:type=""定义,默认为唯一支持的表达式类型。
1
2
3
4
5 <sequenceFlow id="flow" sourceRef="theStart" targetRef="theTask">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.price > 100 && order.price < 250}]]>
</conditionExpression>
</sequenceFlow>
目前conditionalExpressions只能使用UEL。详细信息可以在表达式章节找到。使用的表达式需要能解析为boolean值,否则当计算条件时会抛出异常。
-
下面的例子,通过典型的JavaBean的方式,使用getter引用流程变量的数据。
1
2
3 <conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.price > 100 && order.price < 250}]]>
</conditionExpression>
-
这个例子调用了一个解析为boolean值的方法。
1
2
3 <conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.isStandardOrder()}]]>
</conditionExpression>
Flowable发行版中包含了下列示例流程,用于展示值表达式与方法表达式的使用(参见org.flowable.examples.bpmn.expression)。
8.3.5. 默认顺序流
描述
所有的BPMN 2.0任务与网关都可以使用默认顺序流(default sequence flow)。只有当没有其他顺序流可以选择时,才会选择默认顺序流作为活动的出口顺序流。流程会忽略默认顺序流上的条件。
图示
默认顺序流用起点带有“斜线”标记的一般顺序流表示。
XML表示
活动的默认顺序流由该活动的default属性定义。下面的XML片段展示了一个排他网关(exclusive gateway),带有默认顺序流flow 2。只有当conditionA与conditionB都计算为false时,才会选择默认顺序流作为网关的出口顺序流。
1
2
3
4
5
6
7
8
9
10
11 <exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" default="flow2" />
<sequenceFlow id="flow1" sourceRef="exclusiveGw" targetRef="task1">
<conditionExpression xsi:type="tFormalExpression">${conditionA}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="task2"/>
<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="task3">
<conditionExpression xsi:type="tFormalExpression">${conditionB}</conditionExpression>
</sequenceFlow>
对应下面的图示:
8.4. 网关
网关(gateway)用于控制执行的流向(或者按BPMN 2.0的用词:执行的“标志(token)”)。网关可以消费(consuming)与生成(generating)标志。
网关用其中带有图标的菱形表示。该图标显示了网关的类型。
8.4.1. 排他网关
描述
排他网关(exclusive gateway)(也叫异或网关 XOR gateway,或者更专业的,基于数据的排他网关 exclusive data-based gateway),用于对流程中的决策建模。当执行到达这个网关时,会按照所有出口顺序流定义的顺序对它们进行计算。选择第一个条件计算为true的顺序流(当没有设置条件时,认为顺序流为true)继续流程。
请注意这里出口顺序流的含义与BPMN 2.0中的一般情况不一样。一般情况下,会选择所有条件计算为true的顺序流,并行执行。而使用排他网关时,只会选择一条顺序流。当多条顺序流的条件都计算为true时,会且仅会选择在XML中最先定义的顺序流继续流程。如果没有可选的顺序流,会抛出异常。
图示
排他网关用内部带有’X’图标的标准网关(菱形)表示,'X’图标代表异或的含义。请注意内部没有图标的网关默认为排他网关。BPMN 2.0规范不允许在同一个流程中混合使用有及没有X的菱形标志。
XML表示
排他网关的XML表示格式很简洁:一行定义网关的XML。条件表达式定义在其出口顺序流上。查看条件顺序流章节了解这种表达式的可用选项。
以下面的模型为例:
其XML表示如下:
1
2
3
4
5
6
7
8
9
10
11
12
13 <exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" />
<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="theTask1">
<conditionExpression xsi:type="tFormalExpression">${input == 1}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="theTask2">
<conditionExpression xsi:type="tFormalExpression">${input == 2}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow4" sourceRef="exclusiveGw" targetRef="theTask3">
<conditionExpression xsi:type="tFormalExpression">${input == 3}</conditionExpression>
</sequenceFlow>
8.4.2. 并行网关
描述
网关也可以建模流程中的并行执行。在流程模型中引入并行的最简单的网关,就是并行网关(parallel gateway)。它可以将执行分支(fork)为多条路径,也可以合并(join)多条入口路径的执行。
并行网关的功能取决于其入口与出口顺序流:
-
分支:所有的出口顺序流都并行执行,为每一条顺序流创建一个并行执行。
-
合并:所有到达并行网关的并行执行都会在网关处等待,直到每一条入口顺序流都到达了有个执行。然后流程经过该合并网关继续。
请注意,如果并行网关同时具有多条入口与出口顺序流,可以同时具有分支与合并的行为。在这种情况下,网关首先合并所有入口顺序流,然后分裂为多条并行执行路径。
与其他网关类型有一个重要区别:并行网关不计算条件。如果连接到并行网关的顺序流上定义了条件,会直接忽略该条件。
图示
并行网关,用内部带有’加号’图标的网关(菱形)表示,代表与(AND)的含义。
XML表示
定义并行网关只需要一行XML:
1 <parallelGateway id="myParallelGateway" />
实际行为(分支,合并或两者皆有),由连接到该并行网关的顺序流定义。
例如,上面的模型表示为下面的XML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />
<parallelGateway id="fork" />
<sequenceFlow sourceRef="fork" targetRef="receivePayment" />
<sequenceFlow sourceRef="fork" targetRef="shipOrder" />
<userTask id="receivePayment" name="Receive Payment" />
<sequenceFlow sourceRef="receivePayment" targetRef="join" />
<userTask id="shipOrder" name="Ship Order" />
<sequenceFlow sourceRef="shipOrder" targetRef="join" />
<parallelGateway id="join" />
<sequenceFlow sourceRef="join" targetRef="archiveOrder" />
<userTask id="archiveOrder" name="Archive Order" />
<sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />
<endEvent id="theEnd" />
在上面的例子中,当流程启动后会创建两个任务:
1
2
3
4
5
6
7
8
9
10
11
12
13 ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
.processInstanceId(pi.getId())
.orderByTaskName()
.asc();
List<Task> tasks = query.list();
assertEquals(2, tasks.size());
Task task1 = tasks.get(0);
assertEquals("Receive Payment", task1.getName());
Task task2 = tasks.get(1);
assertEquals("Ship Order", task2.getName());
当这两个任务完成后,第二个并行网关会合并这两个执行。由于它只有一条出口顺序流,因此就不会再创建并行执行路径,而只是激活Archive Order(存档订单)任务。
请注意并行网关不需要“平衡”(也就是说,前后对应的两个并行网关,其入口/出口顺序流的数量不需要一致)。每个并行网关都会简单地等待所有入口顺序流,并为每一条出口顺序流创建并行执行,而不受流程模型中的其他结构影响。因此,下面的流程在BPMN 2.0中是合法的:
8.4.3. 包容网关
描述
可以把包容网关(inclusive gateway)看做排他网关与并行网关的组合。与排他网关一样,可以在包容网关的出口顺序流上定义条件,包容网关会计算条件。然而主要的区别是,包容网关与并行网关一样,可以同时选择多于一条出口顺序流。
包容网关的功能取决于其入口与出口顺序流:
-
分支:流程会计算所有出口顺序流的条件。对于每一条计算为true的顺序流,流程都会创建一个并行执行。
-
合并:所有到达包容网关的并行执行,都会在网关处等待。直到每一条具有流程标志(process token)的入口顺序流,都有一个执行到达。这是与并行网关的重要区别。换句话说,包容网关只会等待可以被执行的入口顺序流。在合并后,流程穿过合并并行网关继续。
请注意,如果包容网关同时具有多条入口与出口顺序流,可以同时具有分支与合并的行为。在这种情况下,网关首先合并所有具有流程标志的入口顺序流,然后为每一个条件计算为true的出口顺序流分裂出并行执行路径。
包容网关的汇聚行为比并行网关更复杂。所有到达包容网关的并行执行,都会在网关等待,直到所有“可以到达”包容网关的执行都“到达”包容网关。 判断方法为:计算当前流程实例中的所有执行,检查从其位置是否有一条到达包容网关的路径(忽略顺序流上的任何条件)。如果存在这样的执行(可到达但尚未到达),则不会触发包容网关的汇聚行为。
图示
包容网关,用内部带有’圆圈’图标的网关(菱形)表示。
XML表示
定义包容网关需要一行XML:
1 <inclusiveGateway id="myInclusiveGateway" />
实际行为(分支,合并或两者皆有),由连接到该包容网关的顺序流定义。
例如,上面的模型表现为下面的XML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 <startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />
<inclusiveGateway id="fork" />
<sequenceFlow sourceRef="fork" targetRef="receivePayment" >
<conditionExpression xsi:type="tFormalExpression">${paymentReceived == false}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="fork" targetRef="shipOrder" >
<conditionExpression xsi:type="tFormalExpression">${shipOrder == true}</conditionExpression>
</sequenceFlow>
<userTask id="receivePayment" name="Receive Payment" />
<sequenceFlow sourceRef="receivePayment" targetRef="join" />
<userTask id="shipOrder" name="Ship Order" />
<sequenceFlow sourceRef="shipOrder" targetRef="join" />
<inclusiveGateway id="join" />
<sequenceFlow sourceRef="join" targetRef="archiveOrder" />
<userTask id="archiveOrder" name="Archive Order" />
<sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />
<endEvent id="theEnd" />
在上面的例子中,当流程启动后,如果流程变量paymentReceived == false且shipOrder == true,会创建两个任务。如果只有一个流程变量等于true,则只会创建一个任务。如果没有条件计算为true,会抛出异常(可通过指定默出口顺序流避免)。在下面的例子中,只会创建ship order(传递订单)一个任务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 HashMap<String, Object> variableMap = new HashMap<String, Object>();
variableMap.put("receivedPayment", true);
variableMap.put("shipOrder", true);
ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
.processInstanceId(pi.getId())
.orderByTaskName()
.asc();
List<Task> tasks = query.list();
assertEquals(1, tasks.size());
Task task = tasks.get(0);
assertEquals("Ship Order", task.getName());
当这个任务完成后,第二个包容网关会合并这两个执行。并且由于它只有一条出口顺序流,所有不会再创建并行执行路径,而只会激活Archive Order(存档订单)任务。
请注意包容网关不需要“平衡”(也就是说,对应的包容网关,其入口/出口顺序流的数量不需要匹配)。包容网关会简单地等待所有入口顺序流,并为每一条出口顺序流创建并行执行,不受流程模型中的其他结构影响。
请注意包容网关不需要“平衡”(也就是说,前后对应的两个包容网关,其入口/出口顺序流的数量不需要一致)。每个包容网关都会简单地等待所有入口顺序流,并为每一条出口顺序流创建并行执行,不受流程模型中的其他结构影响。
8.4.4. 基于事件的网关
描述
基于事件的网关(event-based gateway)提供了根据事件做选择的方式。网关的每一条出口顺序流都需要连接至一个捕获中间事件。当流程执行到达基于事件的网关时,与等待状态类似,网关会暂停执行,并且为每一条出口顺序流创建一个事件订阅。
请注意:基于事件的网关的出口顺序流与一般的顺序流不同。这些顺序流从不实际执行。相反,它们用于告知流程引擎:当执行到达一个基于事件的网关时,需要订阅什么事件。有以下限制:
-
一个基于事件的网关,必须有两条或更多的出口顺序流。
-
基于事件的网关,只能连接至
intermediateCatchEvent(捕获中间事件)
类型的元素(Flowable不支持在基于事件的网关之后连接“接收任务 Receive Task”)。 -
连接至基于事件的网关的
intermediateCatchEvent
,必须只有一个入口顺序流。
图示
基于事件的网关,用内部带有特殊图标的网关(菱形)表示。
XML表示
用于定义基于事件的网关的XML元素为eventBasedGateway
。
示例
下面是一个带有基于事件的网关的示例流程。当执行到达基于事件的网关时,流程执行暂停。流程实例订阅alert信号事件,并创建一个10分钟后触发的定时器。流程引擎会等待10分钟,并同时等待信号事件。如果信号在10分钟内触发,则会取消定时器,流程沿着信号继续执行,激活Handle alert用户任务。如果10分钟内没有触发信号,则会继续执行,并取消信号订阅。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 <definitions id="definitions"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples">
<signal id="alertSignal" name="alert" />
<process id="catchSignal">
<startEvent id="start" />
<sequenceFlow sourceRef="start" targetRef="gw1" />
<eventBasedGateway id="gw1" />
<sequenceFlow sourceRef="gw1" targetRef="signalEvent" />
<sequenceFlow sourceRef="gw1" targetRef="timerEvent" />
<intermediateCatchEvent id="signalEvent" name="Alert">
<signalEventDefinition signalRef="alertSignal" />
</intermediateCatchEvent>
<intermediateCatchEvent id="timerEvent" name="Alert">
<timerEventDefinition>
<timeDuration>PT10M</timeDuration>
</timerEventDefinition>
</intermediateCatchEvent>
<sequenceFlow sourceRef="timerEvent" targetRef="exGw1" />
<sequenceFlow sourceRef="signalEvent" targetRef="task" />
<userTask id="task" name="Handle alert"/>
<exclusiveGateway id="exGw1" />
<sequenceFlow sourceRef="task" targetRef="exGw1" />
<sequenceFlow sourceRef="exGw1" targetRef="end" />
<endEvent id="end" />
</process>
</definitions>
8.5. 任务
8.5.1. 用户任务
描述
“用户任务(user task)”用于对需要人工执行的任务进行建模。当流程执行到达用户任务时,会为指派至该任务的用户或组的任务列表创建一个新任务。
图示
用户任务用左上角有一个小用户图标的标准任务(圆角矩形)表示。
XML表示
用户任务在XML中如下定义。其中id是必须属性,name是可选属性。
1 <userTask id="theTask" name="Important task" />
也可以为用户任务添加描述(description)。事实上任何BPMN 2.0元素都可以有描述。描述由documentation元素定义。
1
2
3
4
5
<userTask id="theTask" name="Schedule meeting" >
<documentation>
Schedule an engineering meeting for next week with the new hire.
</documentation>
可以使用标准Java方式获取描述文本:
1 task.getDescription()
到期日期
每个任务都可以使用一个字段标志该任务的到期日期(due date)。可以使用查询API,查询在给定日期前或后到期的任务。
可以在任务定义中使用扩展指定表达式,以在任务创建时设定到期日期。该表达式必须解析为java.util.Date
,java.util.String (ISO8601格式)
,ISO8601时间长度(例如PT50M),或者null
。例如,可以使用在流程里前一个表单中输入的日期,或者由前一个服务任务计算出的日期。如果使用的是时间长度,则到期日期基于当前时间加上给定长度计算。例如当dueDate使用“PT30M”时,任务在从现在起30分钟后到期。
1 <userTask id="theTask" name="Important task" flowable:dueDate="${dateVariable}"/>
任务的到期日期也可以使用TaskService
,或者在TaskListener
中使用传递的DelegateTask
修改。
用户指派
用户任务可以直接指派(assign)给用户。可以定义humanPerformer子元素来实现。humanPerformer需要resourceAssignmentExpression来实际定义用户。目前,只支持formalExpressions。
1
2
3
4
5
6
7
8
9
10
11 <process >
...
<userTask id='theTask' name='important task' >
<humanPerformer>
<resourceAssignmentExpression>
<formalExpression>kermit</formalExpression>
</resourceAssignmentExpression>
</humanPerformer>
</userTask>
只能指定一个用户作为任务的humanPerformer。在Flowable术语中,这个用户被称作办理人(assignee)。拥有办理人的任务,在其他人的任务列表中不可见,而只能在该办理人的个人任务列表中看到。
可以通过TaskService获取特定用户办理的任务:
1 List<Task> tasks = taskService.createTaskQuery().taskAssignee("kermit").list();
任务也可以放在用户的候选任务列表中。在这个情况下,需要使用potentialOwner(潜在用户)结构。用法与humanPerformer结构类似。请注意需要指定表达式中的每一个元素为用户还是组(引擎无法自行判断)。
1
2
3
4
5
6
7
8
9
10
11 <process >
...
<userTask id='theTask' name='important task' >
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>user(kermit), group(management)</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>
可用如下方法获取定义了potentialOwner结构的任务:
1 List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit");
将获取所有kermit作为候选用户的任务,也就是说,表达式含有user(kermit)的任务。同时也将获取所有指派给kermit为其成员的组的任务(例如,kermit时management组的成员,且任务指派给management组)。组在运行时解析,并可通过身份服务管理。
如果并未指定给定字符串是用户还是组,引擎默认其为组。下列代码与声明group(accountancy)效果一样。
1 <formalExpression>accountancy</formalExpression>
用于任务指派的Flowable扩展
很明显,当指派关系不复杂时,这种用户与组的指派方式十分笨重。为避免这种复杂性,可以在用户任务上使用自定义扩展。
-
assignee(办理人)属性:这个自定义扩展用于直接将用户指派至用户任务。
1 <userTask id="theTask" name="my task" flowable:assignee="kermit" />
与上面定义的humanPerformer结构效果完全相同。
-
candidateUsers(候选用户)属性:这个自定义扩展用于为任务指定候选用户。
1 <userTask id="theTask" name="my task" flowable:candidateUsers="kermit, gonzo" />
与使用上面定义的potentialOwner结构效果完全相同。请注意不需要像在potentialOwner中一样,使用user(kermit)的声明,因为这个属性只能用于用户。
-
candidateGroups(候选组)attribute:这个自定义扩展用于为任务指定候选组。
1 <userTask id="theTask" name="my task" flowable:candidateGroups="management, accountancy" />
与使用上面定义的potentialOwner结构效果完全相同。请注意不需要像在potentialOwner中一样,使用group(management)的声明,因为这个属性只能用于组。
-
可以定义在一个用户任务上同时定义candidateUsers与candidateGroups。
请注意:尽管Flowable提供了IdentityService身份管理组件,但并不会检查给定的用户是否实际存在。这是为了便于将Flowable嵌入应用时,与已有的身份管理解决方案进行集成。
自定义身份关联类型
在用户指派中已经介绍过,BPMN标准支持单个指派用户即hunamPerformer,或者由一组用户构成potentialOwners潜在用户池。另外,Flowable为用户任务定义了扩展属性元素,用于代表任务的办理人或者候选用户。
Flowable支持的身份关联(identity link)类型有:
1
2
3
4
5
6
7
8 public class IdentityLinkType {
/* Flowable内置角色 */
public static final String ASSIGNEE = "assignee";
public static final String CANDIDATE = "candidate";
public static final String OWNER = "owner";
public static final String STARTER = "starter";
public static final String PARTICIPANT = "participant";
}
BPMN标准及Flowable示例中,身份认证是用户与组。在前一章节提到过,Flowable的身份管理实现并不适用于生产环境,而需要在支持的认证概要下自行扩展。
如果需要添加额外的关联类型,可按照下列语法,使用自定义资源作为扩展元素:
1
2
3
4
5
6
7
8
9 <userTask id="theTask" name="make profit">
<extensionElements>
<flowable:customResource flowable:name="businessAdministrator">
<resourceAssignmentExpression>
<formalExpression>user(kermit), group(management)</formalExpression>
</resourceAssignmentExpression>
</flowable:customResource>
</extensionElements>
</userTask>
自定义关联表达式添加至TaskDefinition类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected Map<String, Set<Expression>> customUserIdentityLinkExpressions =
new HashMap<String, Set<Expression>>();
protected Map<String, Set<Expression>> customGroupIdentityLinkExpressions =
new HashMap<String, Set<Expression>>();
public Map<String, Set<Expression>> getCustomUserIdentityLinkExpressions() {
return customUserIdentityLinkExpressions;
}
public void addCustomUserIdentityLinkExpression(
String identityLinkType, Set<Expression> idList) {
customUserIdentityLinkExpressions.put(identityLinkType, idList);
}
public Map<String, Set<Expression>> getCustomGroupIdentityLinkExpressions() {
return customGroupIdentityLinkExpressions;
}
public void addCustomGroupIdentityLinkExpression(
String identityLinkType, Set<Expression> idList) {
customGroupIdentityLinkExpressions.put(identityLinkType, idList);
}
这些方法将在运行时,由UserTaskActivityBehavior的handleAssignments方法调用。
最后,需要扩展IdentityLinkType类,以支持自定义身份关联类型:
1
2
3
4
5
6
7
8 package com.yourco.engine.task;
public class IdentityLinkType extends org.flowable.engine.task.IdentityLinkType {
public static final String ADMINISTRATOR = "administrator";
public static final String EXCLUDED_OWNER = "excludedOwner";
}
通过任务监听器自定义指派
如果上面的方式仍不能满足要求,可以在创建事件(create event)上使用任务监听器,调用自定义指派逻辑:
1
2
3
4
5 <userTask id="task1" name="My task" >
<extensionElements>
<flowable:taskListener event="create" class="org.flowable.MyAssignmentHandler" />
</extensionElements>
</userTask>
传递至TaskListener
的DelegateTask
,可用于设置办理人与候选用户/组:
1
2
3
4
5
6
7
8
9
10
11
12
13 public class MyAssignmentHandler implements TaskListener {
public void notify(DelegateTask delegateTask) {
// 在这里执行自定义身份查询
// 然后调用如下命令:
delegateTask.setAssignee("kermit");
delegateTask.addCandidateUser("fozzie");
delegateTask.addCandidateGroup("management");
...
}
}
当使用Spring时,可以按上面章节的介绍使用自定义指派属性,并交由使用任务监听器、带有表达式的Spring bean,监听任务创建事件。在下面的例子中,通过调用ldapService
Spring bean的findManagerOfEmployee
方法设置办理人。传递的emp参数是一个流程变量。
1 <userTask id="task" name="My Task" flowable:assignee="${ldapService.findManagerForEmployee(emp)}"/>
也可以用于候选用户与组:
1 <userTask id="task" name="My Task" flowable:candidateUsers="${ldapService.findAllSales()}"/>
请注意调用方法的返回类型必须是String
或Collection<String>
(候选用户或组):
1
2
3
4
5
6
7
8
9
10
11 public class FakeLdapService {
public String findManagerForEmployee(String employee) {
return "Kermit The Frog";
}
public List<String> findAllSales() {
return Arrays.asList("kermit", "gonzo", "fozzie");
}
}
8.5.2. 脚本任务
描述
脚本任务(script task)是自动执行的活动。当流程执行到达脚本任务时,会执行相应的脚本。
图示
脚本任务用左上角有一个小“脚本”图标的标准BPMN 2.0任务(圆角矩形)表示。
XML表示
脚本任务使用script与scriptFormat元素定义。
1
2
3
4
5
6
7
8 <scriptTask id="theScriptTask" name="Execute script" scriptFormat="groovy">
<script>
sum = 0
for ( i in inputArray ) {
sum += i
}
</script>
</scriptTask>
scriptFormat属性的值,必须是兼容JSR-223(Java平台脚本)的名字。默认情况下,JavaScript包含在每一个JDK中,因此不需要添加任何JAR文件。如果想使用其它(兼容JSR-223的)脚本引擎,则需要在classpath中添加相应的jar,并使用适当的名字。例如,Flowable单元测试经常使用Groovy,因为其语法与Java十分相似。
请注意Groovy脚本引擎与groovy-all JAR捆绑在一起。在Groovy 2.0版本以前,脚本引擎是Groovy JAR的一部分。因此,必须添加如下依赖:
1
2
3
4
5 <dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.x.x<version>
</dependency>
脚本中的变量
到达脚本引擎的执行中,所有的流程变量都可以在脚本中使用。在这个例子里,脚本变量'inputArray'实际上就是一个流程变量(一个integer的数组)。
1
2
3
4
5
6 <script>
sum = 0
for ( i in inputArray ) {
sum += i
}
</script>
也可以简单地调用execution.setVariable("variableName", variableValue),在脚本中设置流程变量。默认情况下,变量不会自动储存(请注意,在一些早期版本中是会储存的!)。可以将scriptTask
的autoStoreVariables
参数设置为true
,以自动保存任何在脚本中定义的变量(例如上例中的sum)。然而这并不是最佳实践。最佳实践是显式调用execution.setVariable(),因为在JDK近期的一些版本中,某些脚本语言不能自动保存变量。查看这个链接了解更多信息。
1 <scriptTask id="script" scriptFormat="JavaScript" flowable:autoStoreVariables="false">
这个参数的默认值为false
。也就是说如果在脚本任务定义中忽略这个参数,则脚本声明的所有变量将只在脚本执行期间有效。
在脚本中设置变量的例子:
1
2
3
4 <script>
def scriptVar = "test123"
execution.setVariable("myVar", scriptVar)
</script>
请注意:下列名字是保留字,不能用于变量名:out,out:print,lang:import,context,elcontext。
脚本任务的结果
脚本任务的返回值,可以通过为脚本任务定义的'flowable:resultVariable'属性设置为流程变量。可以是已经存在的,或者新的流程变量。如果指定为已存在的流程变量,则流程变量的值会被脚本执行的结果值覆盖。如果不指定结果变量名,则脚本结果值将被忽略。
1
2
3 <scriptTask id="theScriptTask" name="Execute script" scriptFormat="juel" flowable:resultVariable="myVar">
<script>#{echo}</script>
</scriptTask>
在上面的例子中,脚本执行的结果(解析表达式'#{echo}'的值),将在脚本完成后,设置为名为'myVar'的流程变量。
安全性
当使用javascript作为脚本语言时,可以使用“安全脚本(secure scripting)”。参见安全脚本章节。
8.5.3. Java服务任务
描述
Java服务任务(Java service task)用于调用Java类。
图示
服务任务用左上角有一个小齿轮图标的圆角矩形表示。
XML表示
有四种方法声明如何调用Java逻辑:
-
指定实现了JavaDelegate或ActivityBehavior的类
-
调用解析为委托对象(delegation object)的表达式
-
调用方法表达式(method expression)
-
对值表达式(value expression)求值
使用flowable:class属性提供全限定类名(fully qualified classname),指定流程执行时调用的类。
1
2
3 <serviceTask id="javaService"
name="My Java Service Task"
flowable:class="org.flowable.MyJavaDelegate" />
查看实现章节,了解使用这种类的更多信息。
也可以使用解析为对象的表达式。该对象必须遵循的规则,与使用flowable:class
创建的对象规则相同(查看更多)。
1 <serviceTask id="serviceTask" flowable:delegateExpression="${delegateExpressionBean}" />
delegateExpressionBean
是一个实现了JavaDelegate
接口的bean,定义在Spring容器中。
使用flowable:expression属性指定需要计算的UEL方法表达式。
1
2
3 <serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage()}" />
将在名为printer
的对象上调用printMessage
方法(不带参数)。
也可以为表达式中使用的方法传递变量。
1
2
3 <serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage(execution, myVar)}" />
将在名为printer
的对象上调用printMessage
方法。传递的第一个参数为DelegateExecution
,名为execution
,在表达式上下文中默认可用。传递的第二个参数,是当前执行中,名为myVar
变量的值。
可以使用flowable:expression属性指定需要计算的UEL值表达式。
1
2
3 <serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{split.ready}" />
会调用名为split
的bean的ready
参数的getter方法,getReady
(不带参数)。该对象会被解析为执行的流程变量或(如果可用的话)Spring上下文中的bean。
实现
要实现可以在流程执行中调用的类,需要实现org.flowable.engine.delegate.JavaDelegate接口,并在execute方法中提供所需逻辑。当流程执行到达该活动时,会执行方法中定义的逻辑,并按照BPMN 2.0的默认方法离开活动。
下面是一个Java类的示例,用于将流程变量String改为大写。这个类需要实现org.flowable.engine.delegate.JavaDelegate接口,因此需要实现execute(DelegateExecution)方法。这个方法就是引擎将调用的方法,需要实现业务逻辑。可以通过DelegateExecution接口(点击链接获取该接口操作的详细Javadoc)访问流程实例信息,如流程变量等。
1
2
3
4
5
6
7
8
9 public class ToUppercase implements JavaDelegate {
public void execute(DelegateExecution execution) {
String var = (String) execution.getVariable("input");
var = var.toUpperCase();
execution.setVariable("input", var);
}
}
请注意:只会为serviceTask上定义的Java类创建一个实例。所有流程实例共享同一个类实例,用于调用execute(DelegateExecution)。这意味着该类不能有任何成员变量,并需要是线程安全的,因为它可能会在不同线程中同时执行。这也影响了字段注入的使用方法。(译者注:原文可能较老,不正确。5.21中,flowable:class指定的类,会在流程实例启动时,为每个活动分别进行实例化。不过,当该活动在流程中重复执行,或者为多实例时,使用的都会是同一个类实例。)
在流程定义中引用(如flowable:class
)的类,不会在部署时实例化。只有当流程执行第一次到达该类使用的地方时,才会创建该类的实例。如果找不到这个类,会抛出FlowableException
。这是因为部署时的环境(更准确的说classpath),与实际运行的环境经常不一样。例如当使用ant或者Flowable应用中业务存档上传的方式部署的流程,其classpath中不会自动包含流程引用的类。
[内部:非公开实现类]也可以使用实现了org.flowable.engine.impl.delegate.ActivityBehavior接口的类。该实现可以访问更强大的引擎功能,例如,可以影响流程的控制流程。请注意这并不是很好的实践,需要避免这么使用。建议只有在高级使用场景下,并且你确知在做什么的时候,才使用ActivityBehavior接口。
字段注入
可以为委托类的字段注入值。支持下列注入方式:
-
字符串常量
-
表达式
如果可以的话,会按照Java Bean命名约定(例如,firstName
成员使用setter setFirstName(…)
),通过委托类的公有setter方法,注入变量。如果该字段没有可用的setter,会直接设置该委托类的私有成员的值。有的环境中,SecurityManagers不允许修改私有字段,因此为想要注入的字段暴露一个公有setter方法,是更安全的做法。
不论在流程定义中声明的是什么类型的值,注入对象的setter/私有字段的类型,总是org.flowable.engine.delegate.Expression
。解析表达式后,可以被转型为合适的类型。
'flowable:class'属性支持字段注入。也可以在使用flowable:delegateExpression属性时,进行字段注入。然而考虑到线程安全,需要遵循特殊的规则(参见下一章节)。
下面的代码片段展示了如何为类中声明的字段注入常量值。请注意按照BPMN 2.0 XML概要的要求,在实际字段注入声明前,需要先声明’extensionElements’XML元素。
1
2
3
4
5
6
7 <serviceTask id="javaService"
name="Java service invocation"
flowable:class="org.flowable.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
<extensionElements>
<flowable:field name="text" stringValue="Hello World" />
</extensionElements>
</serviceTask>
ToUpperCaseFieldInjected
类有一个字段text
,为org.flowable.engine.delegate.Expression
类型。当调用text.getValue(execution)
时,会返回配置的字符串Hello World
:
1
2
3
4
5
6
7
8
9 public class ToUpperCaseFieldInjected implements JavaDelegate {
private Expression text;
public void execute(DelegateExecution execution) {
execution.setVariable("var", ((String)text.getValue(execution)).toUpperCase());
}
}
另外,对于较长文本(例如邮件正文),可以使用'flowable:string'子元素:
1
2
3
4
5
6
7
8
9
10
11 <serviceTask id="javaService"
name="Java service invocation"
flowable:class="org.flowable.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
<extensionElements>
<flowable:field name="text">
<flowable:string>
This is a long string with a lot of words and potentially way longer even!
</flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
可以使用表达式在运行时动态解析注入的值。这种表达式可以使用流程变量,或者(若使用Spring)Spring定义的Bean。在服务任务实现中提到过,当服务任务中使用flowable:class属性时,该Java类的实例在所有流程实例中共享。要动态地为字段注入值,可以在org.flowable.engine.delegate.Expression
中注入值或方法表达式,它们会通过execute
方法传递的DelegateExecution
计算/调用。
下面的示例类使用了注入的表达式,并使用当前的DelegateExecution
解析它们。调用genderBean方法时传递的是gender变量。完整的代码与测试可以在org.flowable.examples.bpmn.servicetask.JavaServiceTaskTest.testExpressionFieldInjection
中找到
1
2
3
4
5
6
7
8
9
10
11
12 <serviceTask id="javaService" name="Java service invocation"
flowable:class="org.flowable.examples.bpmn.servicetask.ReverseStringsFieldInjected">
<extensionElements>
<flowable:field name="text1">
<flowable:expression>${genderBean.getGenderString(gender)}</flowable:expression>
</flowable:field>
<flowable:field name="text2">
<flowable:expression>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</flowable:expression>
</flowable:field>
</ extensionElements>
</ serviceTask>
1
2
3
4
5
6
7
8
9
10
11
12
13 public class ReverseStringsFieldInjected implements JavaDelegate {
private Expression text1;
private Expression text2;
public void execute(DelegateExecution execution) {
String value1 = (String) text1.getValue(execution);
execution.setVariable("var1", new StringBuffer(value1).reverse().toString());
String value2 = (String) text2.getValue(execution);
execution.setVariable("var2", new StringBuffer(value2).reverse().toString());
}
}
另外,为避免XML太过冗长,可以将表达式设置为属性,而不是子元素。
1
2 <flowable:field name="text1" expression="${genderBean.getGenderString(gender)}" />
<flowable:field name="text1" expression="Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}" />
字段注入与线程安全
通常情况下,在服务任务中使用Java委托与字段注入是线程安全的。然而,有些情况下不能保证线程安全。这取决于设置,或Flowable运行的环境。
当使用flowable:class属性时,使用字段注入总是线程安全的(译者注:仍不完全安全,如对于多实例服务任务,使用的是同一个实例)。对于引用了某个类的每一个服务任务,都会实例化新的实例,并且在创建实例时注入一次字段。在不同的任务或流程定义中多次使用同一个类没有问题。
当使用flowable:expression属性时,不能使用字段注入。只能通过方法调用传递变量。总是线程安全的。
当使用flowable:delegateExpression属性时,委托实例的线程安全性,取决于表达式解析的方式。如果该委托表达式在多个任务或流程定义中重复使用,并且表达式总是返回相同的示例,则字段注入不是线程安全的。让我们看几个例子。
假设表达式为${factory.createDelegate(someVariable)},其中factory为引擎可用的Java bean(例如使用Spring集成时的Spring bean),并在每次表达式解析时创建新的实例。这种情况下,使用字段注入时,没有线程安全性问题:每次表达式解析时,都会注入新实例的字段。
然而,如果表达式为${someJavaDelegateBean},解析为JavaDelegate的实现,并且在创建单例的环境(如Spring)中运行。当在不同的任务或流程定义中使用这个表达式时,表达式总会解析为相同的实例。这种情况下,使用字段注入不是线程安全的。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13 <serviceTask id="serviceTask1" flowable:delegateExpression="${someJavaDelegateBean}">
<extensionElements>
<flowable:field name="someField" expression="${input * 2}"/>
</extensionElements>
</serviceTask>
<!-- 其它流程定义元素 -->
<serviceTask id="serviceTask2" flowable:delegateExpression="${someJavaDelegateBean}">
<extensionElements>
<flowable:field name="someField" expression="${input * 2000}"/>
</extensionElements>
</serviceTask>
这段示例代码有两个服务任务,使用同一个委托表达式,但是expression字段填写不同的值。如果该表达式解析为相同的实例,就会在并发场景下,注入someField字段时出现竞争条件。
最简单的解决方法是:
-
使用表达式代替直接使用Java委托,并将所需数据通过方法参数传递给委托。
-
或者,在每次委托表达式解析时,返回委托类的新实例。这意味着这个bean的scope必须是prototype(原型)(例如在委托类上加上@Scope(SCOPE_PROTOTYPE)注解)。
在Flowable 5.22版本中,可以通过配置流程引擎配置,禁用在委托表达式上使用字段注入。需要设置delegateExpressionFieldInjectionMode参数(取org.flowable.engine.imp.cfg.DelegateExpressionFieldInjectionMode枚举中的值)。
可使用下列选项:
-
DISABLED(禁用):当使用委托表达式时,完全禁用字段注入。不会再尝试进行字段注入。这是最安全的方式,保证线程安全。
-
COMPATIBILITY(兼容):在这个模式下,行为与V5.21之前完全一样:可以在委托表达式中使用字段注入,如果委托类中没有定义该字段,会抛出异常。这是最不线程安全的模式,但可以保证历史版本兼容性,也可以在委托表达式只在一个任务中使用的时候(因此不会产生并发竞争条件),安全使用。
-
MIXED(混合):可以在使用委托表达式时注入,但当委托中没有定义字段时,不会抛出异常。这样可以在部分委托(比如不是单例的实例)中使用注入,而在部分委托中不使用注入。
-
Flowable 5.x版本的默认模式为COMPATIBILITY。
-
Flowable 6.x版本的默认模式为MIXED。
例如,假设使用MIXED模式,并使用Spring集成,在Spring配置中定义了如下bean:
1
2
3
4
5
6 <bean id="singletonDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.SingletonDelegateExpressionBean" />
<bean id="prototypeDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.PrototypeDelegateExpressionBean"
scope="prototype" />
第一个bean是一般的Spring bean,因此是单例的。第二个bean的scope为prototype,因此每次请求这个bean时,Spring容器都会返回一个新实例。
在以下流程定义中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 <serviceTask id="serviceTask1" flowable:delegateExpression="${prototypeDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${input * 2}"/>
<flowable:field name="fieldB" expression="${1 + 1}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask1"/>
</extensionElements>
</serviceTask>
<serviceTask id="serviceTask2" flowable:delegateExpression="${prototypeDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${123}"/>
<flowable:field name="fieldB" expression="${456}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask2"/>
</extensionElements>
</serviceTask>
<serviceTask id="serviceTask3" flowable:delegateExpression="${singletonDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${input * 2}"/>
<flowable:field name="fieldB" expression="${1 + 1}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask1"/>
</extensionElements>
</serviceTask>
<serviceTask id="serviceTask4" flowable:delegateExpression="${singletonDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${123}"/>
<flowable:field name="fieldB" expression="${456}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask2"/>
</extensionElements>
</serviceTask>
有四个服务任务,第一、二个使用${prototypeDelegateExpressionBean}委托表达式,第三、四个使用${singletonDelegateExpressionBean}委托表达式。
先看原型bean:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 public class PrototypeDelegateExpressionBean implements JavaDelegate {
public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);
private Expression fieldA;
private Expression fieldB;
private Expression resultVariableName;
public PrototypeDelegateExpressionBean() {
INSTANCE_COUNT.incrementAndGet();
}
@Override
public void execute(DelegateExecution execution) {
Number fieldAValue = (Number) fieldA.getValue(execution);
Number fieldValueB = (Number) fieldB.getValue(execution);
int result = fieldAValue.intValue() + fieldValueB.intValue();
execution.setVariable(resultVariableName.getValue(execution).toString(), result);
}
}
在运行上面流程定义的流程实例后,INSTANCE_COUNT的值为2。这是因为每次解析${prototypeDelegateExpressionBean}时,都会创建新实例。可以看到三个Expression成员字段的注入没有任何问题。
单例bean则有一点区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 public class SingletonDelegateExpressionBean implements JavaDelegate {
public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);
public SingletonDelegateExpressionBean() {
INSTANCE_COUNT.incrementAndGet();
}
@Override
public void execute(DelegateExecution execution) {
Expression fieldAExpression = DelegateHelper.getFieldExpression(execution, "fieldA");
Number fieldA = (Number) fieldAExpression.getValue(execution);
Expression fieldBExpression = DelegateHelper.getFieldExpression(execution, "fieldB");
Number fieldB = (Number) fieldBExpression.getValue(execution);
int result = fieldA.intValue() + fieldB.intValue();
String resultVariableName = DelegateHelper.getFieldExpression(execution,
"resultVariableName").getValue(execution).toString();
execution.setVariable(resultVariableName, result);
}
}
在对于单例bean,INSTANCE_COUNT总是1。在这个委托中,没有Expression成员字段(使用MIXED模式)。而在COMPATIBILITY模式下,就会抛出异常,因为需要有成员字段。这个bean也可以使用DISABLED模式,但会禁用上面进行了字段注入的原型bean。
在委托的代码里,使用了org.flowable.engine.delegate.DelegateHelper。它提供了一些有用的工具方法,用于执行相同的逻辑,并且在单例中是线程安全的。与注入Expression不同,它通过getFieldExpression读取。这意味着在服务任务的XML里,字段定义与单例bean完全相同。查看上面的XML代码,可以看到定义是相同的,只是实现逻辑不同。
技术提示:getFieldExpression直接读取BpmnModel,并在方法执行时创建表达式,因此是线程安全的。
-
在Flowable V5.x版本中,(由于架构缺陷)不能在ExecutionListener或TaskListener中使用DelegateHelper。要保证监听器的线程安全,仍需使用表达式,或确保每次解析委托表达式时,都创建新实例。
-
在Flowable V6.x版本中,在ExecutionListener或TaskListener中可以使用DelegateHelper。例如在V6.x版本中,下列代码可以使用DelegateHelper:
1
2
3
4
5
6
7 <extensionElements>
<flowable:executionListener
delegateExpression="${testExecutionListener}" event="start">
<flowable:field name="input" expression="${startValue}" />
<flowable:field name="resultVar" stringValue="processStartValue" />
</flowable:executionListener>
</extensionElements>
其中testExecutionListener解析为ExecutionListener接口的一个实现的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 @Component("testExecutionListener")
public class TestExecutionListener implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) {
Expression inputExpression = DelegateHelper.getFieldExpression(execution, "input");
Number input = (Number) inputExpression.getValue(execution);
int result = input.intValue() * 100;
Expression resultVarExpression = DelegateHelper.getFieldExpression(execution, "resultVar");
execution.setVariable(resultVarExpression.getValue(execution).toString(), result);
}
}
服务任务的结果
服务执行的返回值(仅对使用表达式的服务任务),可以通过为服务任务定义的'flowable:resultVariable'属性设置为流程变量。可以是已经存在的,或者新的流程变量。 如果指定为已存在的流程变量,则流程变量的值会被服务执行的结果值覆盖。 如果使用'flowable:useLocalScopeForResultVariable',则会将结果值设置为局部变量。 如果不指定结果变量名,则服务任务的结果值将被忽略。
1
2
3 <serviceTask id="aMethodExpressionServiceTask"
flowable:expression="#{myService.doSomething()}"
flowable:resultVariable="myVar" />
在上例中,服务执行的结果(流程变量或Spring bean中,使用'myService'名字所获取的对象,调用'doSomething()'方法的返回值),在服务执行完成后,会设置为名为'myVar'的流程变量。
可触发
一种常见的模式是发送JMS消息或HTTP调用触发外部服务,然后流程实例进入等待状态。之后外部系统会回复响应,流程实例继续执行下一个活动。在默认的BPMN中,需要使用服务任务和接收任务(receive task)。但是这样会引入竞争条件:外部服务的响应可能会早于流程实例持久化及接收任务激活。为了解决这个问题,Flowable为服务任务增加了triggerable(可触发)属性,可以将服务任务转变为执行服务逻辑,并在继续执行之前等待外部触发的任务。如果在可触发服务任务上同时设置异步(async 为 true),则流程实例会先持久化,然后在异步作业中执行服务任务逻辑。在BPMN XML中,可以这样实现可触发服务任务:
1
2
3
4 <serviceTask id="aTriggerableServiceTask"
flowable:expression="#{myService.doSomething()}"
flowable:triggerable="true"
flowable:async="true" />
外部服务可以同步或异步地触发等待中的流程实例。为了避免乐观锁异常,最好使用异步触发。默认情况下,异步作业是排他的,也就是说流程实例会被锁定,以保证流程实例中的其他活动不会影响到触发器的逻辑。可以使用RuntimeService的triggerAsync方法,异步触发等待中的流程实例。当然还是可以使用RuntimeService的trigger方法,同步触发。
处理异常
当执行自定义逻辑时,通常需要捕获并在流程中处理特定的业务异常。Flowable提供了多种选择。
抛出BPMN错误
可以在服务任务或脚本任务的用户代码中抛出BPMN错误。可以在Java委托、脚本、表达式与委托表达式中,抛出特殊的FlowableException:BpmnError。引擎会捕获这个异常,并将其转发至合适的错误处理器,如错误边界事件或错误事件子流程。
1
2
3
4
5
6
7
8
9
10
11 public class ThrowBpmnErrorDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
try {
executeBusinessLogic();
} catch (BusinessException e) {
throw new BpmnError("BusinessExceptionOccurred");
}
}
}
构造函数的参数是错误代码。错误代码决定了处理这个错误的错误处理器。参见错误边界事件了解如何捕获BPMN错误。
这个机制只应该用于业务错误,需要通过流程中定义的错误边界事件或错误事件子流程处理。技术错误应该通过其他异常类型表现,并且通常不在流程内部处理。
异常映射
也可以使用mapException
扩展,直接将Java异常映射至业务异常(错误)。单映射是最简单的形式:
1
2
3
4
5
6 <serviceTask id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException
errorCode="myErrorCode1">org.flowable.SomeException</flowable:mapException>
</extensionElements>
</serviceTask>
在上面的代码中,如果服务任务抛出org.flowable.SomeException
的实例,引擎会捕获该异常,并将其转换为带有给定errorCode的BPMN错误。然后就可以像普通BPMN错误完全一样地处理。其他的异常没有映射,仍将抛出至API调用处。
也可以在单行中使用includeChildExceptions
属性,映射特定异常的所有子异常。
1
2
3
4
5
6 <serviceTask id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException errorCode="myErrorCode1"
includeChildExceptions="true">org.flowable.SomeException</flowable:mapException>
</extensionElements>
</serviceTask>
上面的代码中,Flowable会将SomeException
的任何直接或间接的子类,转换为带有指定错误代码的BPMN错误。
当未指定includeChildExceptions
时,视为“false”。
默认映射最泛用。默认映射是一个不指定类的映射,可以匹配任何Java异常:
1
2
3
4
5 <serviceTask id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException errorCode="myErrorCode1"/>
</extensionElements>
</serviceTask>
除了默认映射,会按照从上至下的顺序检查映射,使用第一个匹配的映射。只在所有映射都不能成功匹配时使用默认映射。
只有第一个不指定类的映射会作为默认映射。默认映射忽略includeChildExceptions
。
异常顺序流
也可以选择在发生异常时,将流程执行路由至另一条路径。下面是一个例子。
1
2
3
4
5
6
7 <serviceTask id="javaService"
name="Java service invocation"
flowable:class="org.flowable.ThrowsExceptionBehavior">
</serviceTask>
<sequenceFlow id="no-exception" sourceRef="javaService" targetRef="theEnd" />
<sequenceFlow id="exception" sourceRef="javaService" targetRef="fixException" />
服务任务有两条出口顺序流,命名为exception
与no-exception
。在发生异常时,使用顺序流ID控制流程流向:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 public class ThrowsExceptionBehavior implements ActivityBehavior {
public void execute(DelegateExecution execution) {
String var = (String) execution.getVariable("var");
String sequenceFlowToTake = null;
try {
executeLogic(var);
sequenceFlowToTake = "no-exception";
} catch (Exception e) {
sequenceFlowToTake = "exception";
}
DelegateHelper.leaveDelegate(execution, sequenceFlowToTake);
}
}
在JavaDelegate中使用Flowable服务
有的时候,需要在Java服务任务中使用Flowable服务(例如调用活动(call activity)不满足需要的场景下,使用RuntimeService启动流程实例)。
1
2
3
4
5
6
7
8 public class StartProcessInstanceTestDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
RuntimeService runtimeService = Context.getProcessEngineConfiguration().getRuntimeService();
runtimeService.startProcessInstanceByKey("myProcess");
}
}
可以使用这个接口访问所有Flowable服务API。
使用这些API调用造成的所有数据变更都在当前事务中。在依赖注入的环境(如Spring或CDI,无论是否使用启用JTA的数据源)下也可以使用这个接口。例如,下面的代码片段与上面的代码具有相同功能,但通过注入而不是org.flowable.engine.EngineServices接口获得RuntimeService。
1
2
3
4
5
6
7
8
9
10
11 @Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {
@Autowired
private RuntimeService runtimeService;
public void startProcess() {
runtimeService.startProcessInstanceByKey("oneTaskProcess");
}
}
重要技术提示:由于服务调用是在当前事务中完成的,因此在服务任务执行前产生或修改的数据尚未存入数据库。所有API调用都基于数据库数据处理,这意味着这些未提交的修改在服务任务的API调用中“不可见”。
8.5.4. Web服务任务
描述
Web服务任务(Web service task)用于同步地调用外部的Web服务。
图示
Web服务任务与Java服务任务图标一样。
XML表示
使用Web服务之前,需要导入其操作及复杂的类型。可以使用导入标签(import tag)指向Web服务的WSDL,自动处理:
1
2
3 <import importType="http://schemas.xmlsoap.org/wsdl/"
location="http://localhost:63081/counter?wsdl"
namespace="http://webservice.flowable.org/" />
按照上面的声明,Flowable会导入定义,但不会创建条目定义(item definition)与消息。如果需要调用一个名为’prettyPrint’的方法,则需要先为请求及回复消息创建对应的消息与条目定义:
1
2
3
4
5 <message id="prettyPrintCountRequestMessage" itemRef="tns:prettyPrintCountRequestItem" />
<message id="prettyPrintCountResponseMessage" itemRef="tns:prettyPrintCountResponseItem" />
<itemDefinition id="prettyPrintCountRequestItem" structureRef="counter:prettyPrintCount" />
<itemDefinition id="prettyPrintCountResponseItem" structureRef="counter:prettyPrintCountResponse" />
在声明服务任务前,需要定义实际引用Web服务的BPMN接口与操作。基本上,是定义“接口”与所需的“操作”。对每一个操作都可以重复使用之前定义的“传入”与“传出”消息。例如,下面的声明定义了“counter”接口及“prettyPrintCountOperation”操作:
1
2
3
4
5
6
7 <interface name="Counter Interface" implementationRef="counter:Counter">
<operation id="prettyPrintCountOperation" name="prettyPrintCount Operation"
implementationRef="counter:prettyPrintCount">
<inMessageRef>tns:prettyPrintCountRequestMessage</inMessageRef>
<outMessageRef>tns:prettyPrintCountResponseMessage</outMessageRef>
</operation>
</interface>
这样就可以使用##WebService实现,声明Web服务任务,并引用Web服务操作。
1
2
3
4 <serviceTask id="webService"
name="Web service invocation"
implementation="##WebService"
operationRef="tns:prettyPrintCountOperation">
Web服务任务IO规范
除非使用简化方法处理输入与输出数据关联(见下),否则需要为每个Web服务任务声明IO规范,指出任务的输入与输出是什么。这个方法很简单,也兼容BPMN 2.0。在prettyPrint例子中,根据之前声明的条目定义,定义输入与输出:
1
2
3
4
5
6
7
8
9
10 <ioSpecification>
<dataInput itemSubjectRef="tns:prettyPrintCountRequestItem" id="dataInputOfServiceTask" />
<dataOutput itemSubjectRef="tns:prettyPrintCountResponseItem" id="dataOutputOfServiceTask" />
<inputSet>
<dataInputRefs>dataInputOfServiceTask</dataInputRefs>
</inputSet>
<outputSet>
<dataOutputRefs>dataOutputOfServiceTask</dataOutputRefs>
</outputSet>
</ioSpecification>
Web服务任务数据输入关联
有两种指定数据输入关联的方式:
-
使用表达式
-
使用简化方法
使用表达式指定数据输入关联,需要定义源及目标条目,并指定每个条目与字段的关联。下面的例子中,我们针对每个条目,指定prefix与suffix字段:
1
2
3
4
5
6
7
8
9
10
11
12 <dataInputAssociation>
<sourceRef>dataInputOfProcess</sourceRef>
<targetRef>dataInputOfServiceTask</targetRef>
<assignment>
<from>${dataInputOfProcess.prefix}</from>
<to>${dataInputOfServiceTask.prefix}</to>
</assignment>
<assignment>
<from>${dataInputOfProcess.suffix}</from>
<to>${dataInputOfServiceTask.suffix}</to>
</assignment>
</dataInputAssociation>
也可以使用更简单明了的简化方法。'sourceRef’元素是一个Flowable变量名,'targetRef’是条目定义的参数。在下面的例子里,将’PrefixVariable’变量的值关联至’prefix’字段,并将’SuffixVariable’变量的值关联至’suffix’字段。
1
2
3
4
5
6
7
8 <dataInputAssociation>
<sourceRef>PrefixVariable</sourceRef>
<targetRef>prefix</targetRef>
</dataInputAssociation>
<dataInputAssociation>
<sourceRef>SuffixVariable</sourceRef>
<targetRef>suffix</targetRef>
</dataInputAssociation>
Web服务任务数据输出关联
有两种指定数据输出关联的方式:
-
使用表达式
-
使用简化方法
使用表达式指定数据输出关联,需要定义目标变量及源表达式。这种方法很简单,与数据输入关联类似:
1
2
3
4 <dataOutputAssociation>
<targetRef>dataOutputOfProcess</targetRef>
<transformation>${dataOutputOfServiceTask.prettyPrint}</transformation>
</dataOutputAssociation>
也可以使用更简单明了的简化方法。'sourceRef’是条目定义的参数,'targetRef’元素是Flowable变量名。这种方法很简单,与数据输入关联类似:
1
2
3
4 <dataOutputAssociation>
<sourceRef>prettyPrint</sourceRef>
<targetRef>OutputVariable</targetRef>
</dataOutputAssociation>
8.5.5. 业务规则任务
描述
业务规则任务(business rule task)用于同步地执行一条或多条规则。Flowable使用名为Drools Expert的Drools规则引擎执行业务规则。目前,业务规则中包含的.drl文件,必须与定义了业务规则服务并执行规则的流程定义一起部署。这意味着流程中使用的所有.drl文件都需要打包在流程BAR文件中,与任务表单等类似。要了解如何为Drools Expert创建业务规则,请访问位于JBoss Drools的Drools文档。
如果想要使用自己的规则任务实现,比如希望通过不同方法使用Drools,或者想使用完全不同的规则引擎,则可以使用BusinessRuleTask的class或expression属性。这样它会与服务任务的行为完全相同。
图示
业务规则任务显示为带有表格图标的圆角矩形。
XML表示
要执行与流程定义在同一个BAR文件中部署的一条或多条业务规则,需要定义输入与结果变量。输入变量可以用流程变量的列表定义,使用逗号分隔。输出变量只能有一个变量名,将执行业务规则后的输出对象存储至流程变量。请注意结果变量会包含对象的List。如果没有指定结果变量名,默认为org.flowable.engine.rules.OUTPUT。
下面的业务规则任务,执行与流程定义一起部署的所有业务规则:
1
2
3
4
5
6
7
8
9
10
11
12
13 <process id="simpleBusinessRuleProcess">
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="businessRuleTask" />
<businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
flowable:resultVariable="rulesOutput" />
<sequenceFlow sourceRef="businessRuleTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
也可以将业务规则任务配置为只执行部署的.drl文件中的一组规则。要做到这一点,需要指定规则名字的列表,用逗号分隔。
1
2 <businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
flowable:rules="rule1, rule2" />
这样只会执行rule1与rule2。
也可以定义需要从执行中排除的规则列表。
1
2 <businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
flowable:rules="rule1, rule2" exclude="true" />
这个例子中,除了rule1与rule2之外,其它所有与流程定义一起部署在同一个BAR文件中的规则都会被执行。
前面提到过,还可以自行指定BusinessRuleTask的实现:
1 <businessRuleTask id="businessRuleTask" flowable:class="${MyRuleServiceDelegate}" />
这样配置的业务规则任务与服务任务的行为完全一样,但仍保持业务规则任务的图标,代表在这里处理业务规则。
8.5.6. 邮件任务
Flowable让你可以通过自动的邮件服务任务(email task),增强业务流程。可以向一个或多个收信人发送邮件,支持cc,bcc,HTML文本,等等。请注意邮件任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,邮件任务实现为一种特殊的服务任务。
配置邮件服务器
Flowable引擎使用支持SMTP的外部邮件服务器发送邮件。为了发送邮件,引擎需要了解如何连接邮件服务器。可以在flowable.cfg.xml配置文件中设置下面的参数:
参数 | 必填? | 描述 |
---|---|---|
mailServerHost |
否 |
邮件服务器的主机名(如mail.mycorp.com)。默认为 |
mailServerPort |
是,如果不使用默认端口 |
邮件服务器的SMTP端口。默认值为25 |
mailServerDefaultFrom |
否 |
若用户没有提供地址,默认使用的邮件发件人地址。默认为flowable@flowable.org |
mailServerUsername |
若服务器需要 |
部分邮件服务器发信时需要进行认证。默认为空。 |
mailServerPassword |
若服务器需要 |
部分邮件服务器发信时需要进行认证。默认为空。 |
mailServerUseSSL |
若服务器需要 |
部分邮件服务器要求ssl通信。默认设置为false。 |
mailServerUseTLS |
若服务器需要 |
部分邮件服务器要求TLS通信(例如gmail)。默认设置为false。 |
定义邮件任务
邮件任务实现为特殊的服务任务,将服务任务的type定义为'mail'进行设置。
1 <serviceTask id="sendMail" flowable:type="mail">
邮件任务通过字段注入配置。这些参数的值可以使用EL表达式,并将在流程执行运行时解析。可以设置下列参数:
参数 | 必填? | 描述 |
---|---|---|
to |
是 |
邮件的收信人。可以使用逗号分隔的列表定义多个接收人 |
from |
否 |
邮件的发信人地址。如果不设置,会使用默认配置的地址 |
subject |
否 |
邮件的主题 |
cc |
否 |
邮件的抄送人。可以使用逗号分隔的列表定义多个接收人 |
bcc |
否 |
邮件的密送人。可以使用逗号分隔的列表定义多个接收人 |
charset |
否 |
可以指定邮件的字符集,对许多非英语语言很必要。 |
html |
否 |
邮件的HTML文本 |
text |
否 |
邮件的内容,用于纯文本邮件。对于不支持富文本内容的客户端,可以与html一起使用。邮件客户端可以回退为显式纯文本格式。 |
htmlVar |
否 |
存储邮件HTML内容的流程变量名。与html参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。 |
textVar |
否 |
存储邮件纯文本内容的流程变量名。与text参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。 |
ignoreException |
否 |
处理邮件失败时,是忽略还是抛出FlowableException。默认设置为false。 |
exceptionVariableName |
否 |
如果设置ignoreException = true,而处理邮件失败时,则使用给定名字的变量保存失败信息 |
示例 usage
下面的XML代码片段是使用邮件任务的示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 <serviceTask id="sendMail" flowable:type="mail">
<extensionElements>
<flowable:field name="from" stringValue="order-shipping@thecompany.com" />
<flowable:field name="to" expression="${recipient}" />
<flowable:field name="subject" expression="Your order ${orderId} has been shipped" />
<flowable:field name="html">
<flowable:expression>
<![CDATA[
<html>
<body>
Hello ${male ? 'Mr.' : 'Mrs.' } ${recipientName},<br/><br/>
As of ${now}, your order has been <b>processed and shipped</b>.<br/><br/>
Kind regards,<br/>
TheCompany.
</body>
</html>
]]>
</flowable:expression>
</flowable:field>
</extensionElements>
</serviceTask>
8.5.7. Http任务
Http任务(Http task)用于发出HTTP请求,增强了Flowable的集成能力。请注意Http任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Http任务实现为一种特殊的服务任务。
配置Http客户端
Flowable使用可配置的Http客户端发出Http请求。如果不进行设置,会使用默认配置。
示例配置:
1
2
3
4
5
6
7
8
9
10
11
12 <bean id="processEngineConfiguration"
class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- http客户端配置 -->
<property name="httpClientConfig" ref="httpClientConfig"/>
</bean>
<bean id="httpClientConfig" class="org.flowable.engine.cfg.HttpClientConfig">
<property name="connectTimeout" value="5000"/>
<property name="socketTimeout" value="5000"/>
<property name="connectionRequestTimeout" value="5000"/>
<property name="requestRetryLimit" value="5"/>
</bean>
参数 | 必填? | 描述 |
---|---|---|
connectTimeout |
否 |
连接超时时间,以毫秒计。 |
socketTimeout |
否 |
Socket超时时间,以毫秒计。 |
connectionRequestTimeout |
否 |
请求连接超时时间。以毫秒计 |
requestRetryLimit |
否 |
请求重试次数(“0”代表不重试) |
disableCertVerify |
否 |
禁用SSL证书验证。 |
定义Http任务
Http任务实现为特殊的服务任务,将服务任务的type定义为'http'进行设置。
1 <serviceTask id="httpGet" flowable:type="http">
可以使用自定义的实现,覆盖Http任务的默认行为。 需要扩展org.flowable.http.HttpActivityBehavior,并覆盖perform()方法。
需要在任务定义中设置httpActivityBehaviorClass字段(默认值为 org.flowable.http.impl.HttpActivityBehaviorImpl)。
当前使用的默认实现HttpActivityBehaviorImpl基于Apache Http Client。尽管Apache Http Client可以使用很多方法自定义,但我们并没有在Http客户端配置中使用全部选项。
参考 Http Client builder 创建自定义客户端。
<serviceTask id="httpGet" flowable:type="http"> <extensionElements> <flowable:field name="httpActivityBehaviorClass"> <flowable:string> <![CDATA[org.example.flowable.HttpActivityBehaviorCustomImpl]]> </flowable:string> </flowable:field> </extensionElements> </sericeTask>
配置Http任务
Http任务通过字段注入配置。所有参数都可以配置为EL表达式,在运行时进行解析。可以设置下列参数:
参数 | 必填? | 描述 |
---|---|---|
requestMethod |
是 |
请求方法 |
requestUrl |
yes |
请求URL |
requestHeaders |
否 |
行分隔的Http请求头。 |
requestBody |
否 |
请求体 |
requestTimeout |
否 |
请求超时时间。单位为毫秒 |
disallowRedirects |
否 |
是否禁用Http重定向。 |
failStatusCodes |
否 |
逗号分隔的Http状态码,将令请求失败并抛出FlowableException。 |
handleStatusCodes |
否 |
逗号分隔的Http状态码,将令任务抛出BpmnError,并可用错误边界事件捕获。 |
ignoreException |
否 |
是否忽略异常。异常将被捕获,并存储在名为<taskId>.errorMessage的变量中。 |
saveRequestVariables |
否 |
是否保存请求变量。 |
saveResponseParameters |
否 |
是否保存全部的响应变量,包括HTTP状态码,响应头等。 |
resultVariablePrefix |
否 |
执行变量名的前缀。 |
saveResponseParametersTransient |
否 |
若为true,则会将响应体变量(如果设置了保存响应头,状态码,也包括在内)设置为瞬时变量。 |
saveResponseVariableAsJson |
否 |
若为true,则响应体会保存为JSON变量,而非String。如果HTTP服务返回JSON,并且想使用点注记方式使用字段(如myResponse.user.name),这个配置就很有用。 |
httpActivityBehaviorClass |
否 |
org.flowable.http.HttpActivityBehavior类的自定义扩展的全限定类名。 |
除了上面提到的字段,使用saveResponseParameters还会在执行成功后设置下列变量。
变量 | 可选? | 描述 |
---|---|---|
responseProtocol |
是 |
Http版本。 |
responseReason |
是 |
Http响应原因短语。 |
responseStatusCode |
是 |
Http响应状态码(例如 - 200)。 |
responseHeaders |
是 |
行分隔的Http响应头。 |
responseBody |
是 |
字符串形式的响应体,若有。 |
errorMessage |
是 |
被忽略的异常信息,若有。 |
结果变量
请注意上述执行变量名都会使用resultVariablePrefix前缀。 例如,可以在其他活动中,使用task7.responseStatusCode获取响应状态码。 其中task7是服务任务的id。可以设置resultVariablePrefix覆盖这个行为。
示例
下面的XML片段是使用Http任务的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 <serviceTask id="httpGet" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod" stringValue="GET" />
<flowable:field name="requestUrl" stringValue="http://flowable.org" />
<flowable:field name="requestHeaders">
<flowable:expression>
<![CDATA[
Accept: text/html
Cache-Control: no-cache
]]>
</flowable:expression>
</flowable:field>
<flowable:field name="requestTimeout">
<flowable:expression>
<![CDATA[
${requestTimeout}
]]>
</flowable:expression>
</flowable:field>
<flowable:field name="resultVariablePrefix">
<flowable:string>task7</flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
错误处理
默认情况下,当发生链接、IO或其他未处理的异常时,Http任务抛出FlowableException。 默认情况下,不会处理任何重定向/客户端/服务端错误状态码。
可以设置failStatusCodes及/或handleStatusCodes字段,配置Http任务处理异常及Http状态的方式。参见配置Http任务。
由handleStatusCodes抛出的BpmnError与其他BPMN异常一样,需要由对应的错误边界事件处理。 下面是一些Http任务错误处理及重试的例子。
400及5XX失败,异步执行,并按照failedJobRetryTimeCycle重试的Http任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <serviceTask id="failGet" name="Fail test" flowable:async="true" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod">
<flowable:string><![CDATA[GET]]></flowable:string>
</flowable:field>
<flowable:field name="requestUrl">
<flowable:string><![CDATA[http://localhost:9798/api/fail]]></flowable:string>
</flowable:field>
<flowable:field name="failStatusCodes">
<flowable:string><![CDATA[400, 5XX]]></flowable:string>
</flowable:field>
<flowable:failedJobRetryTimeCycle>R3/PT5S</flowable:failedJobRetryTimeCycle>
</extensionElements>
</serviceTask>
将400处理为BmpnError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 <serviceTask id="handleGet" name="HTTP Task" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod">
<flowable:string><![CDATA[GET]]></flowable:string>
</flowable:field>
<flowable:field name="requestUrl">
<flowable:string><![CDATA[http://localhost:9798/api/fail]]></flowable:string>
</flowable:field>
<flowable:field name="handleStatusCodes">
<flowable:string><![CDATA[4XX]]></flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
<boundaryEvent id="catch400" attachedToRef="handleGet">
<errorEventDefinition errorRef="HTTP400"></errorEventDefinition>
</boundaryEvent>
忽略异常
1
2
3
4
5
6
7
8
9
10
11
12
13 <serviceTask id="ignoreTask" name="Fail test" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod">
<flowable:string><![CDATA[GET]]></flowable:string>
</flowable:field>
<flowable:field name="requestUrl">
<flowable:string><![CDATA[http://nohost:9798/api]]></flowable:string>
</flowable:field>
<flowable:field name="ignoreException">
<flowable:string><![CDATA[true]]></flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
异常映射
参见异常映射
8.5.8. Mule任务
Mule任务可以向Mule发送消息,增强Flowable的集成特性。请注意Mule任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Mule任务实现为一种特殊的服务任务。
定义Mule任务
Mule任务实现为特殊的服务任务,将服务任务的type定义为'mule'进行设置。
1 <serviceTask id="sendMule" flowable:type="mule">
Mule任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:
参数 | 必填? | 描述 |
---|---|---|
endpointUrl |
是 |
希望调用的Mule端点(endpoint)。 |
language |
是 |
计算payloadExpression字段所用的语言。 |
payloadExpression |
是 |
消息载荷的表达式。 |
resultVariable |
否 |
存储调用结果的变量名。 |
示例
下面的XML代码片段是使用Mule任务的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <extensionElements>
<flowable:field name="endpointUrl">
<flowable:string>vm://in</flowable:string>
</flowable:field>
<flowable:field name="language">
<flowable:string>juel</flowable:string>
</flowable:field>
<flowable:field name="payloadExpression">
<flowable:string>"hi"</flowable:string>
</flowable:field>
<flowable:field name="resultVariable">
<flowable:string>theVariable</flowable:string>
</flowable:field>
</extensionElements>
8.5.9. Camel任务
Camel任务(Camel task)可以向Camel发送消息,增强Flowable的集成特性。请注意Camel任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Camel任务实现为一种特殊的服务任务。还请注意,需要在项目中包含Flowable Camel模块才能使用Camel任务。
定义Camel任务
Camel任务实现为特殊的服务任务,将服务任务的type定义为'camel'进行设置。
1 <serviceTask id="sendCamel" flowable:type="camel">
只需要在流程定义的服务任务上定义Camel类型即可。集成逻辑都通过Camel容器委托。默认情况下Flowable引擎在Spring容器中查找camelContext Bean。camelContext Bean定义由Camel容器装载的Camel路由。在下面的例子中,按照指定的Java包装载路由。但也可以自行在Spring配置中直接定义路由。
1
2
3
4
5 <camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
<packageScan>
<package>org.flowable.camel.route</package>
</packageScan>
</camelContext>
可以在Camel网站找到关于Camel路由的更多文档。下面只通过几个小例子展示基本概念。在第一个例子中,在Flowable工作流中进行最简单的Camel调用。叫做SimpleCamelCall。
如果想要定义多个Camel上下文Bean,或想使用不同的Bean名字,可以在Camel任务定义中像这样覆盖:
1
2
3
4
5 <serviceTask id="serviceTask1" flowable:type="camel">
<extensionElements>
<flowable:field name="camelContext" stringValue="customCamelContext" />
</extensionElements>
</serviceTask>
简单Camel调用示例
这个例子相关的所有文件,都可以在flowable-camel模块的org.flowable.camel.examples.simpleCamelCall包中找到。目标是简单地启动一个Camel路由。首先需要一个配置了上面提到的路由的Spring上下文。下面的代码实现这个目的:
1
2
3
4
5 <camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
<packageScan>
<package>org.flowable.camel.examples.simpleCamelCall</package>
</packageScan>
</camelContext>
1
2
3
4
5
6
7 public class SimpleCamelCallRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("flowable:SimpleCamelCallProcess:simpleCall").to("log:org.flowable.camel.examples.SimpleCamelCall");
}
}
路由只是记录消息体,不做更多事情。请注意from端点(endpoint)的格式,包含冒号分隔的三个部分:
端点URL部分 | 描述 |
---|---|
flowable |
指向引擎端点 |
SimpleCamelCallProcess |
流程名 |
simpleCall |
流程中Camel服务的名字 |
现在已经配置好路由,可以访问Camel。下面需要像这样定义工作流:
1
2
3
4
5
6
7
8
9 <process id="SimpleCamelCallProcess">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="simpleCall"/>
<serviceTask id="simpleCall" flowable:type="camel"/>
<sequenceFlow id="flow2" sourceRef="simpleCall" targetRef="end"/>
<endEvent id="end"/>
</process>
连通性测试示例
示例已经可以工作,但实际上Camel与Flowable之间并没有通信,因此没有太多价值。在这个例子里,将试着从Camel接收与发送消息。我们将发送一个字符串,Camel在其上附加一些文字并返回作为结果。发送部分比较普通,即以变量的格式将信息发送给Camel服务。这是我们的调用代码:
1
2
3
4
5
6
7
8
9
10
11
12
13 @Deployment
public void testPingPong() {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("input", "Hello");
Map<String, String> outputMap = new HashMap<String, String>();
variables.put("outputMap", outputMap);
runtimeService.startProcessInstanceByKey("PingPongProcess", variables);
assertEquals(1, outputMap.size());
assertNotNull(outputMap.get("outputValue"));
assertEquals("Hello World", outputMap.get("outputValue"));
}
“input”变量是实际上是Camel路由的输入,而outputMap用于捕获Camel传回的结果。流程像是这样:
1
2
3
4
5
6
7
8
9 <process id="PingPongProcess">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="ping"/>
<serviceTask id="ping" flowable:type="camel"/>
<sequenceFlow id="flow2" sourceRef="ping" targetRef="saveOutput"/>
<serviceTask id="saveOutput" flowable:class="org.flowable.camel.examples.pingPong.SaveOutput" />
<sequenceFlow id="flow3" sourceRef="saveOutput" targetRef="end"/>
<endEvent id="end"/>
</process>
请注意SaveOutput服务任务会从上下文中取出“Output”变量,并存储至上面提到的OutputMap。现在需要了解变量如何发送至Camel,以及如何返回。这就需要了解Camel行为(Behavior)的概念。变量与Camel通信的方式可以通过CamelBehavior配置。在这个例子里使用默认配置,其它配置在后面会进行简短介绍。可以使用类似的代码配置期望的Camel行为:
1
2
3
4
5 <serviceTask id="serviceTask1" flowable:type="camel">
<extensionElements>
<flowable:field name="camelBehaviorClass" stringValue="org.flowable.camel.impl.CamelBehaviorCamelBodyImpl" />
</extensionElements>
</serviceTask>
如果不指定行为,则会设置为org.flowable.camel.impl.CamelBehaviorDefaultImpl。这个行为会将变量复制到相同名字的Camel参数。无论选择什么行为,对于返回值:如果Camel消息体是一个map,则其中的每个元素都将复制为变量;否则整个对象将复制为名为"camelBody"的特定变量。以第二个例子作为Camel路由的总结:
1
2
3
4 @Override
public void configure() throws Exception {
from("flowable:PingPongProcess:ping").transform().simple("${property.input} World");
}
在这个路由中,字符串"world"会在结尾连接上名为“input”的参数,并将结果作为消息体返回。可以通过Java服务任务访问"camelBody"变量。也可以访问“outputMap”获取。除了这个例子中使用的默认行为之外,我们还可以看看其他的方式。在每个Camel路由开始时,流程实例ID会复制为名为"PROCESS_ID_PROPERTY"的Camel参数。之后会用于将流程实例与Camel路由相关联。也可以在Camel路由中直接使用。
Flowable提供了三种不同的行为。可以通过修改路由URL中特定的部分覆写行为。这里有个在URL中覆写已有行为的例子:
1 from("flowable:asyncCamelProcess:serviceTaskAsync2?copyVariablesToProperties=true").
下表展示了三种可用的Camel行为:
行为 | URL中 | 描述 |
---|---|---|
CamelBehaviorDefaultImpl |
copyVariablesToProperties |
将Flowable变量复制为Camel参数。 |
CamelBehaviorCamelBodyImpl |
copyCamelBodyToBody |
只将名为"camelBody"的Flowable变量复制为Camel消息体。 |
CamelBehaviorBodyAsMapImpl |
copyVariablesToBodyAsMap |
将一个map中的所有Flowable变量复制为Camel消息体。 |
上表展示了Flowable变量如何传递给Camel。下表展示Camel变量如何返回至Flowable。需要在路由URL中进行配置。
URL | 描述 |
---|---|
Default |
如果Camel消息体是一个map,则将其中每一对象复制为Flowable变量;否则将整个Camel消息体复制为"camelBody" Flowable变量。 |
copyVariablesFromProperties |
将Camel参数以同名复制为Flowable变量。 |
copyCamelBodyToBodyAsString |
与default相同,但如果Camel消息体不是map,则首先将其转换为字符串,然后再复制为"camelBody"。 |
copyVariablesFromHeader |
额外地,将Camel头复制为Flowable的同名变量。 |
返回变量
上面提到的变量传递,不论是从Camel到Flowable还是反向,都只用于变量传递的起始侧。
要特别注意,由于Flowable的行为是非阻塞的,Flowable不会自动向Camel返回变量。
为此提供了特殊的语法。可以在Camel路由URL中,以var.return.someVariableName
的格式,指定一个或多个参数。与这些参数同名(但没有var.return
部分)的变量会作为输出变量。因此将会以相同的名字复制回Camel参数。
例如在如下路由中:
from("direct:start").to("flowable:process?var.return.exampleVar").to("mock:result");
名为exampleVar
的Flowable变量会作为输出变量。因此会以同名复制回Camel参数。
异步连通性测试示例
上面的例子全都是同步的。流程实例等待,直到Camel路由结束并返回。有时,需要Flowable流程实例继续运行。这时可以使用Camel服务任务的异步功能。可以将Camel服务任务的async参数设置为true,启用这个功能。
1 <serviceTask id="serviceAsyncPing" flowable:type="camel" flowable:async="true"/>
设置这个参数后,Camel路由会由Flowable作业执行器异步启动。如果在Camel路由中定义了队列,Flowable流程实例会继续执行流程定义中Camel服务任务之后的活动。Camel路由会与流程执行完全异步地执行。如果需要在流程定义的某处等待Camel服务任务的响应,可以使用接收任务(receive task)。
1 <receiveTask id="receiveAsyncPing" name="Wait State" />
流程实例会等待,直到接收到来自Camel的信号。可以在Camel中向特定的Flowable端点发送消息,来为流程实例发送信号。
1 from("flowable:asyncPingProcess:serviceAsyncPing").to("flowable:asyncPingProcess:receiveAsyncPing");
-
“flowable”字符串常量
-
流程名
-
接收任务名
使用Camel路由实例化工作流
上面的例子都是先启动Flowable流程实例,然后在流程实例中启动Camel路由。也可以反过来,在已经启动的Camel路由中启动或调用流程实例。类似于为接收任务发送消息。例如,一个简单的路由:
1 from("direct:start").to("flowable:camelProcess");
可以看到,URL包含两部分:第一部分是“flowable”字符串常量,第二部分是流程定义的名字。当然,需要提前在Flowable引擎中部署这个流程定义。
也可以在Camel头中,将流程实例起动人设置为某个已认证用户ID。为此,首先需要在流程定义中指定启动人变量:
1 <startEvent id="start" flowable:initiator="initiator" />
然后使用Camel头CamelProcessInitiatorHeader指定用户ID。Camel路由定义如下:
1
2
3 from("direct:startWithInitiatorHeader")
.setHeader("CamelProcessInitiatorHeader", constant("kermit"))
.to("flowable:InitiatorCamelCallProcess?processInitiatorHeaderName=CamelProcessInitiatorHeader");
8.5.10. 手动任务
描述
手动任务(manual task)定义在BPM引擎之外的任务。它用于建模引擎不需要了解,也不需要提供系统或用户界面的工作。对于引擎来说,手动任务将按直接穿过活动处理,在流程执行到达手动任务时,自动继续执行流程。
图示
手动任务用左上角有一个小“手”图标的标准BPMN 2.0任务(圆角矩形)表示。
XML表示
1 <manualTask id="myManualTask" name="Call client for more information" />
8.5.11. Java接收任务
描述
接收任务(receive task),是等待特定消息到达的简单任务。目前,我们只为这个任务实现了Java语义。当流程执行到达接收任务时,流程状态将提交至持久化存储。这意味着流程将保持等待状态,直到引擎接收到特定的消息,触发流程穿过接收任务继续执行。
图示
接收任务用左上角有一个消息图标的标准BPMN 2.0任务(圆角矩形)表示。消息图标是白色的(对应的黑色消息图标代表发送的含义)。
XML表示
1 <receiveTask id="waitState" name="wait" />
要使流程实例从接收任务的等待状态中继续执行,需要使用到达接收任务的执行id,调用runtimeService.signal(executionId)。下面的代码片段展示了如何操作:
1
2
3
4
5
6
7
8 ProcessInstance pi = runtimeService.startProcessInstanceByKey("receiveTask");
Execution execution = runtimeService.createExecutionQuery()
.processInstanceId(pi.getId())
.activityId("waitState")
.singleResult();
assertNotNull(execution);
runtimeService.trigger(execution.getId());
8.5.12. Shell任务
描述
Shell任务(Shell task)可以运行Shell脚本与命令。请注意Shell任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。
定义Shell任务
Shell任务实现为特殊的服务任务,将服务任务的type定义为'shell'进行设置。
1 <serviceTask id="shellEcho" flowable:type="shell">
Shell任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:
参数 | 必填? | 类型 | 描述 | 默认值 |
---|---|---|---|---|
command |
是 |
String |
要执行的Shell命令。 |
|
arg0-5 |
否 |
String |
参数0至参数5 |
|
wait |
否 |
true/false |
是否等待Shell进程终止。 |
true |
redirectError |
否 |
true/false |
是否将标准错误(standard error)并入标准输出(standard output)。 |
false |
cleanEnv |
否 |
true/false |
是否避免Shell进程继承当前环境。 |
false |
outputVariable |
否 |
String |
保存输出的变量名 |
不会记录输出。 |
errorCodeVariable |
否 |
String |
保存结果错误码的变量名 |
不会记录错误码。 |
directory |
否 |
String |
Shell进程的默认目录 |
当前目录 |
示例
下面的XML代码片段是使用Shell任务的例子。将会运行"cmd /c echo EchoTest" Shell脚本,等待其结束,并将其结果存入resultVar。
1
2
3
4
5
6
7
8
9
10 <serviceTask id="shellEcho" flowable:type="shell" >
<extensionElements>
<flowable:field name="command" stringValue="cmd" />
<flowable:field name="arg1" stringValue="/c" />
<flowable:field name="arg2" stringValue="echo" />
<flowable:field name="arg3" stringValue="EchoTest" />
<flowable:field name="wait" stringValue="true" />
<flowable:field name="outputVariable" stringValue="resultVar" />
</extensionElements>
</serviceTask>
8.5.13. 执行监听器
执行监听器(execution listener)可以在流程执行中发生特定的事件时,执行外部Java代码或计算表达式。可以被捕获的事件有:
-
流程实例的启动和结束。
-
流程执行转移。
-
活动的启动和结束。
-
网关的启动和结束。
-
中间事件的启动和结束。
-
启动事件的结束,和结束事件的启动。
下面的流程定义包含了三个执行监听器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 <process id="executionListenersProcess">
<extensionElements>
<flowable:executionListener
class="org.flowable.examples.bpmn.executionlistener.ExampleExecutionListenerOne"
event="start" />
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="secondTask">
<extensionElements>
<flowable:executionListener
class="org.flowable.examples.bpmn.executionListener.ExampleExecutionListenerTwo" />
</extensionElements>
</sequenceFlow>
<userTask id="secondTask" >
<extensionElements>
<flowable:executionListener
expression="${myPojo.myMethod(execution.event)}"
event="end" />
</extensionElements>
</userTask>
<sequenceFlow sourceRef="secondTask" targetRef="thirdTask" />
<userTask id="thirdTask" />
<sequenceFlow sourceRef="thirdTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
第一个执行监听器将在流程启动时收到通知。这个监听器是一个外部Java类(ExampleExecutionListenerOne
),并且需要实现org.flowable.engine.delegate.ExecutionListener
接口。当该事件发生时(这里是start
事件),会调用notify(ExecutionListenerExecution execution)
方法。
1
2
3
4
5
6
7 public class ExampleExecutionListenerOne implements ExecutionListener {
public void notify(ExecutionListenerExecution execution) throws Exception {
execution.setVariable("variableSetInExecutionListener", "firstValue");
execution.setVariable("eventReceived", execution.getEventName());
}
}
也可以使用实现了org.flowable.engine.delegate.JavaDelegate
接口的委托类。这些委托类也可以用于其他的结构,如服务任务的委托。
第二个执行监听器在流程执行转移时被调用。请注意listener
元素并未定义event
,因为在转移上只会触发take
事件。当监听器定义在转移上时,event
属性的值将被忽略。
最后一个执行监听器在secondTask
活动结束时被调用。监听器声明中没有使用class
,而是定义了expression
。这个表达式将在事件触发时计算/调用。
1 <flowable:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />
与其他表达式一样,可以使用与解析execution变量。execution对象提供了露事件名参数,可以使用execution.eventName
向你的方法传递事件名。
与服务任务类似,执行监听器也支持使用delegateExpression
。
1 <flowable:executionListener event="start" delegateExpression="${myExecutionListenerBean}" />
较早之前,我们也引入了新的执行监听器类型,org.flowable.engine.impl.bpmn.listener.ScriptExecutionListener。这个脚本执行监听器可以为执行监听器事件执行一段脚本代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <flowable:executionListener event="start"
class="org.flowable.engine.impl.bpmn.listener.ScriptExecutionListener">
<flowable:field name="script">
<flowable:string>
def bar = "BAR"; // 局部变量
foo = "FOO"; // 将变量放入执行上下文
execution.setVariable("var1", "test"); // 测试访问执行实例
bar // 隐式返回值
</flowable:string>
</flowable:field>
<flowable:field name="language" stringValue="groovy" />
<flowable:field name="resultVariable" stringValue="myVar" />
</flowable:executionListener>
执行监听器上的字段注入
使用通过class
属性配置的执行监听器时,可以使用字段注入。与服务任务字段注入使用完全相同的机制,可以在那里看到字段注入的各种用法。
下面的代码片段展示了一个简单的示例流程,带有一个使用了字段注入的执行监听器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <process id="executionListenersProcess">
<extensionElements>
<flowable:executionListener
class="org.flowable.examples.bpmn.executionListener.ExampleFieldInjectedExecutionListener"
event="start">
<flowable:field name="fixedValue" stringValue="Yes, I am " />
<flowable:field name="dynamicValue" expression="${myVar}" />
</flowable:executionListener>
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
1
2
3
4
5
6
7
8
9
10
11 public class ExampleFieldInjectedExecutionListener implements ExecutionListener {
private Expression fixedValue;
private Expression dynamicValue;
public void notify(ExecutionListenerExecution execution) throws Exception {
execution.setVariable("var", fixedValue.getValue(execution).toString() +
dynamicValue.getValue(execution).toString());
}
}
ExampleFieldInjectedExecutionListener
类将连接两个字段(一个是固定值-fixedValue,另一个是动态值-dynamicValue),并将其存储在'var
'流程变量中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 @Deployment(resources = {
"org/flowable/examples/bpmn/executionListener/ExecutionListenersFieldInjectionProcess.bpmn20.xml"})
public void testExecutionListenerFieldInjection() {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("myVar", "listening!");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"executionListenersProcess", variables);
Object varSetByListener = runtimeService.getVariable(processInstance.getId(), "var");
assertNotNull(varSetByListener);
assertTrue(varSetByListener instanceof String);
// 结果为固定注入字段及注入表达式的连接
assertEquals("Yes, I am listening!", varSetByListener);
}
请注意,与服务任务使用相同的线程安全规则。请阅读相应章节了解更多信息。
8.5.14. 任务监听器
任务监听器(task listener)用于在特定的任务相关事件发生时,执行自定义的Java逻辑或表达式。
任务监听器只能在流程定义中作为用户任务的子元素。请注意,任务监听器是一个Flowable自定义结构,因此也需要作为BPMN 2.0 extensionElements,放在flowable命名空间下。
1
2
3
4
5 <userTask id="myTask" name="My Task" >
<extensionElements>
<flowable:taskListener event="create" class="org.flowable.MyTaskCreateListener" />
</extensionElements>
</userTask>
任务监听器包含下列属性:
-
event(事件)(必填):触发任务监听器的任务事件类型。可用的事件有:
-
create(创建):当任务已经创建,并且所有任务参数都已经设置时触发。
-
assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,在触发create事件之前,会首先触发assignment事件。这顺序看起来不太自然,但是有实际原因的:当收到create事件时,我们通常希望能看到任务的所有参数,包括办理人。
-
complete(完成):当任务已经完成,从运行时数据中删除前触发。
-
delete(删除):在任务即将被删除前触发。请注意任务由completeTask正常完成时也会触发。
-
-
class:需要调用的委托类。这个类必须实现
org.flowable.engine.delegate.TaskListener
接口。
1
2
3
4
5
6 public class MyTaskCreateListener implements TaskListener {
public void notify(DelegateTask delegateTask) {
// 这里是要实现的业务逻辑
}
}
也可以使用字段注入,为委托类传递流程变量或执行。请注意委托类的实例在流程部署时创建(与Flowable中其它的委托类一样),这意味着该实例会在所有流程实例执行中共享。
-
expression:(不能与class属性一起使用):指定在事件发生时要执行的表达式。可以为被调用的对象传递
DelegateTask
对象与事件名(使用task.eventName
)作为参数。
1 <flowable:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
-
delegateExpression:指定一个能够解析为
TaskListener
接口实现类的对象的表达式。与服务任务类似。
1 <flowable:taskListener event="create" delegateExpression="${myTaskListenerBean}" />
-
较早之前,我们也引入了新的执行监听器类型,org.flowable.engine.impl.bpmn.listener.ScriptTaskListener。这个脚本任务监听器可以为一个任务监听器事件执行一段脚本代码。
1
2
3
4
5
6
7
8
9
10
11
12 <flowable:taskListener event="complete" class="org.flowable.engine.impl.bpmn.listener.ScriptTaskListener" >
<flowable:field name="script">
<flowable:string>
def bar = "BAR"; // 局部变量
foo = "FOO"; // 将变量放入执行上下文
task.setOwner("kermit"); // 测试访问任务实例
bar // 隐式返回值
</flowable:string>
</flowable:field>
<flowable:field name="language" stringValue="groovy" />
<flowable:field name="resultVariable" stringValue="myVar" />
</flowable:taskListener>
8.5.15. 多实例(for each)
描述
多实例活动(multi-instance activity)是在业务流程中,为特定步骤定义重复的方式。在编程概念中,多实例类似for each结构:可以为给定集合中的每一条目,顺序或并行地,执行特定步骤,甚至是整个子流程。
多实例是一个普通活动,加上定义(被称作“多实例特性的”)额外参数,会使得活动在运行时被多次执行。下列活动可以成为多实例活动:
按照BPMN2.0规范的要求,用于为每个实例创建执行的父执行,会提供下列变量:
-
nrOfInstances:实例总数。
-
nrOfActiveInstances:当前活动的(即未完成的),实例数量。对于顺序多实例,这个值总为1。
-
nrOfCompletedInstances:已完成的实例数量。
可以调用execution.getVariable(x)
方法获取这些值。
另外,每个被创建的执行,都有局部变量(对其他执行不可见,也不存储在流程实例级别):
-
loopCounter:给定实例在for-each循环中的index。可以通过Flowable的elementIndexVariable属性为loopCounter变量重命名。
图示
如果一个活动是多实例,将通过在该活动底部的三条短线表示。三条竖线代表实例会并行执行,而三条横线代表顺序执行。
XML表示
要将活动变成多实例,该活动的XML元素必须有multiInstanceLoopCharacteristics
子元素
1
2
3 <multiInstanceLoopCharacteristics isSequential="false|true">
...
</multiInstanceLoopCharacteristics>
isSequential属性代表了活动的实例为顺序还是并行执行。
实例的数量在进入活动时,计算一次。有几种不同方法可以配置数量。一个方法是通过loopCardinality子元素,直接指定数字。
1
2
3 <multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>5</loopCardinality>
</multiInstanceLoopCharacteristics>
也可以使用解析为正整数的表达式:
1
2
3 <multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
</multiInstanceLoopCharacteristics>
另一个定义实例数量的方法,是使用loopDataInputRef
子元素,指定一个集合型流程变量的名字。对集合中的每一项,都会创建一个实例。可以使用inputDataItem
子元素,将该项设置给该实例的局部变量。在下面的XML示例中展示:
1
2
3
4
5
6 <userTask id="miTasks" name="My Task ${loopCounter}" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false">
<loopDataInputRef>assigneeList</loopDataInputRef>
<inputDataItem name="assignee" />
</multiInstanceLoopCharacteristics>
</userTask>
假设变量assigneeList
包含[kermit, gonzo, fozzie]
。上面的代码会创建三个并行的用户任务。每一个执行都有一个名为assignee
的(局部)流程变量,含有集合中的一项,并在这个例子中被用于指派用户任务。
loopDataInputRef
与inputDataItem
的缺点是名字很难记,并且由于BPMN 2.0概要的限制,不能使用表达式。Flowable通过在multiInstanceCharacteristics
上提供collection与elementVariable属性解决了这些问题:
1
2
3
4
5 <userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="${myService.resolveUsersForTask()}" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
请注意collection
属性会作为表达式进行解析。如果表达式解析为字符串而不是一个集合,不论是因为本身配置的就是静态字符串值,还是表达式计算结果为字符串,这个字符串都会被当做变量名,在流程变量中用于获取实际的集合。
例如,下面的代码片段会要求集合存储在assigneeList
流程变量中:
1
2
3
4
5 <userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="assigneeList" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
假如myService.getCollectionVariableName()
返回字符串值,引擎就会用这个值作为变量名,获取流程变量保存的集合。
1
2
3
4
5 <userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="${myService.getCollectionVariableName()}" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
多实例活动在所有实例都完成时结束。然而,也可以指定一个表达式,在每个实例结束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例活动,继续执行流程。这个表达式必须通过completionCondition子元素定义。
1
2
3
4
5
6 <userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="assigneeList" flowable:elementVariable="assignee" >
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
在这个例子里,会为assigneeList
集合中的每个元素创建并行实例。当60%的任务完成时,其他的任务将被删除,流程继续运行。
边界事件与多实例
多实例是普通活动,因此可以在其边界定义边界事件。如果是中断边界事件,当其捕获事件时,会销毁活动中的所有实例。以下面的多实例子流程为例:
当定时器触发时,子流程的所有实例都会被销毁,无论有多少实例,或者实例的内部活动是否完成。
多实例与执行监听器
执行监听器与多实例一起使用时需要特别注意。以下面的BPMN 2.0 XML代码片段为例。这段XML定义在与multiInstanceLoopCharacteristics XML元素相同的级别:
1
2
3
4 <extensionElements>
<flowable:executionListener event="start" class="org.flowable.MyStartListener"/>
<flowable:executionListener event="end" class="org.flowable.MyEndListener"/>
</extensionElements>
对于普通的BPMN活动,会在活动开始与结束时调用一次监听器。
但是当该活动为多实例时,行为有区别:
-
当进入多实例活动时,在任何内部活动执行前,抛出一个启动事件。loopCounter变量还未设置(为null)。
-
进入每个实际执行的活动时,抛出一个启动事件。loopCounter变量已经设置。
结束事件类似:
-
离开每个实际执行的活动后,抛出一个结束事件。loopCounter变量已经设置。
-
多实例活动整体完成后,抛出一个结束事件。loopCounter变量未设置。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13 <subProcess id="subprocess1" name="Sub Process">
<extensionElements>
<flowable:executionListener event="start" class="org.flowable.MyStartListener"/>
<flowable:executionListener event="end" class="org.flowable.MyEndListener"/>
</extensionElements>
<multiInstanceLoopCharacteristics isSequential="false">
<loopDataInputRef>assignees</loopDataInputRef>
<inputDataItem name="assignee"></inputDataItem>
</multiInstanceLoopCharacteristics>
<startEvent id="startevent2" name="Start"></startEvent>
<endEvent id="endevent2" name="End"></endEvent>
<sequenceFlow id="flow3" name="" sourceRef="startevent2" targetRef="endevent2"></sequenceFlow>
</subProcess>
在这个例子中,假设assignees有三项。在运行时会发生如下事情:
-
多实例整体抛出一个启动事件。调用一次start执行监听器,loopCounter与assignee变量均未设置(即为null)。
-
每一个活动实例抛出一个启动事件。调用三次start执行监听器,loopCounter与assignee变量均已设置(也就是说不为null)。
-
因此启动执行监听器总共被调用四次。
请注意,即使multiInstanceLoopCharacteristics不是定义在子流程上,也是一样。例如,如果上面的例子中只是一个简单的用户任务,抛出事件的行为也是一样。
8.5.16. 补偿处理器
描述
如果要使用一个活动补偿另一个活动的影响,可以将其声明为补偿处理器(compensation handler)。补偿处理器不在正常流程中执行,而只在流程抛出补偿事件时才会执行。
补偿处理器不得有入口或出口顺序流。
补偿处理器必须通过单向的连接,关联一个补偿边界事件。
图示
如果一个活动是补偿处理器,则会在其下部中间显示补偿事件图标。下面摘录的流程图展示了一个带有补偿边界事件的服务任务,并关联至一个补偿处理器。请注意补偿处理器图标显示在"cancel hotel reservation(取消酒店预订)"服务任务的下部中间。
XML表示
要将一个活动声明为补偿处理器,需要将isForCompensation
属性设置为true:
1
2 <serviceTask id="undoBookHotel" isForCompensation="true" flowable:class="...">
</serviceTask>
8.6. 子流程与调用活动
8.6.1. 子流程
描述
子流程(sub-process)是包含其他的活动、网关、事件等的活动。其本身构成一个流程,并作为更大流程的一部分。子流程完全在父流程中定义(这就是为什么经常被称作嵌入式子流程)。
子流程有两个主要的使用场景:
-
子流程可以分层建模。很多建模工具都可以折叠子流程,隐藏子流程的所有细节,而只显示业务流程的高层端到端总览。
-
子流程会创建新的事件范围。在子流程执行中抛出的事件可以通过子流程边界上的边界事件捕获,为该事件创建了限制在子流程内的范围。
使用子流程也要注意以下几点:
-
子流程只能有一个空启动事件,而不允许有其他类型的启动事件。请注意BPMN 2.0规范允许省略子流程的启动与结束事件,但目前Flowable的实现尚不支持省略。
-
顺序流不能跨越子流程边界。
图示
子流程表示为标准活动(圆角矩形)。若折叠了子流程,则只显示其名字与一个加号,以展示流程的高层概览:
若展开了子流程,则在子流程内显示子流程的所有步骤:
使用子流程的一个主要原因是为事件定义范围。下面的流程模型展示了这种用法:investigate software(调查硬件)/investigate hardware(调查软件)两个任务需要并行执行,且需要在给定时限内,在Level 2 support(二级支持)响应前完成。在这里,定时器的范围(即需要按时完成的活动)通过子流程进行限制。
XML表示
子流程通过subprocess元素定义。子流程中的所有活动、网关、事件等,都需要定义在这个元素内。
1
2
3
4
5
6
7
8
9 <subProcess id="subProcess">
<startEvent id="subProcessStart" />
... 其他子流程元素 ...
<endEvent id="subProcessEnd" />
</subProcess>
8.6.2. 事件子流程
描述
事件子流程(event sub-process)是BPMN 2.0新定义的。事件子流程是通过事件触发的子流程。可以在流程级别,或者任何子流程级别,添加事件子流程。用于触发事件子流程的事件,使用启动事件进行配置。因此可知,不能在事件子流程中使用空启动事件。事件子流程可以通过消息事件、错误事件、信号时间、定时器事件或补偿事件等触发。在事件子流程的宿主范围(流程实例或子流程)创建时,创建对启动事件的订阅。当该范围销毁时,删除订阅。
事件子流程可以是中断或不中断的。中断的子流程将取消当前范围内的任何执行。非中断的事件子流程将创建新的并行执行。宿主范围内的每个活动,只能触发一个中断事件子流程,而非中断事件子流程可以多次触发。子流程是否是中断的,通过触发事件子流程的启动事件配置。
事件子流程不能有任何入口或出口顺序流。事件子流程是由事件触发的,因此入口顺序流不合逻辑。当事件子流程结束时,要么同时结束当前范围(中断事件子流程的情况),要么是非中断子流程创建的并行执行结束。
目前的限制:
-
Flowable支持错误、定时器、信号与消息启动事件触发事件子流程。
XML表示
事件子流程的XML表示形式与嵌入式子流程相同。但需要将triggeredByEvent
属性设置为true
:
1
2
3 <subProcess id="eventSubProcess" triggeredByEvent="true">
...
</subProcess>
示例
下面是使用错误启动事件触发事件子流程的例子。该事件子流程处于“流程级别”,即流程实例的范围:
事件子流程在XML中是这样的:
1
2
3
4
5
6
7 <subProcess id="eventSubProcess" triggeredByEvent="true">
<startEvent id="catchError">
<errorEventDefinition errorRef="error" />
</startEvent>
<sequenceFlow id="flow2" sourceRef="catchError" targetRef="taskAfterErrorCatch" />
<userTask id="taskAfterErrorCatch" name="Provide additional data" />
</subProcess>
前面已经指出,事件子流程也可以添加到嵌入式子流程内。若添加到嵌入式子流程内,可以代替边界事件的功能。例如在下面两个流程图中,嵌入式子流程都抛出错误事件。错误事件都被捕获,并由用户任务处理。
对比:
两种情况下都执行相同的任务。然而,两种模型有如下不同:
-
嵌入式(事件)子流程使用其宿主范围的执行来执行。这意味着嵌入式(事件)子流程可以访问其范围的局部变量。当使用边界事件时,执行嵌入式子流程的执行,会被边界事件的出口顺序流删除。意味着嵌入式子流程创建的变量将不再可用。
-
使用事件子流程时,事件完全由其所在的子流程处理。当使用边界事件时,事件由其父流程处理。
这两点可以帮助你判断哪种方式更适合解决特定的流程建模或实现问题,以选择使用边界事件还是嵌入式(事件)子流程。
8.6.3. 事务子流程
描述
事务子流程(transaction sub-process)是一种嵌入式子流程,用于将多个活动组织在一个事务里。事务是工作的逻辑单元,可以组织一组独立活动,使得它们可以一起成功或失败。
事务的可能结果:事务有三种不同的结果:
-
若未被取消,或被意外终止,则事务成功(successful)。若事务子流程成功,将使用出口顺序流离开。若流程后面抛出了补偿事件,成功的事务可以被补偿。请注意:与“普通”嵌入式子流程一样,可以使用补偿抛出中间事件,在事务成功完成后补偿。
-
若执行到达取消结束事件时,事务被取消(canceled)。在这种情况下,所有执行都将被终止并移除。只会保留一个执行,设置为取消边界事件,并将触发补偿。在补偿完成后,事务子流程通过取消边界事件的出口顺序流离开。
-
若由于抛出了错误结束事件,且未被事务子流程所在的范围捕获,则事务会被意外(hazard)终止。错误被事件子流程的边界捕获也一样。在这种情况下,不会进行补偿。
下面的流程图展示这三种不同的结果:
与ACID事务的关系:要注意不要将BPMN事务子流程与技术(ACID)事务混淆。BPMN事务子流程不是划分技术事务范围的方法。要理解Acitivit中的事务管理,请阅读并发与事务章节。BPMN事务与ACID事务有如下区别:
-
ACID事务生存期一般比较短,而BPMN事务可以持续几小时,几天甚至几个月才完成。考虑一个场景:事务包括的活动中有一个用户任务。通常人的响应时间要比程序长。或者,在另一个场景下,BPMN事务可能等待某些业务事件发生,像是特定订单的填写完成。这些操作通常要比更新数据库字段、使用事务队列存储消息等,花长得多的时间完成。
-
不可能将业务活动的持续时间限定为ACID事务的范围,因此一个BPMN事务通常会生成多个ACID事务。
-
一个BPMN事务可以生成多个ACID事务,也就不能使用ACID特性。例如,考虑上面的流程例子。假设"book hotel(预订酒店)"与"charge credit card(信用卡付款)"操作在分开的ACID事务中处理。再假设"book hotel(预订酒店)"活动已经成功。这时,因为已经进行了预订酒店操作,而还没有进行信用卡扣款,就处在中间不一致状态(intermediary inconsistent state)。在ACID事务中,会顺序进行不同的操作,因此也处在中间不一致状态。在这里不一样的是,不一致状态在事务范围外可见。例如,如果通过外部预订服务进行预定,则使用该预订服务的其他部分将能看到酒店已被预订。这意味着,当使用业务事务时,完全不会使用隔离参数(的确,当使用ACID事务时,我们通常也会降低隔离级别,以保证高并发级别。但ACID事务可以细粒度地进行控制,而中间不一致状态也只会存在于一小段时间内)。
-
BPMN业务事务不使用传统方式回滚。这是因为它生成多个ACID事务,在BPMN事务取消时,部分ACID事务可能已经提交。这样它们没法回滚。
因为BPMN事务天生需要长时间运行,因此就需要使用不同的方式缺乏隔离与回滚机制造成的问题。在实际使用中,通常只能通过领域特定(domain specific)的方式解决这些问题:
-
通过补偿实现回滚。如果在事务范围内抛出了取消事件,就补偿所有成功执行并带有补偿处理器的活动所造成的影响。
-
缺乏隔离通常使用特定领域的解决方案来处理。例如,在上面的例子里,在我们确定第一个客户可以付款前,一个酒店房间可能被第二个客户预定。这可能不满足业务预期,因此预订服务可能会选择允许一定量的超量预定。
-
另外,由于事务可以由于意外而终止,预订服务需要处理这种情况,比如酒店房间已经预定,但从未付款(因为事务可能已经终止)。在这种情况下,预定服务可能选择这种策略:一个酒店房间有最大预留时间,若到时还未付款,则取消预订。
总结一下:尽管ACID事务提供了对回滚、隔离级别,与启发式结果(heuristic outcomes)等问题的通用解决方案,但仍然需要在实现业务事务时,为这些问题寻找特定领域的解决方案。
目前的限制:
-
BPMN规范要求,流程引擎响应底层事务协议提交的事务。如果在底层协议中发生了取消事件,则取消事务。作为嵌入式的引擎,Flowable当前不支持这点。查看下面介绍一致性的段落,了解其后果。
基于ACID事务与乐观锁(optimistic concurrency)的一致性:BPMN事务在如下情况保证一致性:所有活动都成功完成;或若部分活动不能执行,则所有已完成活动都被补偿。两种方法都可以达到最终一致性状态。然而需要了解的是:Flowable中BPMN事务的一致性模型,以流程执行的一致性模型为基础。Flowable以事务的方式执行流程,并通过乐观锁标记处理并发。在Flowable中,BPMN的错误、取消与补偿事件,都建立在相同的ACID事务与乐观锁之上。例如,只有在实际到达时,取消结束事件才能触发补偿。如果服务任务抛出了非受检异常,导致并未实际到达取消结束事件;或者,由于底层ACID事务中的其他操作,将事务设置为rollback-only(回滚)状态,导致补偿处理器的操作不能提交;或者,当两个并行执行到达一个取消结束事件时,补偿会被两次触发,并由于乐观锁异常而失败。这些情况下都不能真正完成补偿。想说明的是,当在Flowable中实现BPMN事务时,与实施“普通”流程与子流程,需要遵守相同的规则。因此实现流程时需要有效地保证一致性,需要将乐观锁、事务执行模型纳入考虑范围。
XML表示
事务子流程在XML中通过transaction
标签表示:
1
2
3 <transaction id="myTransaction" >
...
</transaction>
示例
下面是一个事务子流程的例子:
8.6.4. 调用活动(子流程)
描述
尽管看起来很相像,但在BPMN 2.0中,调用活动(call activity)有别于一般的子流程——通常也称作嵌入式子流程。从概念上说,两者都在流程执行到达该活动时,调用一个子流程。
两者的区别为,调用活动引用一个流程定义外部的流程,而子流程嵌入在原有流程定义内。调用活动的主要使用场景是,在多个不同流程定义中调用一个可复用的流程定义。
当流程执行到达调用活动时,会创建一个新的执行,作为到达调用活动的执行的子执行。这个子执行用于执行子流程,也可用于创建并行子执行(与普通流程中行为类似)。父执行将等待子流程完成,之后沿原流程继续执行。
XML表示
调用活动是一个普通活动,在calledElement中通过key引用流程定义。在实际使用中,通常在calledElement中配置流程的ID。
1 <callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
请注意子流程的流程定义在运行时解析。这意味着如果需要的话,子流程可以与调用流程分别部署。
传递变量
可以向子流程传递与接收流程变量。数据将在子流程启动时复制到子流程,并在其结束时复制回主流程。
1
2
3
4
5
6
7
8 <callActivity id="callSubProcess" calledElement="checkCreditProcess">
<extensionElements>
<flowable:in source="someVariableInMainProcess"
target="nameOfVariableInSubProcess" />
<flowable:out source="someVariableInSubProcess"
target="nameOfVariableInMainProcess" />
</extensionElements>
</callActivity>
可以将inheritVariables
设置为true,将所有流程变量传递给子流程。
1 <callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:inheritVariables="true"/>
除了需要按照BPMN 2.0标准的方式声明流程变量的BPMN标准元素dataInputAssociation与dataOutputAssociation之外, Flowable还提供了扩展作为快捷方式。
也可以在这里使用表达式:
1
2
3
4
5
6 <callActivity id="callSubProcess" calledElement="checkCreditProcess" >
<extensionElements>
<flowable:in sourceExpression="${x+5}" target="y" />
<flowable:out source="${y+5}" target="z" />
</extensionElements>
</callActivity>
因此最终 z = y+5 = x+5+5 。
调用活动元素还提供了一个自定义Flowable属性扩展,businessKey,用于设置子流程实例的businessKey。
<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:businessKey="${myVariable}"> ... </callActivity>
将inheritBusinessKey属性设置为true
,会将子流程的businessKey值设置为调用流程的businessKey的值。
<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:inheritBusinessKey="true"> ... </callActivity>
引用同一部署中的流程
默认会使用引用流程最后部署的流程定义版本。但有的时候也会想引用与主流程一起部署的引用流程定义。这需要将主流程与引用流程放在同一个部署单元中,以便引用相同的部署。
在callActivity
元素中,将sameDeployment
属性设置为true
,即可引用相同部署的流程。
如下例所示:
<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:sameDeployment="true"> ... </callActivity>
sameDeployment
默认值为false。
示例
下面的流程图展示了简单的订单处理流程。因为检查客户的信用额度的操作在许多其他流程中都通用,因此将check credit step(检查信用额度步骤)建模为调用活动。
流程像是下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13 <startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="receiveOrder" />
<manualTask id="receiveOrder" name="Receive Order" />
<sequenceFlow id="flow2" sourceRef="receiveOrder" targetRef="callCheckCreditProcess" />
<callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
<sequenceFlow id="flow3" sourceRef="callCheckCreditProcess" targetRef="prepareAndShipTask" />
<userTask id="prepareAndShipTask" name="Prepare and Ship" />
<sequenceFlow id="flow4" sourceRef="prepareAndShipTask" targetRef="end" />
<endEvent id="end" />
子流程像是下面这样:
与子流程的流程定义相比没什么特别。但调用活动的流程也可以不通过其他流程调用而直接使用。
8.7. 事务与并发
8.7.1. 异步延续
(Asynchronous Continuations)
Flowable以事务方式执行流程,并可按照你的需求配置。让我们从Flowable一般如何为事务划分范围开始介绍。如果Flowable被触发(启动流程,完成任务,为执行发送信号),Flowable将沿流程执行,直到到达每个执行路径的等待状态。更具体地说,它以深度优先方式搜索流程图,并在每个执行分支都到达等待状态时返回。等待状态是“之后”再执行的任务,也就是说着Flowable将当前执行持久化,并等待再次触发。触发可以来自外部来源如用户任务或消息接受任务,也可以来自Flowable自身如定时器事件。以下面的图片说明:
这是一个BPMN流程的片段,有一个用户任务、一个服务任务,与一个定时器事件。用户任务的完成操作与验证地址(validate address)在同一个工作单元内,因此需要原子性地(atomically)成功或失败。这意味着如果服务任务抛出了异常,我们会想要回滚当前事务,以便执行返回到用户任务,并希望用户任务仍然保存在数据库中。这也是Flowable的默认行为。在(1)中,应用或客户端线程完成任务。在相同的线程中,Flowable执行服务并继续,直到到达等待状态,在这个例子中,是定时器事件(2)。然后将控制权返回至调用者(3),同时提交事务(如果事务由Flowable开启)。
在有的情况下,我们不想要这样。有时我们需要在流程中自定义地控制事务边界,以便为工作的逻辑单元划分范围。这就需要使用异步延续。考虑下面的流程(片段):
完成用户任务,生成发票,并将发票发送给客户。这次发票的生成不再是同一个工作单元的一部分,因此我们不希望当发票生成失败时,回滚用户任务。所以我们希望Flowable做的,是完成用户任务(1),提交事务,并将控制权返回给调用程序。然后我们希望在后台线程中,异步地生成发票。这个后台线程就是Flowable作业执行器(事实上是一个线程池),它周期性地将作业保存至数据库。因此在幕后,当到达"generate invoice(生成发票)"任务时,Flowable会创建“消息”作业并将其持久化到数据库中,用于继续执行流程。这个作业之后会被作业执行器选中并执行。Flowable也会向本地的作业执行器进行提示,告知其有新作业到来,以提升性能。
要使用这个特性,可以使用flowable:async="true"扩展。因此,一个示例的服务任务会像是这样:
1
2
3 <serviceTask id="service1" name="Generate Invoice"
flowable:class="my.custom.Delegate"
flowable:async="true" />
可以为下列BPMN任务类型指定flowable:async:任务,服务任务,脚本任务,业务规则任务,发送任务,接收任务,用户任务,子流程,调用活动。
对于用户任务、接收任务与其他等待状态来说,异步操作允许我们在一个独立的线程/事务中启动执行监听器。
8.7.2. 失败重试
默认配置下,如果作业执行中有任何异常,Flowable将三次重试执行作业。对异步作业也是这样。需要更灵活的配置时可以使用这两个参数:
-
重试的次数
-
重试的间隔
这两个参数可以通过flowable:failedJobRetryTimeCycle
元素配置。这有一个简单的例子:
1
2
3
4
5
6
7 <serviceTask id="failingServiceTask" flowable:async="true"
flowable:class="org.flowable.engine.test.jobexecutor.RetryFailingDelegate">
<extensionElements>
<flowable:failedJobRetryTimeCycle>R5/PT7M</flowable:failedJobRetryTimeCycle>
</extensionElements>
</serviceTask>
时间周期表达式与定时器事件表达式一样遵循ISO 8601标准。上面的例子会让作业执行器重试5次,并在每次重试前等待7分钟。
8.7.3. 排他作业
从近期版本开始,JobExecutor确保同一个流程实例的作业永远不会并发执行。为什么这样?
为什么使用排他作业?
考虑下面的流程定义:
一个并行网关,之后是三个服务任务,都使用异步操作执行。其结果是在数据库中添加了三个作业。当作业储存在数据库后,就由JobExecutor处理。JobExecutor获取作业,并将其委托至工作线程的线程池,由它们实际执行作业。这意味着通过使用异步操作,可以将工作分发至线程池(在集群场景下,甚至会在集群中跨越多个线程池)。通常这都是好事。但也有其固有问题:一致性。考虑服务任务后的并行合并:当服务任务的执行完成时到达并行合并,并需要决定等待其他执行,还是需要继续向前。这意味着,对于每一个到达并行合并的分支,都需要选择继续执行,还是需要等待其他分支上的一个或多个其他执行。
为什么这是问题呢?这是因为服务任务配置为使用异步延续,有可能所有相应的作业都同时被作业执行器处理,并委托至不同的工作线程。结果是服务执行的事务,与到达并行合并的3个独立执行所在的事务会发生重叠。如果这样,每一个独立事务都“看不到”其他事物已经并发地到达了同样的并行合并,并因此判断自己需要等待其他事务。然而,如果每个事务都判断需要等待其他事务,在并行合并后不会有继续流程的事务,而流程实例也就会永远保持这个状态。
Flowable如何解决这个问题呢?Flowable使用乐观锁。每当需要基于数据进行判断,而数据可能不是最新值(因为其他事务可能在我们提交前修改了这个数据)时,我们确保会在每个事务中都增加同一个数据库记录行的版本号。这样,无论哪个事务第一个提交,都将成功,而其他的会抛出乐观锁异常并失败。这样就解决了上面流程中讨论的问题:如果多个执行并发到达并行合并,它们都判断需要等待,增加其父执行(流程实例)的版本号并尝试提交。无论哪个执行第一个提交,都可以成功提交,而其他的将会抛出乐观锁异常并失败。因为这些执行由作业触发,Flowable会在等待给定时间后,重试执行相同的作业,期望这一次通过这个同步的网关。
这是好的解决方案么?我们可以看到乐观锁使Flowable避免不一致状态。它确保了我们不会“在合并网关卡住”,也就是说:要么所有的执行都通过网关,要么数据库中的作业能确保可以重试通过它。然而,尽管这是一个持久化与一致性角度的完美解决方案,但从更高层次看,仍然不一定总是理想行为:
-
Flowable只会为同一个作业重试一个固定的最大次数(默认配置为3次)。在这之后,作业仍然保存在数据库中,但不会再重试。这就需要手动操作来触发作业。
-
如果一个作业有非事务性的副作用,则副作用将不会由于事务失败而回滚。例如,如果"book concert tickets(预定音乐会门票)"服务与Flowable不在同一个事务中,则重试执行作业将预定多张票。
什么是排他作业?
排他作业不能与同一个流程实例中的其他排他作业同时执行。考虑上面展示的流程:如果我们将服务任务都声明为排他的,则JobExecutor将确保相关的作业都不会并发执行。相反,它将确保不论何时从特定流程实例中获取了排他作业,都会从同一个流程实例中获取所有其他的排他作业,并将它们委托至同一个工作线程。这保证了作业的顺序执行。
如何启用这个特性?从近期版本开始,排他作业成为默认配置。所有异步操作与定时器事件都默认为排他的。另外,如果希望作业成为非排他的,可以使用flowable:exclusive="false"
配置。例如,下面的服务任务是异步,但非排他的。
1
2 <serviceTask id="service" flowable:expression="${myService.performBooking(hotel, dates)}"
flowable:async="true" flowable:exclusive="false" />
这是好的解决方案么?有很多人都在问这个问题。他们的顾虑是,这将阻止并行操作,因此会有性能问题。但也需要考虑以下两点:
-
如果你是专家,并且知道你在做什么(并理解“为什么排他作业?”章节的内容),可以关掉排他。否则,对大多数用户来说,异步操作与定时器能够正常工作才更重要。
-
事实上不会有性能问题。只有在重负载下才会有性能问题。重负载意味着作业执行器的所有的工作线程都一直忙碌。对于排他作业,Flowable会简单的根据负载不同进行分配。排他作业意味着同一个流程实例的作业都将在同一个线程中顺序执行。但是请想一下:我们有不止一个流程实例。其他流程实例的作业将被委托至其他线程,并与本实例的作业并发执行。也就是说Flowable不会并发执行同一个流程实例的排他作业,但会并发执行多个实例。从总吞吐量角度来看,可以期望大多数场景下都可以保证实例很快地完成。此外,执行同一个流程实例中下一个作业所需的数据,已经缓存在同一个执行集群节点中。如果作业与节点没有关联关系,则可能需要重新从数据库中获取数据。
8.8. 流程启动认证
默认情况下,任何人都可以启动已部署流程定义的新流程实例。可以使用流程启动认证功能定义用户与组,让Web客户端可以选择性的限制能够启动新流程实例的用户。请注意Flowable引擎不会用任何方式验证认证定义。这个功能只是为了开发人员可以简化Web客户端认证规则的实现。语法与为用户任务指派用户的语法类似:可以使用<flowable:potentialStarter>标签,将用户或组指派为流程的潜在启动者。这里有一个例子:
1
2
3
4
5
6
7
8
9
10
11 <process id="potentialStarter">
<extensionElements>
<flowable:potentialStarter>
<resourceAssignmentExpression>
<formalExpression>group2, group(group3), user(user3)</formalExpression>
</resourceAssignmentExpression>
</flowable:potentialStarter>
</extensionElements>
<startEvent id="theStart"/>
...
在上面的XML中,user(user3)直接引用用户user3,而group(group3)引用组group3。不显式标明的话,默认为组。也可以使用<process>标签提供的<flowable:candidateStarterUsers>与<flowable:candidateStarterGroups>的属性。这里有一个例子:
1
2
3 <process id="potentialStarter" flowable:candidateStarterUsers="user1, user2"
flowable:candidateStarterGroups="group1">
...
这些属性可以同时使用。
在流程启动认证定义后,开发者可以使用下列方法获取该认证定义。 这段代码获取可以由给定用户启动的流程定义列表:
1 processDefinitions = repositoryService.createProcessDefinitionQuery().startableByUser("userxxx").list();
也可以获取给定流程定义中,所有定义为潜在启动者的身份关联
1 identityLinks = repositoryService.getIdentityLinksForProcessDefinition("processDefinitionId");
下面的例子展示了如何获取能够启动给定流程的用户列表:
1
2
3 List<User> authorizedUsers = identityService().createUserQuery()
.potentialStarter("processDefinitionId")
.list();
用完全相同的方法,可以获取配置为给定流程定义的潜在启动者的组列表:
1
2
3 List<Group> authorizedGroups = identityService().createGroupQuery()
.potentialStarter("processDefinitionId")
.list();
8.9. 数据对象
BPMN提供了将数据对象定义为流程或子流程元素的一部分的可能性。根据BPMN规范,数据对象可以包含复杂的XML结构,并可以从XSD定义中引入。下列XSD类型为Flowable支持的第一批数据对象:
1
2
3
4
5
6 <dataObject id="dObj1" name="StringTest" itemSubjectRef="xsd:string"/>
<dataObject id="dObj2" name="BooleanTest" itemSubjectRef="xsd:boolean"/>
<dataObject id="dObj3" name="DateTest" itemSubjectRef="xsd:datetime"/>
<dataObject id="dObj4" name="DoubleTest" itemSubjectRef="xsd:double"/>
<dataObject id="dObj5" name="IntegerTest" itemSubjectRef="xsd:int"/>
<dataObject id="dObj6" name="LongTest" itemSubjectRef="xsd:long"/>
数据对象定义使用name属性值作为新变量的名字,将其自动转换为流程变量。另外,Flowable也提供了为变量设置默认值的扩展元素。下面的BPMN代码片段示例:
1
2
3
4
5
6
7 <process id="dataObjectScope" name="Data Object Scope" isExecutable="true">
<dataObject id="dObj123" name="StringTest123" itemSubjectRef="xsd:string">
<extensionElements>
<flowable:value>Testing123</flowable:value>
</extensionElements>
</dataObject>
...
9. 表单
Flowable提供了一种简便灵活的方式,用来为业务流程中的人工步骤添加表单。 有两种使用表单的方法:使用(由表单设计器创建的)表单定义的内置表单渲染,以及外部表单渲染。 使用外部表单渲染时,可以使用(自Explorer web应用V5版本支持的)表单参数;也可以使用表单key定义,引用外部的、使用自定义代码解析的表单。
9.1. 表单定义
表单引擎用户手册中提供了关于表单定义与Flowable表单引擎的完整信息。 可以使用Flowable表单设计器创建表单定义。表单设计器是Flowable Modeler web应用的一部分。也可以直接使用JSON编辑器创建表单定义。 表单引擎用户手册中介绍了表单定义JSON的完整结构。表单支持下列表单字段类型:
-
Text: 渲染为文本框
-
Multiline text: 渲染为多行文本框
-
Number: 渲染为只允许数字值的文本框
-
CheckBox: 渲染为复选框
-
Date: 渲染为日期框
-
Dropdown: 渲染为下拉选择框,候选值由字段定义配置
-
Radio buttons: 渲染为单选按钮,候选值由字段定义配置
-
People: 渲染为选人框,可以选择用户身份表中的用户
-
Group of people: 渲染为选组框,可以选择组身份表中的组
-
Upload: 渲染为上传框
-
Expression: 渲染为一个标签,可以在标签文字中使用JUEL表达式,以使用变量及/或其他动态值
Flowable Task应用可以用表单定义JSON渲染出html表单。 也可以使用Flowable API,自行获取表单定义JSON。
1 FormModel RuntimeService.getStartFormModel(String processDefinitionId, String processInstanceId)
或
1 FormModel TaskService.getTaskFormModel(String taskId)
FormModel对象是一个代表了表单定义JSON的Java对象。
可以调用下列API,使用启动表单定义以启动流程实例:
1
2 ProcessInstance RuntimeService.startProcessInstanceWithForm(String processDefinitionId, String outcome,
Map<String, Object> variables, String processInstanceName)
如果在流程定义的(某一个或多个)启动事件上定义了表单定义,则可以使用这个方法,使用启动表单中填写的值启动流程实例。 Flowable Task应用使用同样的方法,用表单启动流程实例。 通过变量map传入所有需要的表单值,也可以指定表单输出字符串及流程实例名。
类似的,可以调用下列API,使用表单完成用户任务:
1
2 void TaskService.completeTaskWithForm(String taskId, String formDefinitionId,
String outcome, Map<String, Object> variables)
再次强调,要获取关于表单定义的更多信息,请查看表单引擎用户手册。
9.2. 表单参数
所有与业务流程相关的信息,要么包含在流程变量里,要么可以通过流程变量引用。Flowable也支持将复杂的Java对象,以Serializable
对象、JPA实体,或整个XML文档存储为String
等方式,存储为流程变量。
启动流程与完成用户任务是用户参与流程的地方,所以需要使用UI界面渲染表单。为了简化UI设计,可以在流程定义中,将流程变量中的复杂的Java对象转换为Map<String,String>
格式的参数。
这样UI就可以使用Flowable API方法,在这些参数的基础上构建表单。这些参数可看做是专用(也更受限)的流程变量视图。可以使用FormData返回值,获取用于显示表单的参数。如
1 StartFormData FormService.getStartFormData(String processDefinitionId)
或
1 TaskFormData FormService.getTaskFormData(String taskId)
默认情况下,内建表单引擎能够“看到”参数与流程变量。因此如果任务表单参数一对一对应流程变量,则不需要专门进行声明。例如,对于如下声明:
1 <startEvent id="start" />
当执行到达startEvent时,所有流程变量都可用。但
1 formService.getStartFormData(String processDefinitionId).getFormProperties()
将为空,因为并未指定映射。
在上面的例子中,所有提交的参数都存储为流程变量。也就是说,只要简单地在表单中添加输入框,就可以存储新变量。
参数从流程变量衍生而来,但并不是必须存储为流程变量。例如,流程变量可以是Address类的JPA实体。而UI使用的StreetName
表单参数,则通过#{address.street}
表达式获取。
类似的,表单中用户需要提交的参数,可以存储为流程变量,也可以使用UEL值表达式,保存至某个流程变量的参数。如#{address.street}
。
除非使用formProperty
声明,否则提交参数时,默认将其存储为流程变量。
在表单参数与流程变量的处理过程中,也可以进行类型转换。
例如:
1
2
3
4
5
6
7
8 <userTask id="task">
<extensionElements>
<flowable:formProperty id="room" />
<flowable:formProperty id="duration" type="long"/>
<flowable:formProperty id="speaker" variable="SpeakerName" writable="false" />
<flowable:formProperty id="street" expression="#{address.street}" required="true" />
</extensionElements>
</userTask>
-
room
表单参数将作为String,映射为room
流程变量 -
duration
表单参数将作为java.lang.Long,映射为duration
流程变量 -
speaker
表单参数将被映射为SpeakerName
流程变量。将只在TaskFormData对象中可用。若提交了speaker参数,将抛出FlowableException。对应地,使用readable="false"
属性,参数可以提交,但不会在FormData中提供。 -
street
表单参数将作为String,映射为address
流程变量的Java bean参数street
。如果在提交时没有提供这个字段,required="true"将抛出异常。
StartFormData FormService.getStartFormData(String processDefinitionId)
与TaskFormdata FormService.getTaskFormData(String taskId)
方法返回的FormData同样提供类型元数据。
支持下列表单参数类型:
-
string
(org.flowable.engine.impl.form.StringFormType -
long
(org.flowable.engine.impl.form.LongFormType) -
double
(org.flowable.engine.impl.form.DoubleFormType) -
enum
(org.flowable.engine.impl.form.EnumFormType) -
date
(org.flowable.engine.impl.form.DateFormType) -
boolean
(org.flowable.engine.impl.form.BooleanFormType)
可以通过List<FormProperty> formService.getStartFormData(String processDefinitionId).getFormProperties()
与List<FormProperty> formService.getTaskFormData(String taskId).getFormProperties()
方法,获取每个表单参数的下列FormProperty
信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 public interface FormProperty {
/**
* 在{@link FormService#submitStartFormData(String, java.util.Map)}
* 或{@link FormService#submitTaskFormData(String, java.util.Map)}
* 中提交参数时使用的key
*/
String getId();
/** 显示标签 */
String getName();
/** 在本接口中定义的类型,例如{@link #TYPE_STRING} */
FormType getType();
/** 可选。这个参数需要显示的值 */
String getValue();
/** 这个参数是否可以读取:在表单中显示,并可通过
* {@link FormService#getStartFormData(String)}
* 与{@link FormService#getTaskFormData(String)}
* 方法访问。
*/
boolean isReadable();
/** 用户提交表单时是否可以包含这个参数? */
boolean isWritable();
/** 输入框中是否必填这个参数 */
boolean isRequired();
}
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <startEvent id="start">
<extensionElements>
<flowable:formProperty id="speaker"
name="Speaker"
variable="SpeakerName"
type="string" />
<flowable:formProperty id="start"
type="date"
datePattern="dd-MMM-yyyy" />
<flowable:formProperty id="direction" type="enum">
<flowable:value id="left" name="Go Left" />
<flowable:value id="right" name="Go Right" />
<flowable:value id="up" name="Go Up" />
<flowable:value id="down" name="Go Down" />
</flowable:formProperty>
</extensionElements>
</startEvent>
所有这些信息都可以通过API获取:用formProperty.getType().getName()
获取类型名、formProperty.getType().getInformation("datePattern")
获取日期格式、formProperty.getType().getInformation("values")
获取枚举值。
下面的XML代码片段
1
2
3
4
5
6
7 <startEvent>
<extensionElements>
<flowable:formProperty id="numberOfDays" name="Number of days" value="${numberOfDays}" type="long" required="true"/>
<flowable:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" value="${startDate}" datePattern="dd-MM-yyyy hh:mm" type="date" required="true" />
<flowable:formProperty id="vacationMotivation" name="Motivation" value="${vacationMotivation}" type="string" />
</extensionElements>
</userTask>
可以在自定义应用中用于渲染流程启动表单。
9.3. 外部表单渲染
也可以使用API,在Flowable引擎之外,自行渲染任务表单。下面的步骤介绍在自行渲染任务表单时,可以使用的钩子。
实际上,渲染表单所需的所有数据都组装在:StartFormData FormService.getStartFormData(String processDefinitionId)
与TaskFormdata FormService.getTaskFormData(String taskId)
这两个方法中。
可以通过ProcessInstance FormService.submitStartFormData(String processDefinitionId, Map<String,String> properties)
与void FormService.submitTaskFormData(String taskId, Map<String,String> properties)
提交表单参数。
查看表单参数了解如何将表单参数映射至流程变量。
如果希望按版本将表单与流程存储在一起,可以将表单模板资源放在部署的业务存档中,并可以使用String ProcessDefinition.getDeploymentId()
与InputStream RepositoryService.getResourceAsStream(String deploymentId, String resourceName);
,作为部署中的资源获取,在你的应用中渲染/显示表单。
除了任务表单之外,也可以获取其他部署资源。
String FormService.getStartFormData(String processDefinitionId).getFormKey()
与String FormService.getTaskFormData(String taskId).getFormKey()
API提供<userTask flowable:formKey="…"
属性。可以用它保存部署中模板的全名(如org/flowable/example/form/my-custom-form.xml
),但这并非唯一选择。例如,也可以在formKey中保存通用的key,通过算法或转换得到实际需要使用的模板。在你需要在不同的UI界面渲染不同的表单时很方便。例如,在普通屏幕尺寸的Web应用中显示一个表单,在手机等小屏幕中显示另一个表单。甚至可以为IM表单或邮件表单提供专门的模板。
10. JPA
(Java Persistence API Java持久化API)
可以使用JPA实体作为流程变量,这样可以:
-
使用流程变量更新已有的JPA实体。流程变量可以在用户任务的表单中填写,或者通过服务任务生成。
-
重用已有的领域模型,而不需要写专门的服务用于读取与更新实体。
-
基于已有实体做决策(网关)。
-
…
10.1. 需求
只支持完全满足下列条件的实体:
-
实体需要使用JPA注解配置,字段与参数访问器都可以。也可以使用映射的父类。
-
实体需要有使用
@Id
注解的主键,不支持复合主键(@EmbeddedId
与@IdClass
)。Id字段/参数可以是任何JPA规范支持的类型:原生类型与其包装器(除了boolean)、String
、BigInteger
、BigDecimal
、java.util.Date
与java.sql.Date
。
10.2. 配置
引擎必须引用EntityManagerFactory
才能使用JPA实体,可以配置引用,或者配置持久化单元名(Persistence Unit Name)。引擎会自动检测用作变量的JPA实体,并按需处理。
下面的示例配置使用jpaPersistenceUnitName:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <bean id="processEngineConfiguration"
class="org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<!-- 数据库配置 -->
<property name="databaseSchemaUpdate" value="true" />
<property name="jdbcUrl" value="jdbc:h2:mem:JpaVariableTest;DB_CLOSE_DELAY=1000" />
<property name="jpaPersistenceUnitName" value="flowable-jpa-pu" />
<property name="jpaHandleTransaction" value="true" />
<property name="jpaCloseEntityManager" value="true" />
<!-- 作业执行器配置 -->
<property name="asyncExecutorActivate" value="false" />
<!-- 邮件服务器配置 -->
<property name="mailServerPort" value="5025" />
</bean>
下面的示例配置使用自定义的EntityManagerFactory
(open-jpa实体管理器)。请注意这段代码只包含了与本例相关的bean,而省略了其他的bean。使用open-jpa实体管理器的完整可用示例,可以在flowable-spring-examples (/flowable-spring/src/test/java/org/flowable/spring/test/jpa/JPASpringTest.java
)中找到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitManager" ref="pum"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.OpenJpaVendorAdapter">
<property name="databasePlatform" value="org.apache.openjpa.jdbc.sql.H2Dictionary" />
</bean>
</property>
</bean>
<bean id="processEngineConfiguration" class="org.flowable.spring.SpringProcessEngineConfiguration">
<property name="dataSource" ref="dataSource" />
<property name="transactionManager" ref="transactionManager" />
<property name="databaseSchemaUpdate" value="true" />
<property name="jpaEntityManagerFactory" ref="entityManagerFactory" />
<property name="jpaHandleTransaction" value="true" />
<property name="jpaCloseEntityManager" value="true" />
<property name="asyncExecutorActivate" value="false" />
</bean>
也可以使用相同的配置,以编程方式构建引擎。例如:
1
2
3
4 ProcessEngine processEngine = ProcessEngineConfiguration
.createProcessEngineConfigurationFromResourceDefault()
.setJpaPersistenceUnitName("flowable-pu")
.buildProcessEngine();
配置参数:
-
jpaPersistenceUnitName
:要使用的持久化单元的名字。(需要确保该持久化单元在classpath中可用。根据规范,默认位置为/META-INF/persistence.xml
)。jpaEntityManagerFactory
与jpaPersistenceUnitName
二选一。 -
jpaEntityManagerFactory
:用于载入实体及刷入更新的,实现javax.persistence.EntityManagerFactory
的bean。jpaEntityManagerFactory与jpaPersistenceUnitName二选一。 -
jpaHandleTransaction
:标示是否需要由引擎负责开启及提交/回滚EntityManager上的事务。当使用Java Transaction API (JTA)时,需设置为false。 -
jpaCloseEntityManager
:标示是否需要由引擎负责关闭从EntityManagerFactory
获取的EntityManager
实例。当EntityManager由容器管理时(例如,使用扩展持久化上下文 Extended Persistence Context,而不将实体范围限制为单一事务时)需设置为false。
10.3. 使用
10.3.1. 简单示例
可以在Flowable源代码的JPAVariableTest中找到使用JPA变量的例子。下面逐步解释JPAVariableTest.testUpdateJPAEntityValues
。
首先,基于META-INF/persistence.xml
为持久化单元创建EntityManagerFactory。包括需要包含在持久化单元内的类及一些(数据库)厂商特定配置。
在这个测试里使用的是简单实体,包括id以及一个String
值参数。在运行测试前,先创建一个实体并保存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 @Entity(name = "JPA_ENTITY_FIELD")
public class FieldAccessJPAEntity {
@Id
@Column(name = "ID_")
private Long id;
private String value;
public FieldAccessJPAEntity() {
// JPA需要的空构造方法
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
启动一个新的流程实例,将这个实体加入变量。与其他变量一样,它们都会在引擎中持久化存储。当下一次请求这个变量时,将会根据类及Id,从EntityManager
载入。
1
2
3
4
5 Map<String, Object> variables = new HashMap<String, Object>();
variables.put("entityToUpdate", entityToUpdate);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"UpdateJPAValuesProcess", variables);
流程定义的第一个节点是服务任务
,将调用entityToUpdate
上的setValue
方法。entityToUpdate
将解析为之前启动流程实例时设置的JPA变量,并使用当前引擎的上下文中关联的EntityManager
载入。
1
2 <serviceTask id='theTask' name='updateJPAEntityTask'
flowable:expression="${entityToUpdate.setValue('updatedValue')}" />
当服务任务完成时,流程实例在流程定义中的用户任务处等待,以便我们可以查看流程实例。在这时,EntityManager
已经刷入,对实体的修改也已经存入数据库。当获取entityToUpdate
变量的值时,将重新载入实体。所以可以得到value
参数设置为updatedValue
的实体。
1
2
3
4 // 流程'UpdateJPAValuesProcess'中的服务任务已经设置了entityToUpdate的值。
Object updatedEntity = runtimeService.getVariable(processInstance.getId(), "entityToUpdate");
assertTrue(updatedEntity instanceof FieldAccessJPAEntity);
assertEquals("updatedValue", ((FieldAccessJPAEntity)updatedEntity).getValue());
10.3.2. 查询JPA流程变量
可以查询以特定JPA实体作为变量值的流程实例
与执行
。请注意ProcessInstanceQuery
与ExecutionQuery
中,只有variableValueEquals(name, entity)
方法支持JPA实体查询。而variableValueNotEquals
、variableValueGreaterThan
、variableValueGreaterThanOrEqual
、variableValueLessThan
与variableValueLessThanOrEqual
等方法都不支持JPA,并会在值传递为JPA实体时,抛出FlowableException
。
1
2 ProcessInstance result = runtimeService.createProcessInstanceQuery()
.variableValueEquals("entityToQuery", entityToQuery).singleResult();
10.3.3. 使用Spring bean与JPA的高级示例
可以在flowable-spring-examples
中找到高级用法的示例——JPASpringTest
。它描述了一个简单的用例:
-
使用一个已有的Spring bean及已定义的JPA实体,用于存储贷款申请。
-
Flowable通过该bean获取该实体,并将其用作流程中的变量。流程定义如下步骤:
-
使用
LoanRequestBean
,并使用启动流程时(从启动表单)接收的变量创建LoanRequest(贷款申请)实体的服务任务。使用flowable:resultVariable
将表达式结果,即所创建的实体存储为流程变量。 -
经理用于审核并批准/驳回申请的用户任务,并将审核结论存储为boolean变量
approvedByManager
。 -
更新贷款申请实体的服务任务,使实体与流程同步。
-
使用一个排他网关,依据
approved
实体参数的值选择下一步采用哪条路径:若申请被批准,结束流程;否则,生成一个额外任务(Send rejection letter 发送拒信),以便客户可以收到拒信得到通知。
-
请注意这个流程只用于单元测试,而不包含任何表单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 <?xml version="1.0" encoding="UTF-8"?>
<definitions id="taskAssigneeExample"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="org.flowable.examples">
<process id="LoanRequestProcess" name="Process creating and handling loan request">
<startEvent id='theStart' />
<sequenceFlow id='flow1' sourceRef='theStart' targetRef='createLoanRequest' />
<serviceTask id='createLoanRequest' name='Create loan request'
flowable:expression="${loanRequestBean.newLoanRequest(customerName, amount)}"
flowable:resultVariable="loanRequest"/>
<sequenceFlow id='flow2' sourceRef='createLoanRequest' targetRef='approveTask' />
<userTask id="approveTask" name="Approve request" />
<sequenceFlow id='flow3' sourceRef='approveTask' targetRef='approveOrDissaprove' />
<serviceTask id='approveOrDissaprove' name='Store decision'
flowable:expression="${loanRequest.setApproved(approvedByManager)}" />
<sequenceFlow id='flow4' sourceRef='approveOrDissaprove' targetRef='exclusiveGw' />
<exclusiveGateway id="exclusiveGw" name="Exclusive Gateway approval" />
<sequenceFlow id="endFlow1" sourceRef="exclusiveGw" targetRef="theEnd">
<conditionExpression xsi:type="tFormalExpression">${loanRequest.approved}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="endFlow2" sourceRef="exclusiveGw" targetRef="sendRejectionLetter">
<conditionExpression xsi:type="tFormalExpression">${!loanRequest.approved}</conditionExpression>
</sequenceFlow>
<userTask id="sendRejectionLetter" name="Send rejection letter" />
<sequenceFlow id='flow5' sourceRef='sendRejectionLetter' targetRef='theOtherEnd' />
<endEvent id='theEnd' />
<endEvent id='theOtherEnd' />
</process>
</definitions>
尽管上面的例子很简单,但也展示了JPA与Spring结合,以及带参数方法表达式的威力。这个流程本身完全不需要写Java代码(当然还是需要写Spring bean),大幅加速了开发。
11. 历史
历史是记录流程执行过程中发生的事情,并将其永久存储的组件。与运行时数据不同,历史数据在流程实例完成以后仍保存在数据库中。
有6个历史实体:
-
HistoricProcessInstance
保存当前及已结束流程实例的信息。 -
HistoricVariableInstance
保存流程变量或任务变量的最新值。 -
HistoricActivityInstance
保存活动(流程中的节点)的一次执行的信息。 -
HistoricTaskInstance
保存当前与历史(完成及删除的)任务实例的信息。 -
HistoricIdentityLink
保存任务及流程实例、当前及历史的身份关联的信息。 -
HistoricDetail
保存与历史流程实例、活动实例或任务实例等有关的多种信息。
历史与当前进行中的流程实例都在数据库中保存历史实体,因此可以选择直接查询历史表,以减少对运行时流程实例数据的访问,并提高运行时执行的性能。
11.1. 查询历史
可以使用HistoryServiceAPI提供的createHistoricProcessInstanceQuery()
、createHistoricVariableInstanceQuery()
、createHistoricActivityInstanceQuery()
、 getHistoricIdentityLinksForTask()
、getHistoricIdentityLinksForProcessInstance()
、createHistoricDetailQuery()
与createHistoricTaskInstanceQuery()
方法,查询全部6种历史实体。
下面用一些例子展示历史查询API的部分用法。可以在javadoc的org.flowable.engine.history
包中找到全部用法。
11.1.1. 历史流程实例查询
获取所有流程中,完成所花费时间(持续时间)排名前10的、流程定义为XXX的、已完成的HistoricProcessInstances(历史流程实例)
。
1
2
3
4
5 historyService.createHistoricProcessInstanceQuery()
.finished()
.processDefinitionId("XXX")
.orderByProcessInstanceDuration().desc()
.listPage(0, 10);
11.1.2. 历史变量实例查询
获取已完成的、id为’XXX’的流程实例中,所有的HistoricVariableInstances(历史变量实例)
,并以变量名排序。
1
2
3
4 historyService.createHistoricVariableInstanceQuery()
.processInstanceId("XXX")
.orderByVariableName.desc()
.list();
11.1.3. 历史活动实例查询
获取最新的、已完成的、流程定义的id为XXX的、服务任务类型的HistoricActivityInstance(历史活动实例)
。
1
2
3
4
5
6 historyService.createHistoricActivityInstanceQuery()
.activityType("serviceTask")
.processDefinitionId("XXX")
.finished()
.orderByHistoricActivityInstanceEndTime().desc()
.listPage(0, 1);
11.1.4. 历史详情查询
下面的的例子获取id为123的流程中所有的变量更新记录。这个查询只会返回HistoricVariableUpdate(历史变量更新)
。请注意一个变量名可能会有多个HistoricVariableUpdate
实体,这代表了流程中的每一次变量更新。可以使用orderByTime(按变量更新的时间)
或orderByVariableRevision(按变量更新的版本号)
对这些更新记录进行排序。
1
2
3
4
5 historyService.createHistoricDetailQuery()
.variableUpdates()
.processInstanceId("123")
.orderByVariableName().asc()
.list()
下面的例子获取流程id为"123"的、启动时提交或任何任务中提交的所有表单参数。这个查询只会返回HistoricFormProperties(历史表单参数)
。
1
2
3
4
5 historyService.createHistoricDetailQuery()
.formProperties()
.processInstanceId("123")
.orderByVariableName().asc()
.list()
最后一个例子获取id为"123"的任务进行的所有变量更新操作。将返回该任务设置的所有变量(任务局部变量)的HistoricVariableUpdates
,而不会返回流程实例中设置的变量。
1
2
3
4
5 historyService.createHistoricDetailQuery()
.variableUpdates()
.taskId("123")
.orderByVariableName().asc()
.list()
可以在TaskListener
中使用TaskService
或DelegateTask
设置任务局部变量:
1 taskService.setVariableLocal("123", "myVariable", "Variable value");
1
2
3 public void notify(DelegateTask delegateTask) {
delegateTask.setVariableLocal("myVariable", "Variable value");
}
11.1.5. 历史任务实例查询
获取所有任务中,完成所花费时间(持续时间)排名前10的、已完成的HistoricTaskInstance(历史任务实例)
。
1
2
3
4 historyService.createHistoricTaskInstanceQuery()
.finished()
.orderByHistoricTaskInstanceDuration().desc()
.listPage(0, 10);
获取删除原因包含"invalid"的、最终指派给kermit用户的HistoricTaskInstance
。
1
2
3
4
5 historyService.createHistoricTaskInstanceQuery()
.finished()
.taskDeleteReasonLike("%invalid%")
.taskAssignee("kermit")
.listPage(0, 10);
11.2. 配置历史
可以使用org.flowable.engine.impl.history.HistoryLevel枚举(或在5.11之前的版本中,ProcessEngineConfiguration
中定义的HISTORY常量),以编程方式配置历史级别:
1
2
3
4 ProcessEngine processEngine = ProcessEngineConfiguration
.createProcessEngineConfigurationFromResourceDefault()
.setHistory(HistoryLevel.AUDIT.getKey())
.buildProcessEngine();
也可以在flowable.cfg.xml或Spring上下文中配置级别:
1
2
3
4 <bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="history" value="audit" />
...
</bean>
可以配置下列历史级别:
-
none(无)
:跳过所有历史的存档。这是流程执行性能最高的配置,但是不会保存任何历史信息。 -
activity(活动)
:存档所有流程实例与活动实例。在流程实例结束时,将顶级流程实例变量的最新值复制为历史变量实例。但不会存档细节。 -
audit(审计)
:默认级别。将存档所有流程实例及活动实例,并保持变量值与提交的表单参数的同步,以保证所有通过表单进行的用户操作都可追踪、可审计。 -
full(完全)
:历史存档的最高级别,因此也最慢。这个级别存储所有audit
级别存储的信息,加上所有其他细节(主要是流程变量的更新)。
在Flowable 5.11版本以前,历史级别保存在数据库中(ACT_GE_PROPERTY
表,参数名为historyLevel
)。从5.11开始,这个值不再使用,并从数据库中忽略/删除。现在可以在引擎每次启动时切换历史级别。不会由于前一次启动时修改了级别,而导致本次启动抛出异常。
11.3. 配置异步历史
[实验性] Flowable 6.1.0引入了异步历史,使用历史作业执行器异步地进行历史数据的持久化。
1
2
3
4
5
6 <bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="asyncHistoryEnabled" value="true" />
<property name="asyncHistoryExecutorNumberOfRetries" value="10" />
<property name="asyncHistoryExecutorActivate" value="true" />
...
</bean>
配置asyncHistoryExecutorActivate
参数后,流程引擎启动时会自动启动历史作业执行器。只有在测试(或不使用异步历史时)才应该设置为false。
asyncHistoryExecutorNumberOfRetries
参数用于配置异步历史作业的重试次数。这个参数与普通的异步作业有些不同,因为历史作业可能需要更多周期才能成功完成。比如,首先需要在ACT_HI_TASK_表中创建一个历史的任务,然后才能在另一个历史作业中记录其办理人的更新。流程引擎配置中,这个参数的默认值为10。到达重试次数后,会忽略这个历史作业(且不会写入死信作业表中)。
另外,可以使用asyncHistoryExecutor
参数配置异步执行器,与普通的异步作业执行器类似。
如果不在默认的历史表中保存历史数据,而是在NoSQL数据库(Elasticsearch、MongoDb、Cassandra等)或其他什么地方保存,可以覆盖处理作业的处理器:
-
使用
historyJobHandlers
参数,配置全部自定义历史作业处理器的map -
或者,配置
customHistoryJobHandlers
列表。启动时会将列表中的所有处理器加入historyJobHandlers
map中。
另外,也可以使用消息队列,让引擎在产生新的历史作业时发送消息。这样,历史数据就可以在另外的服务器中进行处理。也可以配置引擎及消息队列使用JTA(以及JMS),这样就可以不用在作业中记录历史数据,而可以将所有数据发送至全局事务的消息队列中。
Flowable异步历史示例提供了配置异步历史的不同示例,包括默认方式、JMS队列、JTA,还有使用消息队列,并使用Spring Boot应用作为消息监听器。
11.4. 用于审计的历史
如果历史至少配置为audit
级别,则会记录由FormService.submitStartFormData(String processDefinitionId, Map<String, String> properties)
与FormService.submitTaskFormData(String taskId, Map<String, String> properties)
方法提交的所有参数。
表单参数可以像这样通过查询API读取:
1
2
3
4
5 historyService
.createHistoricDetailQuery()
.formProperties()
...
.list();
这段代码只会返回HistoricFormProperty
类型的历史详情。
如果在调用提交方法前,使用IdentityService.setAuthenticatedUserId(String)
设置了认证用户,那么也可以在历史中获取该认证用户信息。使用HistoricProcessInstance.getStartUserId()
获取启动表单的认证用户信息,任务表单则需使用HistoricActivityInstance.getAssignee()
。
12. 身份管理
从Flowable V6起,身份管理(IDM IDentity Management)组件从Flowable引擎模块中抽出,并将其逻辑移至几个不同的模块:flowable-idm-api、flowable-idm-engine、flowable-idm-spring及flowable-idm-engine-configurator。分离IDM主要是因为它并不是Flowable引擎的核心,并且在很多将Flowable引擎嵌入应用的用例中,并不使用或需要这部分身份管理逻辑。
默认情况下,IDM引擎在Flowable引擎启动时初始化并启动。这样身份管理逻辑在Flowable V5中也可以使用。IDM引擎管理自己的数据库表结构及下列实体:
-
User与UserEntity,用户信息。
-
Group与GroupEntity,组信息。
-
MembershipEntity,组中的用户成员。
-
Privilege与PrivilegeEntity,权限定义(例如在Flowable Modeler与Flowable Task应用中,用于控制应用界面的访问)。
-
PrivilegeMappingEntity,将用户及/或组与权限关联。
-
Token与TokenEntity,应用界面程序使用的认证令牌。
历史与当前进行中的流程实例都在数据库中保存历史实体,因此可以选择直接查询历史表,以减少对运行时流程实例数据的访问,并提高运行时执行的性能。
12.1. 配置IDM引擎
默认情况下,IDM引擎使用org.flowable.engine.impl.cfg.IdmEngineConfigurator
启动。这个配置器使用Flowable流程引擎配置中相同的数据源配置。这样使用身份管理就不需要进行额外的配置,就像在Flowable V5中一样。
如果Flowable引擎不需要身份管理,可以在流程引擎配置中禁用IDM引擎。
1
2
3
4 <bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="disableIdmEngine" value="true" />
...
</bean>
这意味着不能使用用户与组进行查询,也不能在任务查询中,按用户查询其所在的候选组。
默认情况下,用户的密码以明文保存在IDM数据库表中。可以在流程引擎配置中定义一个加密算法,以确保密码加密。
1
2
3
4
5
6
7
8
9
10
11 <bean id="bCryptEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<bean id="passwordEncoder" class="org.flowable.idm.spring.authentication.SpringEncoder">
<constructor-arg ref="bCryptEncoder"/>
</bean>
<bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="passwordEncoder" ref="passwordEncoder" />
...
</bean>
这里使用的是ShaPasswordEncoder,但也可以使用其他的加密算法,如org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder,不使用Spring的话也可以改用org.flowable.idm.engine.impl.authentication.ApacheDigester。
也可以覆盖默认的IDM引擎配置器,使用自定义方式初始化IDM引擎。LDAPConfigurator的实现就是一个很好的例子。它覆盖了默认的IDM引擎,使用LDAP服务代替了默认的IDM数据库表。流程引擎配置的idmProcessEngineConfigurator
参数用于设置自定义的配置器,如LDAPConfigurator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 <bean id="processEngineConfiguration" class="...SomeProcessEngineConfigurationClass">
...
<property name="idmProcessEngineConfigurator">
<bean class="org.flowable.ldap.LDAPConfigurator">
<!-- 服务器连接参数 -->
<property name="server" value="ldap://localhost" />
<property name="port" value="33389" />
<property name="user" value="uid=admin, ou=users, o=flowable" />
<property name="password" value="pass" />
<!-- 查询参数 -->
<property name="baseDn" value="o=flowable" />
<property name="queryUserByUserId" value="(&(objectClass=inetOrgPerson)(uid={0}))" />
<property name="queryUserByFullNameLike" value="(&(objectClass=inetOrgPerson)(|({0}=*{1}*)({2}=*{3}*)))" />
<property name="queryGroupsForUser" value="(&(objectClass=groupOfUniqueNames)(uniqueMember={0}))" />
<!-- 配置参数 -->
<property name="userIdAttribute" value="uid" />
<property name="userFirstNameAttribute" value="cn" />
<property name="userLastNameAttribute" value="sn" />
<property name="userEmailAttribute" value="mail" />
<property name="groupIdAttribute" value="cn" />
<property name="groupNameAttribute" value="cn" />
</bean>
</property>
</bean>
13. Eclipse Designer
Flowable提供了名为Flowable Eclipse Designer的Eclipse插件,可以用于图形化地建模、测试与部署BPMN 2.0流程。
13.1. 安装
下面的安装指导在Eclipse Mars与Neon下进行了验证。
选择Help → Install New Software。在下图面板中,点击Add按钮,并填写下列字段:
-
Name: Flowable BPMN 2.0 designer
-
Location: http://www.flowable.org/designer/update/
确保已选中"Contact all updates sites.."复选框。这样Eclipse就可以下载需要的所有插件。
13.2. Flowable Designer编辑器功能
-
创建Flowable项目与流程图(diagram)。
-
Flowable Designer在创建新的Flowable流程图时,会创建一个.bpmn文件。当使用Flowable Diagram Editor(Flowable流程图编辑器)视图打开时,将提供图形化的模型画布与画板。这个文件也可以使用XML编辑器打开,将显示流程定义的BPMN 2.0 XML元素。也就是说,Flowable Designer只用一个文件,既是流程图,也是BPMN 2.0 XML。请注意在早期版本中,不支持使用.bpmn扩展名作为流程定义的部署包。可以使用Flowable Designer的"create deployment artifacts(创建部署包)"功能,生成一个BAR文件,其中有一个.bpmn20.xml文件,包含.bpmn文件的内容。也可以方便的自己重命名。请注意,也可以使用Flowable Diagram Editor打开.bpmn20.xml文件。
-
BPMN 2.0 XML文件导入Flowable Designer会自动显示流程图。只需要将BPMN 2.0 XML文件复制到项目中,并使用Flowable Diagram Editor视图打开它。Flowable Designer使用文件中的BPMN DI信息来创建流程图。如果BPMN 2.0 XML文件中没有BPMN DI信息,会使用Flowable BPMN自动布局模块,创建流程图。
-
可以使用Flowable Designer创建BAR文件或JAR文件进行部署。在包浏览器中的Flowable项目上点击右键,在弹出菜单的下方选择Create deployment artifacts(创建部署包)选项。要了解关于Designer部署功能的更多信息,请查看部署章节。
-
生成单元测试(在包浏览器中的BPMN 2.0 XML文件上点击右键,选择generate unit test 生成单元测试)。将创建一个单元测试及运行在嵌入式H2数据库上的Flowable配置。这样就可以运行单元测试,来测试你的流程定义。
-
Flowable项目可以生成为Maven项目。要配置依赖,需要运行mvn eclipse:eclipse。请注意在流程设计时不需要Maven依赖。只在运行单元测试时才需要依赖。
13.3. Flowable Designer BPMN功能
-
支持空启动事件,错误启动事件,定时器启动事件,空结束事件,错误结束事件,顺序流,并行网关,排他网关,包容网关,事件网关,嵌入式子流程,事件子流程,调用活动,泳池,泳道,脚本任务,用户任务,服务任务,邮件任务,手动任务,业务规则任务,接收任务,定时器边界事件,错误边界事件,信号边界事件,定时器捕获事件,信号捕获事件,信号抛出事件,空抛出事件,以及四个Flowable特殊元素(用户,脚本,邮件任务与启动事件)。
-
可以在元素上悬停并选择新的任务类型,快速改变任务的类型。
-
可以在元素上悬停并选择新的元素类型,快速添加新的元素。
-
Java服务任务支持Java类,表达式或代理表达式配置。另外也可以配置字段扩展。
-
支持泳池与泳道。但由于Flowable会将不同的泳池认作不同的流程定义,因此最好只使用一个泳池。如果使用多个泳池,要小心不要在泳池间画顺序流,否则会在Flowable引擎中部署流程时发生错误。可以在一个泳池中添加任意多的泳道。
-
可以通过填写name参数,为顺序流添加标签。可以决定放置标签的位置,位置将保存为BPMN 2.0 XML DI信息的一部分。
-
支持事件子流程。
-
支持展开嵌入式子流程。也可以在一个嵌入式子流程中加入另一个嵌入式子流程。
-
支持在任务与嵌入式子流程上的定时器边界事件。然而,在Flowable Designer中,在用户任务或嵌入式子流程上使用定时器边界事件最合理。
-
支持额外的Flowable扩展,例如邮件任务,用户任务的候选人配置,或脚本任务配置。
-
支持Flowable执行与任务监听器。也可以为执行监听器添加字段扩展。
-
支持在顺序流上添加条件。
13.4. Flowable Designer部署功能
在Flowable引擎上部署流程定义与任务表单并不困难。只需要提供一个包含有流程定义BPMN 2.0 XML文件的BAR文件,与可选的用于在Flowable应用中查看的任务表单和流程图片。在Flowable Designer中,创建BAR文件十分简单。在完成流程实现后,只要在包浏览器中的Flowable项目上点击右键,在弹出菜单下方选择Create deployment artifacts(创建部署包)选项。
然后就会创建一个部署目录,包含BAR文件与可能的JAR文件。其中JAR文件包含Flowable项目中的Java类。
这样就可以在Flowable Admin应用的部署页签中,将这个文件上传至Flowable引擎。
如果项目包含Java类,部署时要多做一些工作。在这种情况下,Flowable Designer的Create deployment artifacts(创建部署包)操作也会创建包含编译后类的JAR文件。这个JAR文件必须部署在Flowable Tomcat(或其它容器)安装目录的flowable-XXX/WEB-INF/lib目录下。这样Flowable引擎的classpath就会添加这些类。
13.5. 扩展Flowable Designer
可以扩展Flowable Designer提供的默认功能。这段文档介绍了可以使用哪些扩展,如何使用,并提供了一些例子。在建模业务流程时,如果默认功能不能满足需要,需要额外的功能,或有领域专门需求的时候,扩展Flowable Designer就很有用。扩展Flowable Designer分为两个不同领域,扩展画板与扩展输出格式。两者都需要专门的方法,与不同的技术知识。
扩展Flowable Designer需要专业知识,更确切地说,Java编程的知识。取决于你想要创建的扩展类型,你可能需要熟悉Maven,Eclipse,OSGi,Eclipse扩展与SWT。 |
13.5.1. 自定义画板
可以自定义为用户建模流程提供的画板。画板是形状的集合,显示在画布的右侧,可以将形状拖放至画布中的流程图上。在默认画板中可以看到,默认形状进行了分组(被称为“抽屉 drawer”),如事件,网关,等等。Flowable Designer提供了两种选择,用于自定义画板中的抽屉与形状:
-
将你自己的形状/节点添加到已有或新建的抽屉
-
禁用Flowable Designer提供的部分或全部BPMN 2.0默认形状,除了连线与选择工具
要自定义画板,需要创建一个JAR文件,并将其加入每一个Flowable Designer的安装目录(后面介绍如何做)。这个JAR文件叫做扩展(extension)。通过编写扩展中包含的类,就能让Flowable Designer知道需要自定义的形状。为此需要实现特定的接口。Flowable提供了一个集成类库,包含这些接口以及用于扩展的基类。
可以在下列地方找到代码示例:Flowable 源码,flowable-designer
仓库下的examples/money-tasks
目录。
可以使用你喜欢的任何工具设置项目,并使用你选择的构建工具构建JAR。在下面的介绍中,假设使用Eclipse Mars或Neon,并使用Maven(3.x)作为构建工具。但任何设置都可以创建相同的结果。 |
设置扩展 (Eclipse/Maven)
下载并解压缩Eclipse(应该可以使用最新版本),与Apache Maven近期的版本(3.x)。如果使用2.x版本的Maven,可能会在构建项目时遇到错误,因此请确保版本是最新的。我们假设你已经熟悉Eclipse中的基本功能以及Java编辑器。可以使用Eclipse的Maven功能,或直接从命令行运行Maven命令。
在Eclipse中创建一个新项目。可以是通用类型项目。在项目的根路径创建一个pom.xml
文件,以包含Maven项目配置。同时创建src/main/java
与src/main/resources
目录,这是Maven约定的Java源文件与资源文件目录。打开pom.xml
文件并添加下列行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.acme</groupId>
<artifactId>money-tasks</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Acme Corporation Money Tasks</name>
...
</project>
可以看到,这只是一个基础的pom.xml文件,为项目定义了一个groupId
、artifactId
与version
。我们会创建一个定制项,包含一个money业务要用的自定义节点。
在pom.xml
文件中为项目添加这些集成库依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <dependencies>
<dependency>
<groupId>org.flowable.designer</groupId>
<artifactId>org.flowable.designer.integration</artifactId>
<version>5.22.0</version> <!-- 使用当前的Flowable Designer版本 -->
<scope>compile</scope>
</dependency>
</dependencies>
...
<repositories>
<repository>
<id>Flowable</id>
</repository>
</repositories>
最后,在pom.xml
文件中,添加maven-compiler-plugin
配置,设置Java源码级别为1.5以上(参见下面的代码片段)。要使用注解需要这个配置。也可以为Maven包含用于生成JAR的MANIFEST.MF
文件。这不是必须的,但可以在这个manifest中使用特定参数,为你的扩展提供名字(这个名字可以在设计器的特定位置显示,主要用于在设计器中有多个扩展时使用)。如果想要这么做,在pom.xml
中添加下列代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 <build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<showDeprecation>true</showDeprecation>
<showWarnings>true</showWarnings>
<optimize>true</optimize>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<archive>
<index>true</index>
<manifest>
<addClasspath>false</addClasspath>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<FlowableDesigner-Extension-Name>Acme Money</FlowableDesigner-Extension-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
扩展的名字使用FlowableDesigner-Extension-Name
参数描述。现在只剩下让Eclipse按照pom.xml
的指导设置项目。因此打开命令行,并转到Eclipse工作空间中你项目的根目录。然后执行下列Maven命令:
mvn eclipse:eclipse
等待构建完成。刷新项目(使用项目上下文菜单(右键点击),并选择Refresh 刷新
)。现在Eclipse项目中应该已经建立了src/main/java
与src/main/resources
源码目录。
当然也可以使用m2eclipse插件,并简单地在项目的上下文菜单(右键点击)中启用Maven依赖管理。然后在项目的上下文菜单中选择 |
这就完成了配置。现在可以开始为Flowable Designer创建自定义项了!
在Flowable Designer中应用你的扩展
你也许想知道如何将你的扩展加入Flowable Designer,以便应用你的自定义项。需要这些步骤:
-
创建扩展JAR(例如,使用Maven构建时,在项目中运行mvn install)后,需要将扩展传递至Flowable Designer安装的计算机;
-
将扩展存储在硬盘上,方便记忆的位置。请注意:必须保存在Flowable Designer的Eclipse工作空间之外——将扩展保存在工作空间内,会导致弹出错误消息弹框,扩展将不可用;
-
启动Flowable Designer,从菜单中,选择
Window
>Preferences
,或Eclipse
>Preferences
-
在Preferences界面,键入
user
作为关键字。将可以看到在Eclipse中Java
段落内,User Libraries
的选项。
-
选择
User Libraries
选项,将在右侧显示树形界面,可以添加库。应该可以看到一个默认组,可以用于添加Flowable Designer的扩展(根据Eclipse安装不同,也可能看到几个其他的)。
-
选择
Flowable Designer Extensions
组,并点击Add JARs…
或Add External JARs…
按钮。跳转至存储扩展的目录,并选择希望添加的扩展文件。完成后,配置界面会将扩展作为Flowable Designer Extensions
组的成员进行显示,像下面这样。
-
点击
OK
按钮保存并关闭配置对话框。Flowable Designer Extensions
会自动添加至你创建的新Flowable项目。可以在导航条或包管理器的项目树下的用户库条目中看到。如果工作空间中已经有了Flowable项目,也可以看到组中显示了新扩展,像下面这样。
打开的流程图将在其画板上显示新扩展的图形(或者禁用部分图形,取决于扩展中的配置)。如果已经打开了流程图,关闭并重新打开就能在画板上看到变化。
为画板添加图形
项目配置完后,可以很轻松的为画板添加图形。每个添加的图形都表现为JAR中的一个类。请注意这些类并不是Flowable引擎运行时会使用的类。在扩展中可以为每个图形描述Flowable Designer可用的参数。在这些图形中,也可以定义运行时特性,并将由引擎在流程实例到达该节点时使用。运行时特性可以使用任何Flowable对普通ServiceTask
支持的选项。查看这个章节了解更多信息。
图形的类是简单的Java类,加上一些注解。这个类需要实现CustomServiceTask
接口,但不应该直接实现这个接口,而应该扩展AbstractCustomServiceTask
基类(目前必须直接扩展这个类,而不能在中间使用abstract类)。在这个类的Javadoc中,可以看到其默认提供的,与需要覆盖的方法介绍。覆盖可以实现很多功能,例如为画板及画布中的图形提供图标(两个可以不一样),或者指定你希望节点实现的基图形(活动,时间,网关)。
1
2
3
4
5
6
7
8 /**
* @author John Doe
* @version 1
* @since 1.0.0
*/
public class AcmeMoneyTask extends AbstractCustomServiceTask {
...
}
需要实现getName()
方法,来决定节点在画板上的名字。也可以将节点放在自己的抽屉中,并提供图标,只需要覆盖AbstractCustomServiceTask
的对应方法就可以。如果希望提供图标,请确保放在JAR的src/main/resources
包中,需要是16X16像素的JPEG或PNG格式图片。你要提供的路径是到这个目录的相对路径。
可以通过在类中添加成员,并使用@Property
注解,来为形状添加参数。像这样:
1
2
3 @Property(type = PropertyType.TEXT, displayName = "Account Number")
@Help(displayHelpShort = "提供一个账户编码 Provide an account number", displayHelpLong = HELP_ACCOUNT_NUMBER_LONG)
private String accountNumber;
可以使用多种PropertyType
值,在这个章节中详细描述。可以通过将required属性设置为true,将一个字段设为必填。如果用户没有填写这个字段,将会提示消息,背景也会变红。
如果想要调整类中多个参数在参数界面上的显示顺序,需要指定@Property
注解的order属性。
可以看到有个@Help
注解,它用于为用户提供一些填写字段的指导。也可以在类本身上使用@Help
注解——这个信息将在显示给用户的参数表格最上面显示。
下面是MoneyTask
详细介绍的列表。添加了一个备注字