Activiti 6 流程引擎使用示例(基础篇)
# Activiti 6 流程引擎使用示例(基础篇)
Activiti (opens new window) 是著名的、轻量级的、基于 Java 语言开发的开源工作流(Business Process Model and Notation, BPMN)引擎,可广泛用于流程自动化场景,特别是办公自动化(Office Automation, OA)系统中的审批,或类似需要用到流程的企业管理软件中。
Activiti Cloud 是新一代基于云端分布式的流程引擎,目前尚在密集开发过程中。本文将在旧版 v6.0.0 的基础上,讲述 Activiti 流程引擎的使用。
注: 本文还将不定期更新,添加新的内容。囿于个人的知识和实践经验有限,难免会有错误,请读者批判性地阅读,并以官方的用户文档为准。
旧版 Activiti 6.0.0 发布于 2017年5月26日,至今已有3年多无更新发布。因项目历史的原因,我还将在旧版的 Activiti 6.x 上延续开发。
参考资料:
- GitHub - Activiti 6.0.0 (opens new window),这里有发行版二进制包,源代码。
- Activiti v6.0.0 User Guide (opens new window),官方用户文档,此处还有介绍、安装、Getting Started、配置、API文档等详细信息。
- 《Activiti 实战》,闫洪磊,机械工业出版社,2014.12,ISBN: 978-7-111-48595-7
# 编译
我们可以从源代码手动编译。
克隆分支到本地:
# clone the source code. git.exe clone --branch 6.x -v "https://github.com/Activiti/Activiti.git"
注:如果网速不够快导致克隆失败,也可以选择从 GitHub 直接下载源代码压缩包。
我用的是 Oracle JDK 8,从源代码编译,安装到 Maven 的本地仓库:
# build the release packages and install to local Maven repository. mvn install -Dmaven.test.skip=true
# 安装 Demo
下面在 Windows 10 电脑上安装 Activiti 的 Demo 应用程序。
我本机上安装了 Apache Tomcat (可以使用8或者9)。将 3 个“.war”文件 activiti-app.war、activiti-admin.war 和 activiti-rest.war (它们位于二进制发行版的wars
文件夹内)复制到 Tomcat 的 webapps 文件夹里。Tomcat 运行时会自动将它们解包部署,就可以访问 Activiti 的 这2个 Web 应用程序了(注:activiti-rest 是用于REST服务,不需要用浏览器访问)。
activity-app http://localhost:8080/activiti-app/ 默认的登录账号: admin / test
activity-admin http://localhost:8080/activiti-admin/ 默认的登录账号: admin / admin
这些 Web 应用程序默认使用 H2 内存数据库,方便运行但是不能持久化数据(重启后数据会丢失,回到最初的状态)。我本机上已经安装了 MySQL Community Server 5.7,并作为服务启动。因此,可以将这些 Web 应用程序的后台数据库从 H2 改到 MySQL 上。
先将 MySQL 的驱动下载 (opens new window)到本地,将最新的版本 (这里是 mysql-connector-java-5.1.48.jar
),复制到 Tomcat 的 lib 文件夹内。然后按下面修改配置:
1. 修改 activiti-app 的数据库连接
在 MySQL 里,创建数据库( activiti6ui )和用户( activitiuser / ActivitiPasswd1+ ):
-- create a database.
create database activiti6ui CHARACTER SET utf8 COLLATE utf8_bin;
-- create a user and grant permission.
grant usage on *.* to activitiuser@localhost identified by 'ActivitiPasswd1+';
grant all privileges on activiti6ui.* to activitiuser@localhost;
修改配置文件 activiti-app\WEB-INF\classes\META-INF\activiti-app\activiti-app.properties
,将其中 H2 数据库的配置改到 MySQL 上。
datasource.url=jdbc:mysql://127.0.0.1:3306/activitiadmin?characterEncoding=UTF-8&useSSL=false
datasource.username=activitiuser
datasource.password=ActivitiPasswd1+
hibernate.dialect=org.hibernate.dialect.MySQLDialect
注:此 “activiti-app” Web应用的“WEB-INF/lib”文件夹中,已经内置了 MySQL 的 JDBC 驱动,是个旧版本: mysql-connector-java-5.1.30.jar , 可以考虑把它删除。而下面的 “activiti-admin” 和 “activiti-rest ” 应用中没有发现内置了 MySQL 的驱动。
2. 修改 activiti-admin 的数据库连接
在 MySQL 里,创建数据库( activitiadmin )和用户( activitiuser / ActivitiPasswd1+ ):
-- create a database.
create database activitiadmin CHARACTER SET utf8 COLLATE utf8_bin;
-- create a user and grant permission.
grant usage on *.* to activitiuser@localhost identified by 'ActivitiPasswd1+';
grant all privileges on activitiadmin.* to activitiuser@localhost;
修改这个配置文件,将其中 H2 数据库的连接改到 MySQL 上:
activiti-admin\WEB-INF\classes\META-INF\activiti-admin\activiti-admin.properties
同时,将 rest.app.port=9999
修改为 rest.app.port=8080
。
datasource.driver=com.mysql.jdbc.Driver
datasource.url=jdbc:mysql://127.0.0.1:3306/activitiadmin?characterEncoding=UTF-8&useSSL=false
datasource.username=activitiuser
datasource.password=ActivitiPasswd1+
hibernate.dialect=org.hibernate.dialect.MySQLDialect
rest.app.port=8080
3. 修改 activiti-rest 的数据库连接
此 "activiti-rest" 应用程序可以指向"activiti-app"应用的的数据库(即 activiti6ui ),它把里面的流程资源用 REST API 的方式对外提供服务。修改这个配置文件:activiti-rest\WEB-INF\classes\db.properties
修改完成,重新启动 Tomcat 生效。
db=mysql
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/activiti6ui?characterEncoding=UTF-8&useSSL=false
jdbc.username=activitiuser
jdbc.password=ActivitiPasswd1+
关于数据库的说明
当Web 应用程序 activiti-app 首次启动时,它会自动初始化,创建数据库内所需的表,其中包括此 UI 程序自己使用的一些表,还包括 Activiti 引擎使用的多个表。因此,我们可以把此数据库(即 activiti6ui )当作生产环境,通过 activiti-app 在里面部署用于生产的正式流程。
如果我们通过 Java 来调用Activiti Engine 的API (例如流程开发),则它首次启动时,会自动初始化。指向的数据库将会建立 Activiti 引擎使用的多个表。具体做法是,我们可以在流程开发或测试时,选择用 H2 或者 MySQL 来作为数据库,其中 H2 用作内存数据库,不保存任何变更;在 MySQL 中新建立一个数据库实例(例如下面,使用的是 mybpmndb1),它可以保存变更。
# 集成开发环境
官方用户手册里推荐 (opens new window)使用 eclipse 作为集成开发环境(IDE)。eclipse 有多个可下载的包(按使用目的集成了相关插件),推荐下载使用“Eclipse IDE fro Enterprise Java Developers”。
要求在 eclipse 里安装一个 “Activiti BPMN 2.0 designer” 插件 (opens new window),用于流程的编辑:
打开菜单“Help | Install New Software”,在里面填入下列信息,按 “Add” 按钮即可安装。
- Name: Activiti BPMN 2.0 designer
- Location: http://activiti.org/designer/update/
# HelloWorld
学任何一项开发技术,都有一个“HelloWorld”例子。本文也将从最简单的流程开始,让读者先领略一下 Activiti 的 API,对它有一个初步的认识。这里的示例都是最简化的,都不是工程上可用的流程,其目的只是用来展示 Activiti 的某一个局部特性。
# 最简单的流程
我们从先最简单的一个流程 MyProcess.bpmn
开始。此流程在 eclipse 可视化流程编辑器里定义如下图:
流程开始后,第一个任务usertask1
是“请假申请”。
接下来第二个任务 usertask2
是“经理审批”。再往后,流程结束。
我们可以在 eclipse 里设置“Create process definition image when saving the diagram”选项,如下图:
让 eclipse 在保存流程图定义 MyProcess.bpmn
的时候,可视化编辑器会自动创建 MyProcess.png
图片文件,此图片文件的内容就是此流程图。在 eclipse 的 Project Explorer 里可以看到,此图片文件和流程定义文件在同一个文件夹,通常都是在资源文件夹里,方便 API 以资源文件的方式载入并部署。如下图:
此示例包括如下2个源文件:
- 流程定义:
MyProcess.bpmn
- Java 程序:
MyProcessApp.java
其中流程定义文件 MyProcess.bpmn
的内容如下:
<?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:activiti="http://activiti.org/bpmn" 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" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="myProcess" name="My process" isExecutable="true">
<startEvent id="startevent1" name="Start"></startEvent>
<userTask id="usertask1" name="请假申请"></userTask>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
<userTask id="usertask2" name="经理审批"></userTask>
<sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
<sequenceFlow id="flow3" sourceRef="usertask2" targetRef="endevent1"></sequenceFlow>
<endEvent id="endevent1" name="End"></endEvent>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_myProcess">
<bpmndi:BPMNPlane bpmnElement="myProcess" id="BPMNPlane_myProcess">
<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
<omgdc:Bounds height="35.0" width="35.0" x="20.0" y="40.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
<omgdc:Bounds height="55.0" width="105.0" x="140.0" y="30.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
<omgdc:Bounds height="55.0" width="105.0" x="320.0" y="30.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
<omgdc:Bounds height="35.0" width="35.0" x="510.0" y="40.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="55.0" y="57.0"></omgdi:waypoint>
<omgdi:waypoint x="140.0" y="57.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
<omgdi:waypoint x="245.0" y="57.0"></omgdi:waypoint>
<omgdi:waypoint x="320.0" y="57.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
<omgdi:waypoint x="425.0" y="57.0"></omgdi:waypoint>
<omgdi:waypoint x="510.0" y="57.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
Java 程序 MyProcessApp.java
的内容如下:
package mybpm.activiti.bpmn.example.helloworld;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.DeploymentBuilder;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import mybpm.activiti.common.MyActivitiUtils;
/**
* Hello world process!
*/
public class MyProcessApp {
public static void testMyProcess() {
// ==> 创建流程引擎,有2种方式。
// method 1. using a stand-alone in memory h2 engine.
//ProcessEngineConfiguration procEngConf = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration();
//procEngConf.setProcessEngineName("testProcessEngine");
//ProcessEngine procEngine = procEngConf.buildProcessEngine();
//System.out.println("ProcessEngine Name: " + procEngine.getName());
// method 2. through configuration file: activiti.cfg.xml.
ProcessEngine procEngine = ProcessEngines.getDefaultProcessEngine();
// ==> 得到流程存储服务组件
RepositoryService repoService = procEngine.getRepositoryService();
// ==> 部署流程文件
DeploymentBuilder depBuilder = repoService.createDeployment();
depBuilder.addClasspathResource("bpmn/example/helloworld/MyProcess.bpmn");
depBuilder.addClasspathResource("bpmn/example/helloworld/MyProcess.png");
depBuilder.deploy();
// ==> 检查一下刚刚部署的流程
ProcessDefinition procDef = repoService.createProcessDefinitionQuery().singleResult();
MyActivitiUtils.debugPrintProcessDefinition(procDef);
// In output: "Key":"myProcess"
// ==> 得到运行时服务组件
RuntimeService rtService = procEngine.getRuntimeService();
// ==> 启动流程
ProcessInstance procInst = rtService.startProcessInstanceByKey("myProcess");
MyActivitiUtils.debugPrintProcessInstance(procInst);
// In output: "ProcessDefinitionKey":"myProcess","ID":"5","ProcessInstanceId":"5"
// ==> 获取流程任务组件
TaskService taskService = procEngine.getTaskService();
// ==> 查询第一个任务
Task task1 = taskService.createTaskQuery().singleResult();
System.out.println("第一个任务完成前,当前任务名称:" + task1.getName());
// ==> 完成第一个任务
taskService.complete(task1.getId());
// ==> 查询第二个任务
Task task2 = taskService.createTaskQuery().singleResult();
System.out.println("第二个任务完成前,当前任务名称:" + task2.getName());
// ==> 完成第二个任务(流程结束)
taskService.complete(task2.getId());
// ==> 后续不再有任务了
Task task3 = taskService.createTaskQuery().singleResult();
if (task3 == null) {
System.out.println("流程结束,没有后续任务了。");
} else {
System.out.println("流程未结束,查找到任务:" + task3);
}
}
public static void main(String[] args) {
testMyProcess();
}
}
在上面的 Java 代码中,我们可以看到创建流程引擎(ProcessEngine)有2种方式:
默认方式。全部用默认的设置,数据将存储在内存数据库 [H2 Database Engine](H2 Database Engine) 中。这种方式最简单,不需要连接外部数据库服务器,不需要做任何配置。但数据保存在内存中,意味着程序终止后,数据将会丢失。创建流程引擎的代码如下:
// method 1. using a stand-alone in memory h2 engine. ProcessEngineConfiguration procEngConf = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration(); ProcessEngine procEngine = procEngConf.buildProcessEngine();
用配置文件
activiti.cfg.xml
。这种方式是使用 Activiti 流程引擎的推荐方式。此配置文件要放在资源路径下,它将在 Activiti 初始化时读入(实际上是 Spring 读入并初始化),以生成流程引擎对象。配置文件中包括各种属性、 Java Bean、数据库连接信息等。这里可以配置使用 H2 内存数据库,也可以配置使用外部数据库如 MySQL,从而让流程运行的中间数据能够被存储下来。创建流程引擎的代码如下:// method 2. through configuration file: activiti.cfg.xml. ProcessEngine procEngine = ProcessEngines.getDefaultProcessEngine();
例如,一个使用 H2 内存数据库的
activiti.cfg.xml
配置文件内容如下:<?xml version="1.0" encoding="UTF-8"?> <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 --> <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration"> <!-- DB: H2 --> <property name="jdbcUrl" value="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=-1" /> <property name="jdbcDriver" value="org.h2.Driver" /> <property name="jdbcUsername" value="sa" /> <property name="jdbcPassword" value="" /> <!-- DB: MySQL --> <!-- <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybpmndb1?useSSL=false" /> <property name="jdbcDriver" value="com.mysql.jdbc.Driver" /> <property name="jdbcUsername" value="activitiuser" /> <property name="jdbcPassword" value="ActivitiPasswd1+" /> --> <!-- 设置流程引擎启动和关闭时数据库执行的策略 false: Activiti在启动时,会对比数据库表中保存的版本,如果没有表或者版本不匹配,将在启动时抛出异常。 true: Activiti会对数据库中所有的表进行更新,如果表不存在,则Activiti会自动创建。 --> <property name="databaseSchemaUpdate" value="true" /> <property name="history" value="full"/> </bean> </beans>
程序运行后,我们可以看到它最后打印输出“流程结束,没有后续任务了。”,流程的运行结果符合预期,正常结束。
通过这个例子,我们可以了解到 Java 程序里面怎样使用 Activiti API,以及在 eclipse 集成开发环境中,怎样进行流程开发。
# 脚本任务
脚本任务(Script Task)是可以在里面自定义“脚本”的一类任务,Activiti 目前支持两种:
- Groovy Script Task,支持以 Groovy 语言来自定义执行脚本。
- JS Script Task,支持以 JavaScript 语言来自定义执行脚本。
其中,“JS Script Task”并不需要做额外的配置就可以支持。而“Groovy Script Task”需要在项目的 Maven 构建文件pom.xml
中添加 Groovy 的依赖:
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.2</version>
<type>pom</type>
</dependency>
下面我们设计了一个简单的流程 SayHelloToLeave.bpmn
,目的是为了演示这两种脚本任务,看它能做什么。此流程在 eclipse 可视化流程编辑器里定义如图:
经“领导审批”用户任务后,我们希望在“Groovy Script Task”里面,用 Groovy 脚本输出一些流程的信息。您可以看到,在 Script 对应的输入框里面,它定义了一个简单的输出脚本:
out:println "【groovy script task】applyUser:" + applyUser + " ,days:" + days + ", approval:" + approved;
然后在接下来的“JS Script Task”里面,也同样用 JS 脚本输出一些流程的信息。您可以看到,在 Script 对应的输入框里面,它定义了一个类似的简单脚本:
var my = "【JS script task】 applyUser:" + applyUser + " ,days:" + days + ", approval:" + approved;
print(my);
这里的输出都是打印在终端上。在运行的时候,终端会打印出很多的 Activiti 运行时调试信息,为了防止找不到输出的文字,特意在输出前面加了一个“【xx script task】”字样,便于识别。
此示例包括如下2个源文件:
- 流程定义:
SayHelloToLeave.bpmn
- Java 程序:
SayHelloToLeaveTest.java
其中流程定义文件 SayHelloToLeave.bpmn
的内容如下:
<?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:activiti="http://activiti.org/bpmn" 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" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="SayHelloToLeave" name="SayHelloToLeave" isExecutable="true">
<startEvent id="startevent1" name="Start"></startEvent>
<userTask id="usertask1" name="领导审批" activiti:candidateGroups="deptLeader"></userTask>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
<scriptTask id="outputAuditResult" name="Groovy Script Task" scriptFormat="groovy" activiti:autoStoreVariables="false">
<script>out:println "【groovy script task】applyUser:" + applyUser + " ,days:" + days + ", approval:" + approved;</script>
</scriptTask>
<sequenceFlow id="flow3" sourceRef="usertask1" targetRef="outputAuditResult"></sequenceFlow>
<sequenceFlow id="flow4" sourceRef="outputAuditResult" targetRef="scripttask1"></sequenceFlow>
<scriptTask id="scripttask1" name="JS Script Task" scriptFormat="javascript" activiti:autoStoreVariables="false">
<script>var my = "【JS script task】 applyUser:" + applyUser + " ,days:" + days + ", approval:" + approved;
print(my);</script>
</scriptTask>
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow5" sourceRef="scripttask1" targetRef="endevent1"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_SayHelloToLeave">
<bpmndi:BPMNPlane bpmnElement="SayHelloToLeave" id="BPMNPlane_SayHelloToLeave">
<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
<omgdc:Bounds height="35.0" width="35.0" x="30.0" y="42.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
<omgdc:Bounds height="55.0" width="105.0" x="110.0" y="32.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="outputAuditResult" id="BPMNShape_outputAuditResult">
<omgdc:Bounds height="55.0" width="105.0" x="260.0" y="32.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="scripttask1" id="BPMNShape_scripttask1">
<omgdc:Bounds height="55.0" width="105.0" x="420.0" y="32.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
<omgdc:Bounds height="35.0" width="35.0" x="570.0" y="42.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="65.0" y="59.0"></omgdi:waypoint>
<omgdi:waypoint x="110.0" y="59.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
<omgdi:waypoint x="215.0" y="59.0"></omgdi:waypoint>
<omgdi:waypoint x="260.0" y="59.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
<omgdi:waypoint x="365.0" y="59.0"></omgdi:waypoint>
<omgdi:waypoint x="420.0" y="59.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
<omgdi:waypoint x="525.0" y="59.0"></omgdi:waypoint>
<omgdi:waypoint x="570.0" y="59.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
Java 单元测试类 SayHelloToLeaveTest.java
的内容如下:
package mybpm.activiti.bpmn.example.helloworld;
import java.util.HashMap;
import java.util.Map;
import org.activiti.engine.HistoryService;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.Assert;
import org.junit.Test;
/**
* Check the console log output for "Groovy Script Task" and "JS Script Task".
*/
public class SayHelloToLeaveTest {
@Test
public void testStartProcess() throws Exception {
// ==> 创建流程引擎
// using a stand-alone in memory h2 engine (data will get lost when exit).
ProcessEngine processEngine = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration()
.buildProcessEngine();
// ==> 得到流程存储服务组件
RepositoryService repositoryService = processEngine.getRepositoryService();
// ==> 部署流程文件
final String bpmnFileName = "bpmn/example/helloworld/SayHelloToLeave.bpmn";
repositoryService.createDeployment().addInputStream("SayHelloToLeave.bpmn",
this.getClass().getClassLoader().getResourceAsStream(bpmnFileName)).deploy();
// ==> 检查一下刚刚部署的流程
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
Assert.assertEquals("SayHelloToLeave", processDefinition.getKey());
// ==> 得到运行时服务组件
RuntimeService runtimeService = processEngine.getRuntimeService();
// ==> 创建一个用于启动流程的字典,里面包含了流程需要的2个变量
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("applyUser", "employee1");
variables.put("days", 3);
// ==> 启动流程
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("SayHelloToLeave", variables);
Assert.assertNotNull(processInstance);
System.out.println("[Java sysout] pid=" + processInstance.getId() + ", pdid=" + processInstance.getProcessDefinitionId());
// ==> 获取流程任务组件
TaskService taskService = processEngine.getTaskService();
// ==> 来到第一个任务
Task taskOfDeptLeader = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
Assert.assertNotNull(taskOfDeptLeader);
Assert.assertEquals("领导审批", taskOfDeptLeader.getName());
// ==> 完成第一个任务
taskService.claim(taskOfDeptLeader.getId(), "leaderUser");
variables = new HashMap<String, Object>();
variables.put("approved", true);
taskService.complete(taskOfDeptLeader.getId(), variables);
// ==> 已经没有用户任务了(后面的 Script Task 是自动执行的,它已经走到了流程结束)
taskOfDeptLeader = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
Assert.assertNull(taskOfDeptLeader);
// ==> 流程结束后,查看历史状态
HistoryService historyService = processEngine.getHistoryService();
long count = historyService.createHistoricProcessInstanceQuery().finished().count();
Assert.assertEquals(1, count);
}
}
以 Junit 方式运行上述单元测试,从控制台得到类似下面的输出:
...
【groovy script task】applyUser:employee1 ,days:3, approval:true
...
【JS script task】 applyUser:employee1 ,days:3, approval:true
它验证了流程中的2个脚本任务可以顺利执行。
# 部署流程
Activiti 的流程定义文件以“.bpmn”为文件扩展名,还包括可选的一个“.png”图片文件。此“.bpmn”实际上就是 XML 文件,只是修改了扩展名而已。
有多种流程的部署方式:
- 流程文件存储在资源文件夹中,通过 Classpass 加载。
- 流程文件存储在某个文件夹中,通过 InputStream 加载。
- 流程的内容存储在字符串中,通过字符串加载。
- 流程文件(可多个)存储在某个 zip 压缩包(.bar)中,通过压缩包加载。
下面我们假定已经有了 repositoryService 对象。
先看第1种方式,也是较为常用的一种方式:
// 定义classpath
final String bpmnClasspath = "bpmn/example/identity/CandidateUserInUserTask.bpmn";
final String pngClasspath = "bpmn/example/identity/CandidateUserInUserTask.png";
// 创建部署构建器
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
// 添加资源文件
deploymentBuilder.addClasspathResource(bpmnClasspath);
deploymentBuilder.addClasspathResource(pngClasspath);
// 执行部署
deploymentBuilder.deploy();
第2种方式,用 InputStream 方式加载部署:
final String BPMN_RES = "bpmn/example/identity/UserAndGroupInUserTask.bpmn";
// 创建部署构建器
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
// 添加InputStream,它来自流程定义的文件流
InputStream ins = getClass().getClassLoader().getResourceAsStream(BPMN_RES);
deploymentBuilder.addInputStream("userAndGroupInUserTask.bpmn", ins);
// 执行部署
deploymentBuilder.deploy();
第3种情况,流程定义存储在字符串中,例如有这样一个流程定义:
// XML字符串定义的流程
private String text = "<?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:activiti=\"http://activiti.org/bpmn\" 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\" typeLanguage=\"http://www.w3.org/2001/XMLSchema\" expressionLanguage=\"http://www.w3.org/1999/XPath\" targetNamespace=\"http://www.kafeitu.me/activiti-in-action\">"
+ " <process id=\"candidateUserInUserTask\" name=\"candidateUserInUserTask\">"
+ " <startEvent id=\"startevent1\" name=\"Start\"></startEvent>"
+ " <userTask id=\"usertask1\" name=\"用户任务包含多个直接候选人\" activiti:candidateUsers=\"jackchen, henryyan\"></userTask>"
+ " <sequenceFlow id=\"flow1\" name=\"\" sourceRef=\"startevent1\" targetRef=\"usertask1\"></sequenceFlow>"
+ " <endEvent id=\"endevent1\" name=\"End\"></endEvent>"
+ " <sequenceFlow id=\"flow2\" name=\"\" sourceRef=\"usertask1\" targetRef=\"endevent1\"></sequenceFlow>"
+ " </process>"
+ "</definitions>";
Activiti 的 API 支持串起来的编写方式,例如,下面的代码将部署过程串起来,看起来相对简短些。例如:
// 以candidateUserInUserTask.bpmn为资源名称,以text的内容为流程定义加载
repositoryService.createDeployment().addString("CandidateUserInUserTask.bpmn", text).deploy();
第4种情况。将多个流程的定义打一个 zip 压缩包,修改文件扩展名为“.bar”。猜想“bar”应该是指的“BPMN Archive”的意思。
final String BAR_RES = "bpmn/example/identity/zip_archived_process.bar";
// 从classpath读取资源文件并部署
InputStream ins = getClass().getClassLoader().getResourceAsStream(BAR_RES);
repositoryService.createDeployment().addZipInputStream(new ZipInputStream(ins)).deploy();
# 表达式
表达式(Expression)是体现 Activiti 灵活性的一个功能。我们可以在流程定义中,通过表达式来触发自定义的 Java 类方法,从而方便实现定制化的功能。
下面设计一个简单的流程,来验证表达式的基本使用。此流程的设计如下图:
第1个表达式任务“获取流程启动人”:
接下来,第2个表达式任务“计算表达式”:
第3个表达式任务“Execution变量”:
第4个用户任务“在用户任务上添加表达式”:
流程中使用到了一个 名为“MyBean”的 Java Bean,这个类里面定义了一些方法,在各个流程任务中通过设置表达式(Expression)来触发这些方法。其代码如下:
MyBean.java
class MyBean implements Serializable {
/** Generated serial version UID. */
private static final long serialVersionUID = 8498995687344427652L;
//在第1个任务“获取流程启动人”中被触发
public void print() {
System.out.println("print content by print()");
}
//在第2个任务“计算表达式”中被触发
public String print(String myName) {
System.out.println("print content by print(String myName), value is :" + myName);
return myName += ", added by print(String myName)";
}
//在第3个任务“Execution变量”中被触发
public String printBkey(DelegateExecution execution) {
String processInstanceBusinessKey = execution.getProcessInstanceBusinessKey();
System.out.println("process instance id: " + execution.getProcessInstanceId() + ", business key: " + processInstanceBusinessKey);
return processInstanceBusinessKey;
}
//在第4个任务“在用户任务上添加表达式”中被触发
public void invokeTask(DelegateTask task) {
task.setVariable("setByTask", "I'm setted by DelegateTask, " + task.getVariable("myName"));
}
}
流程中表达式的使用:
在第一个“获取流程启动人”任务里通过设置“Expression”为
${authenticatedUserId}
来触发此表达式,并将表达式的返回值保存到了authenticatedUserIdForTest
流程变量中。authenticatedUserId
变量和它的值“henryyan”,是在启动流程时通过identityService.setAuthenticatedUserId("henryyan")
设置进入到了流程变量字典中。在单元测试中,将对authenticatedUserIdForTest
进行验证,它应该等于authenticatedUserId
变量的值。在第二个“计算表达式”任务里通过设置“Expression”为
${myBean.print(myName)}
来触发print()
方法,并将表达式的返回值放到了returnValue
流程变量中。在第三个“Execution变量”任务里通过设置“Expression”为
${myBean.printBkey(execution)}
来触发printBkey()
方法。并将表达式的返回值放到了businessKey
流程变量中。在第四个“在用户任务上添加表达式”用户任务里,还将 MyBeam 设置为监听器,当此用户任务被创建时(Event: create)将触发设定的
${myBean.invokeTask(task)}
表达式,触发调用了invokeTask()
方法。此方法的代码中,将返回值设置到了
setByTask
流程变量中。public void invokeTask(DelegateTask task) { task.setVariable("setByTask", "I'm setted by DelegateTask, " + task.getVariable("myName")); }
对应的单元测试代码(节选)。 ExpressionTest.java
MyBean myBean = new MyBean();
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("myBean", myBean);
String myName = "亨利 严";
variables.put("myName", myName);
// 启动流程
identityService.setAuthenticatedUserId("henryyan");
final String strBusinessKey = "9999"; // 业务ID
ProcessInstance procInst = runtimeService.startProcessInstanceByKey(PROC_DEF_KEY, strBusinessKey, variables);
//检查第1个任务“获取流程启动人”的表达式输出结果
Assert.assertEquals("henryyan", runtimeService.getVariable(procInst.getId(), "authenticatedUserIdForTest"));
//检查第2个任务“计算表达式”的表达式输出结果
Assert.assertEquals("亨利 严, added by print(String myName)", runtimeService.getVariable(procInst.getId(), "returnValue"));
//检查第3个任务“Execution变量”的表达式输出结果
Assert.assertEquals(strBusinessKey, runtimeService.getVariable(procInst.getId(), "businessKey"));
//检查第4个任务“在用户任务上添加表达式”设置的变量
Task task = taskService.createTaskQuery().processInstanceId(procInst.getId()).singleResult();
String setByTask = (String) taskService.getVariable(task.getId(), "setByTask");
Assert.assertEquals("I'm setted by DelegateTask, " + myName, setByTask);
通过本例,我们看到了通过与 Java Bean 的搭配,使用表达式的多种方式。
# 监听器
# 执行监听器
执行监听器(Execution Listener)应用于流程,用来监听流程事件并执行相应的动作。执行监听器可以用来监听以下几种事件:
- 流程实例的启动、结束
- 输出流
- 活动启动、结束
- 路由开始、结束
- 中间事件开始、结束
- 触发开始事件、触发结束事件
执行监听器的 event 属性指定监听事件的类型,它分为三类:start、end、take;我们还需要选择监听器的类型,在这几种中任选其一: Java class、Expression、Delegate expression、Alfresco execution script、Alfresco task script。
例如下面的流程种,在流程的“Execution listeners”里定义了2个执行监听器:
其中第1个执行监听器,在流程启动时(start)被触发:
它是一个 Java class 类型的监听器,必须实现执行监听器接口(ExecutionListener),重载notify()
方法。代码如下:
package mybpm.activiti.bpmn.example.listener;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.ExecutionListener;
/**
* 流程启动监听器
*/
public class ProcessStartExecutionListener implements ExecutionListener {
/** Generated serial version UID. */
private static final long serialVersionUID = 270356408585775460L;
@Override
public void notify(DelegateExecution execution) {
execution.setVariable("setInStartListener", true);
System.out.println(this.getClass().getSimpleName() + ", " + execution.getEventName());
}
}
我们在代码中可以看到,在流程启动时,此监听器将被触发,它的执行逻辑是:设置一个流程变量setInStartListener
为 true,并且在终端打印出一行信息。
其中第2个执行监听器,在流程结束时(end)被触发:
它是一个“Delegate expression”类型的监听器。设定的表达式是:${endListener}
。此endListener
变量,是在流程启动的时候设置到流程变量中的,由它来指定具体的监听实现类。见下面的代码:
private static final String PROC_DEF_KEY = "listener";
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("endListener", new ProcessEndExecutionListener());
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(PROC_DEF_KEY, variables);
代码中指定用 ProcessEndExecutionListener
对象来作为监听实现类。当然,具体监听实现类还可以在引擎中配置或者由 Spring 代理。
此执行监听器的实现代码如下:
package mybpm.activiti.bpmn.example.listener;
import java.io.Serializable;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.ExecutionListener;
/**
* 流程结束监听器
*/
public class ProcessEndExecutionListener implements ExecutionListener, Serializable {
/** Generated serial version UID. */
private static final long serialVersionUID = -1380307719667706030L;
@Override
public void notify(DelegateExecution execution) {
execution.setVariable("setInEndListener", true);
System.out.println(this.getClass().getSimpleName() + ", " + execution.getEventName());
}
}
它会在被触发时,设置一个流程变量setInEndListener
值为 true,并且在终端打印出一行信息。我们可以通过观察控制台的输出,来验证它能正常工作。
# 任务监听器
任务监听器(Task Listener)应用于用户任务,用来监听任务事件并执行相应的动作。任务监听器可以用来监听以下3种事件:
- create: 在任务被创建且所有任务属性设置完成之后触发。
- assignment:在任务被分配给办理人之后触发。注意,assignment 事件总是在 create 事件触发之前被触发,因为任务的办理人是一个属性,create 事件需要逐一处理任务的办理人、候选人、候选组等属性。
- complete:在任务完成时触发。也即运行时任务数据被删除之前(Activiti 的数据分为运行时和历史两种类别,当运行时数据处理完毕后将被清理)触发。
我们还需要选择监听器的类型,在这几种中任选其一: Java class、Expression、Delegate expression、Alfresco execution script、Alfresco task script。
例如下面的流程中,在“任务监听器”用户任务的“Task Listeners”里设定了2个任务监听器:
其中第1个任务监听器,在任务创建时(create)被触发:
它是一个 Java Class ,且必须实现任务监听器接口(TaskListener),重载notify()
方法。代码如下:
/**
* 创建用户任务监听器
*/
ppublic class CreateTaskListener implements TaskListener {
/** Generated serial version UID. */
private static final long serialVersionUID = 7110551000348483426L;
private Expression content;
private Expression task;
@Override
public void notify(DelegateTask delegateTask) {
System.out.println(task.getValue(delegateTask));
delegateTask.setVariable("setInTaskCreate", delegateTask.getEventName() + ", " + content.getValue(delegateTask));
System.out.println(delegateTask.getEventName() + ",任务分配给:" + delegateTask.getAssignee());
delegateTask.setAssignee("jenny");
}
}
我们在代码中可以看到,此任务监听器被触发时的执行逻辑是:先设置流程变量 setInTaskCreate
,然后把当前任务分配给了用户 jenny 。
其中第2个任务监听器,在任务分配时(assignment)被触发:
它是一个“Delegate expression”。设定的表达式是:${assignmentDelegate}
。此assignmentDelegate
变量,是在流程启动的时候设置到流程变量中的,由它来指定具体的监听实现类。见下面的代码:
private static final String PROC_DEF_KEY = "listener";
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("assignmentDelegate", new TaskAssigneeListener());
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(PROC_DEF_KEY, variables);
其中指定用 TaskAssigneeLister
对象来作为监听实现类。当然,具体监听实现类还可以在引擎中配置或者由 Spring 代理。
此任务监听器的实现代码如下:
package mybpm.activiti.bpmn.example.listener;
import java.io.Serializable;
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
public class TaskAssigneeListener implements TaskListener, Serializable {
/** Generated serial version UID. */
private static final long serialVersionUID = 2420020197497369628L;
@Override
public void notify(DelegateTask delegateTask) {
System.out.println(delegateTask.getEventName() + ",任务分配给:" + delegateTask.getAssignee());
}
}
它会在被触发时,在控制台打印输出一行信息。我们可以通过观察控制台的输出,来验证它能正常工作。
# 用户任务
参考链接:
# 多个候选人
在流程设计中,一个用户任务可以设定多个候选人。即他们中的任意一个人都可以签收此任务,一旦其中某个人签收,此任务就归签收人处理(其他候选人将查询不到此任务了),且最终的审批结果将由此签收人唯一决定。类似于“先到先得”的方式。
例如下面的一个示例流程,在用户任务中设定了2个候选人“jackchen,henryyan”,候选人之间用逗号隔开。如下图:
在启动流程实列之前,我们需要先创建2个用户:
// 添加新用户:jackchen
User userJackChen = identityService.newUser("jackchen");
userJackChen.setFirstName("Jack");
userJackChen.setLastName("Chen");
userJackChen.setEmail("jackchen@gmail.com");
identityService.saveUser(userJackChen);
// 添加新用户:henryyan
User userHenryyan = identityService.newUser("henryyan");
userHenryyan.setFirstName("Henry");
userHenryyan.setLastName("Yan");
userHenryyan.setEmail("yanhonglei@gmail.com");
identityService.saveUser(userHenryyan);
单元测试案例代码(节选)如下:
// 启动流程
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(PROC_DEF_KEY);
Assert.assertNotNull(processInstance);
// 查询以jackchen、henryyan作为候选人的一条任务,都应该找到。
Task jackchenTask = taskService.createTaskQuery().taskCandidateUser("jackchen").singleResult();
Assert.assertNotNull(jackchenTask);
Task henryyanTask = taskService.createTaskQuery().taskCandidateUser("henryyan").singleResult();
Assert.assertNotNull(henryyanTask);
// 用户jackchen签收任务,流程的最终审批结果将由他说了算
taskService.claim(jackchenTask.getId(), "jackchen");
// 再次查询另一个用户nenryyan是否拥有刚刚的候选任务,应该找不到
henryyanTask = taskService.createTaskQuery().taskCandidateUser("henryyan").singleResult();
Assert.assertNull(henryyanTask);
// 用户完成任务,流程结束
taskService.complete(jackchenTask.getId());
// 再查询所有用户是否拥有刚刚的候选任务,都应该找不到
jackchenTask = taskService.createTaskQuery().taskCandidateUser("jackchen").singleResult();
Assert.assertNull(jackchenTask);
henryyanTask = taskService.createTaskQuery().taskCandidateUser("henryyan").singleResult();
Assert.assertNull(henryyanTask);
我们从单元测试代码中可以看到:流程启动后,此用户任务将可以被流程定义中设定的多个候选人签收(他们作为候选人,都可以查询到有这么一条任务),一旦其中任意一个候选人签收(在本例中是用户“jackchen”签收),这个用户任务就归他处理,而另一个候选人(用户“henryyan”)将再也查询不到有这么一条任务了。并且,最终的审批结果将由此签收人(用户“jackchen”)唯一决定。
实际进行流程设计中,像上面那样在流程定义中直接指定多个具体候选人的方式,影响流程定义的通用性。为了避免这种情况,我们可以使用流程变量。即在流程定义的“Candidate users (comma seperated)”中填入一个变量(例如填入:${userlist}),然后在启动流程实例的时候,通过 API 把这个变量("userlist")赋值(例如让它等于字符串:"jackchen,henryyan")并带入到流程实例中,实现同样的效果。另外,也可以通过监听器,在流程实例运行到此用户任务之前,将此变量的值设置到流程实例中。
# 候选组
在流程设计中,一个用户任务可以设定给一个候选组。即组内的任意一个人都可以签收此任务,一旦其中某个人签收,此任务就归签收人处理(其他候选人将查询不到此任务了),且最终的审批结果将由此签收人唯一决定。类似于“先到先得”的方式。
例如下面的一个示例流程,在用户任务中设定了一个候选组“deptLeader”。如下图:
在启动流程实例之前,我们需要准备好预先设定了的个用户“jackchen”和“henryyan”,他们都隶属于“deptLeader”组。代码如下:
// 创建并保存用户对象
User user1 = identityService.newUser("jackchen");
user1.setFirstName("Jack");
user1.setLastName("Chen");
user1.setEmail("jackchen@gmail.com");
identityService.saveUser(user1);
// 创建并保存用户对象
User user2 = identityService.newUser("henryyan");
user2.setFirstName("Henry");
user2.setLastName("Yan");
user2.setEmail("yanhonglei@gmail.com");
identityService.saveUser(user2);
// 创建并保存组对象
Group group = identityService.newGroup("deptLeader");
group.setName("部门领导");
group.setType("candidate"); //不清楚这个属性,似乎它并未起任何作用
identityService.saveGroup(group);
// 把用户加入到组deptLeader中
identityService.createMembership(user1.getId(), group.getId());
identityService.createMembership(user2.getId(), group.getId());
单元测试案例代码(节选)如下:
// 启动流程
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(PROC_DEF_KEY);
assertNotNull(processInstance);
// 查询以组作为候选人的一条任务,应该找到
Task groupTask = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
Assert.assertNotNull(groupTask);
// 查询以组成员作为候选人的一条任务,都应该找到
Task jackchenTask = taskService.createTaskQuery().taskCandidateUser("jackchen").singleResult();
assertNotNull(jackchenTask);
Task henryyanTask = taskService.createTaskQuery().taskCandidateUser("henryyan").singleResult();
assertNotNull(henryyanTask);
// 用户jackchen签收任务,流程的最终审批结果将由他说了算
taskService.claim(jackchenTask.getId(), "jackchen");
// 再次查询另一个用户henryyan是否拥有刚刚的候选任务,应该找不到
henryyanTask = taskService.createTaskQuery().taskCandidateUser("henryyan").singleResult();
assertNull(henryyanTask);
// 用户完成任务,流程结束
taskService.complete(jackchenTask.getId());
// 查询以组作为候选人的一条任务,应该找不到
Task groupTask = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
Assert.assertNull(groupTask);
// 再查询所有用户是否拥有刚刚的候选任务,都应该找不到
jackchenTask = taskService.createTaskQuery().taskCandidateUser("jackchen").singleResult();
Assert.assertNull(jackchenTask);
henryyanTask = taskService.createTaskQuery().taskCandidateUser("henryyan").singleResult();
Assert.assertNull(henryyanTask);
我们从单元测试代码中可以看到:流程启动后,此用户任务将可以被流程定义中设定的组(可多个组,用逗号分隔开)内的多个候选人签收(他们都可以查询到有这么一条任务),一旦其中任意一个候选人签收(此例中是用户“jackchen”签收),这个用户任务就归他处理了,而另一个候选人(此例中是用户“henryyan”)将再也查询不到有这么一条任务了。并且,最终的审批结果将由此签收人(用户“jackchen”)唯一决定。
同前例相似,我们也可以在流程定义中用变量来设定组,满足更好的通用性。
# 用户管理API
下面归纳 一下 Activiti 中用户管理的 API。
用户管理
// 创建用户 User user = identityService.newUser("henryyan"); user.setFirstName("Henry"); user.setLastName("Yan"); user.setEmail("yanhonglei@gmail.com"); // 保存用户到数据库 identityService.saveUser(user); // 验证用户是否保存成功 User userInDb = identityService.createUserQuery().userId("henryyan").singleResult(); Assert.assertNotNull(userInDb); // 删除用户 identityService.deleteUser("henryyan"); // 验证是否删除成功 userInDb = identityService.createUserQuery().userId("henryyan").singleResult(); Assert.assertNull(userInDb);
组管理
// 创建一个组对象 Group group = identityService.newGroup("deptLeader"); group.setName("部门领导"); group.setType("assignment"); // 保存组 identityService.saveGroup(group); // 验证组是否已保存成功,首先需要创建组查询对象 List<Group> groupList = identityService.createGroupQuery().groupId("deptLeader").list(); Assert.assertEquals(1, groupList.size()); // 删除组 identityService.deleteGroup("deptLeader"); // 验证是否删除成功 groupList = identityService.createGroupQuery().groupId("deptLeader").list(); Assert.assertEquals(0, groupList.size());
组和用户关联
// 创建并保存组对象 Group group = identityService.newGroup("deptLeader"); group.setName("Department Leader"); group.setType("assignment"); identityService.saveGroup(group); // 创建并保存用户对象 User user = identityService.newUser("henryyan"); user.setFirstName("Henry"); user.setLastName("Yan"); user.setEmail("yanhonglei@gmail.com"); identityService.saveUser(user); // 把用户henryyan加入到组deptLeader中 identityService.createMembership("henryyan", "deptLeader"); // 查询属于组deptLeader的用户 User userInGroup = identityService.createUserQuery().memberOfGroup("deptLeader").singleResult(); Assert.assertNotNull(userInGroup); Assert.assertEquals("henryyan", userInGroup.getId()); // 查询用户henryyan所属组 Group groupContainsHenryyan = identityService.createGroupQuery().groupMember("henryyan").singleResult(); Assert.assertNotNull(groupContainsHenryyan); Assert.assertEquals("deptLeader", groupContainsHenryyan.getId()); // 把用户henryyan从组deptLeader中删除(只是删除关联关系,用户和组都还在) identityService.deleteMembership("henryyan", "deptLeader");
通过上述代码片段,我们可以看到 Activiti 中用户、组、组和用户关联关系的 API 的使用。
# 网关
网关(Gateway)用于控制流程的走向。在 Activiti 中网关共有4种:
- 排他网关(Exclusive Gateway)
- 并行网关(Parrallel Gateway)
- 包容网关(Inclusive Gateway)
- 事件网关(Event Gateway)
# 网关示例1
下面的示例是“并行网关 + 排他网关”的一个例子,在 eclipse 的流程编辑器里,流程设计如下图:
**注意:**并行网关和排他网关的表示符号的差别,并行网关是菱形符号内部为“+”,而排他网关的内部是“×”。
从流程上看,在并行网关的上面是“班主任审批”用户任务:
此任务要求“headTeacher”组的成员处理:
并行网关的下面是“宿舍管理员审批”用户任务:
此任务要求“dormSupervisor”组的成员处理:
我们可以看到“班主任审批”和“宿舍管理员审批”两个审批任务是并行的,它们没有先后顺序,前后连接它们的是两个并行网关符号(将流程走向分开,再汇总)。并行处理的两个审批任务必须全部都完成后,才会继续,进入到下一步——排他网关,做判断后分流:
- 若判断为“审批不通过”,则流程结束。
- 若判断为“全部审批通过”,则进入系统登记确认,再学生确认,最后流程结束。
这就是此示例流程的全部逻辑。
需要特别注意的是,并行网关要求其分流的全部并行任务都完成才会在汇聚点结束,流程得以继续。在本例中,某一个并行任务被拒绝后,尽管此时已经明确最终审批结果是不通过的,但并行网关仍要求另一个并行任务完成后(此时,不管此并行任务的结果是通过还是拒绝,都不影响最终审批是不通过的结果)才能继续。从这一点看,它显得效率不高,并不能完美地实现“一票否决”的应用场景。
当并行网关上的审批都完成后,流程得以继续,来到了排他网关。我们注意到排他网关在“Default flow”设置上选择了“flow22”。如下图:
此“flow22”,作为排他网关的默认分支,是“审批不通过”:
而另一分支,是“全部审批通过”,Id 为“flow20”。如下图:
它还设置了一个判断条件:
${headTeacherApproved == 'true' && dormSupervisorApproved == 'true'}
设置此判断条件的意思是,只有这个表达式结果为真,才会通过此路径,进入下一个任务“系统登记确认”。否则,将默认走排他网关设定的“审批不通过”路径(如前述,排他网关设定了“Default flow”为“flow22”)。
后续的任务“系统登记确认”和“学生确认”是普通的任务,我们这里不关注它们,暂且忽略。
此示例流程的关注点在并行网关和排他网关的流转逻辑。在单元测试中,我们考虑了下列几种情况:
- 并行任务“班主任审批”和“宿舍管理员审批”都通过
- 并行任务“班主任审批”通过,而“宿舍管理员审批”拒绝
- 并行任务“班主任审批”拒绝,而“宿舍管理员审批”通过
- 并行任务“班主任审批”和“宿舍管理员审批”都拒绝
单元测试的结果符合预期,验证了此示例流程的正确性。
# 会签
参考链接:
- 2019-05-07,Activiti会签 (opens new window)
- 2017-09-06,Activiti 工作流会签 / 多人审批时若一人通过即可 (opens new window)
在流程业务管理中,用户任务(UserTask)通常都是由一个人去处理的,而多个人同时处理一个任务,称之为会签任务。
会签有如下几种常见的形式:
- 一票通过:只要有任意一个审批人通过,会签就以通过结束;必须全部审批人都拒绝,会签结束后才算拒绝。
- 一票否决:只要有任意一个审批人拒绝,会签就以拒绝结束;必须全部审批人都通过,会签结束后才算通过。
- 按数量通过:达到一定数量的审批人通过表决后,会签结束才算通过。
- 按比例通过:达到一定比例的审批人通过表决后,会签结束才算通过。
Activiti 会签是基于多实例任务。要将任务节点设置成多实例,是通过在任务节点的“Multi instance”属性上配置。以 eclipse 中的流程编辑器为例,如下图:
选择一个用户任务,在下面的属性中选择“Multil instance”页,可以看到有多个属性:
- Sequential(执行顺序): 必选项,单选(true-多实例顺序执行,false-多实例并行)。用于设置多实例的执行顺序。
- Loop cardinality(循环基数): 可选项,大于零的整型数。表示会签的人数。
- Collection(集合): 可选项,通常为列表(List)。会签人数的集合,此选项和 Loop cardinality 互斥(二选一)。
- Element variable(元素变量): 选择 Collection 时必选,为 Collection 集合每次遍历的元素的变量名。
- Completion condition(完成条件): 可选项,它是一个表达式,表示会签结束的条件。例如设置一个人完成后(如拒绝)会签结束,那么其他人的代办任务就都会消失。
会签环节中涉及的几个默认流程变量有:
- nrOfInstances(意思是 numberOfInstances): 会签中总共的实例数
- nrOfCompletedInstances: 已经完成的实例数量
- nrOfActiviteInstances: 当前活动的实例数量,即还没有完成的实例数量
我们可以选择它们用于设置会签结束的条件,例如:条件${nrOfInstances == nrOfCompletedInstances}
表示所有人审批完成后会签才结束;条件${nrOfCompletedInstances == 1}
表示一个人完成审批,该会签就结束。当然,除了这几个默认流程变量外,我们也可以用自定义的流程变量。
为了更好的实现会签,我们可以结合监听(Listener)功能。注意,这是可选项。
监听可以有多种实现,它们是:Java class、Expression、Delegate expression、Alfresco execution script、Alfresco task script。这里仅介绍 Java class 类型的监听实现。
对于任务的监听,有四种触发条件,分别是:
- create:任务创建的时候触发监听
- Assignment:设置受理人的时候触发监听
- Complete:任务完成的时候触发监听
- All:以上三种事件都会触发监听
编写的监听 Java class 需要实现 TaskListener 接口并重载 notify() 方法。
# 会签示例1:一票通过
下面以一个请假申请流程为例。拟提出申请后,由3人并行会签。其中任意一个审批人通过后,会签将结束并认为通过;只有全部审批人都拒绝,会签才认为不通过。会签通过后,再记录到系统中。此请假流程的设计如下图:
示例包括如下源文件:
- 流程定义:
ApplyHoliday1.bpmn
- 监听器:
MultiSign1Listener.java
- 单元测试:
ApplyHoliday1Test.java
在流程中,第一步“申请”中提交请假申请,如下图:
在第二步“会签”中,多人进行会签审批,如下图:
注意,在会签的任务中“Main config”页面的 Assignee,它设定了使用变量${signer}
。如下图:
它实际上是引用了“Multi instance”页面里面“Element variable”设定的变量。如下图:
上图中的${signList}
是存储会签审批人员的流程变量,它是一个列表(List),signer 为每次遍历时的临时变量名,和之前的引用${signer}
对应。这个会签人员的列表,将在第一次申请任务提交时设置进入流程变量中。也即,必须在进入到会签任务之前就将会签人员的列表添加到流程变量中。还有另外一种做法,在申请任务里设置一个监听类,让此任务完成的时候触发此监听类,在监听类执行过程中(还可以根据申请的员工不同,做额外的一些逻辑判断,更灵活)将会签人员的列表设置进去。在“会签示例2:一票否决”中,将展示这种做法。
本例中“Completion condition”填写的是${pass == true}
。前面已经提到过,此处可以用会签中默认的流程变量来控制会签的执行过程,当然,也可以用自定义的流程变量。这里的 pass 就是自定义的流程变量,在当前会签人员处理自己任务时(同意让 pass=true,不同意让 pass=false)添加到流程的变量字典中。只要有一个审批人通过(设置pass=true),整个会签将会结束。在监听器MultiSign1Listener
里,也将设置result=Y,作为会签的结果。反之,只有全部审批人都拒绝(设置pass=false),在监听器里,才会设置result="N",作为总的会签结果,再流转到排他网关(exclusive gateway)进行判断分流。
本例中的会签任务,添加了 Java class 类型的监听,由 MultiSign1Listener.java
实现。它在会签任务完成时将被触发。它的作用是,在当前会签人员完成审批任务后,统计该任务的审批结果,设置result作为总的会签结果("Y"-通过,"N"-不通过)。下图是为会签任务添加监听,事件选择 complete (即任务完成时触发),如图:
经过单元测试,上述流程符合预期,实现了一票通过。只要有任何一个审批人通过,会签就结束并认为通过,然后经过排他网关的“通过”分支,再到“记录”并结束整个流程。否则将一直通过排他网关的“拒绝”分支返回到“申请”任务。
# 会签示例2:一票否决
下面以一个请假申请为例。拟提出申请后,由3人并行会签。其中任意一个审批人拒绝后,会签将结束并认为不通过;只有全部审批人都同意,会签才认为通过。会签通过后,再记录到系统中。此请假流程的设计如下图:
示例包括如下源文件:
- 流程定义:
ApplyHoliday2.bpmn
- 监听器:
- 用于申请的监听器:
ApplyTask2Listener.java
,它用于将会签的审批人员设置进入到流程变量中。 - 用于会签的监听器:
MultiSign2Listener.java
- 用于申请的监听器:
- 单元测试:
ApplyHoliday2Test.java
在流程中,第一步“申请”中提交请假申请,如下图:
在第二步“会签”中,多人进行会签审批,如下图:
注意,在会签的任务中“Main config”页面的 Assignee,它设定了使用变量${signer}
。如下图:
它实际上是引用了“Multi instance”页面里面“Element variable”设定的变量。如下图:
上图中的${signList}
是存储会签审批人员的流程变量,它是一个列表(List),signer 为每次遍历时的临时变量名,和之前的引用${signer}
对应。这个会签人员的列表,将在第一次申请任务结束时,由监听器设置进去。
本例中“Completion condition”填写的是${pass == false}
。前面已经提到过,此处可以用会签中默认的流程变量来控制会签的执行过程,当然,也可以用自定义的流程变量。这里的 pass 就是自定义的流程变量,在当前会签人员处理自己任务时(同意让 pass=true,不同意让 pass=false)添加到流程的变量字典中。只要有一个审批人拒绝(设置pass=false),整个会签将会结束。在监听器MultiSign2Listener
里,也将设置result=N,作为会签的结果。反之,只有全部审批人都通过(设置pass=true),在监听器里,才会设置result="Y",作为总的会签结果,再流转到排他网关(exclusive gateway)进行判断分流。
本例中的会签任务,添加了 Java class 类型的监听,由 MultiSign2Listener.java
实现。它在会签任务完成时将被触发。它的作用是,在当前会签人员完成审批任务后,统计该任务的审批结果,设置result作为总的会签结果("Y"-通过,"N"-不通过)。下图是为会签任务添加监听,事件选择 complete(即任务完成时触发),如下图:
经过单元测试,上述流程符合预期,实现了一票否决。只要有任何一个审批人拒绝,会签就结束并认为不通过,然后经过排他网关的“拒绝”分支返回到“申请”任务。只有全部审批人都通过,才能通过排他网关的“通过”分支,再来到“记录”并结束整个流程。
# 会签示例3:按数量通过
下面以一个请假申请流程为例。拟提出申请后,由4人并行会签。一旦其中任意2个审批人通过后,会签将结束并认为通过;否则,等全部审批人都完成后,会签认为不通过。会签通过后,再记录到系统中。此请假流程的设计如下图:
示例包括如下源文件:
- 流程定义:
ApplyHoliday3.bpmn
- 监听器:
- 用于申请的监听器:
ApplyTask3Listener.java
,它用于将会签的审批人员设置进入到流程变量中。 - 用于会签的监听器:
MultiSign3Listener.java
,它用于审批人通过/拒绝的计数,并判断和设置最终的会签状态。
- 用于申请的监听器:
- 单元测试:
ApplyHoliday3Test.java
在流程中,第一步“申请”中提交请假申请,如下图:
在第二步“会签”中,多人进行会签审批,如下图:
注意,在会签的任务中“Main config”页面的 Assignee,它设定了使用变量${signer}
。如下图:
它实际上是引用了“Multi instance”页面里面“Element variable”设定的变量。如下图:
上图中的${signList}
是存储会签审批人员的流程变量,它是一个列表(List),signer 为每次遍历时的临时变量名,和之前的引用${signer}
对应。这个会签人员的列表,共4人,将在第一次申请任务结束时,由监听器设置进去。
本例中“Completion condition”填写的是${result == "Y"}
,这里的 result 是自定义的流程变量。
本例中的会签任务,添加了 Java class 类型的监听,由 MultiSign3Listener.java
实现。它在会签任务完成时将被触发。当前会签人员处理自己任务时(同意让 pass=true,不同意让 pass=false)添加到流程的变量字典中。在监听器MultiSign3Listener
里,会统计同意的个数(保存在自定义的变量numOfPassed
中)和不同意的个数(保存在自定义的变量numOfRejected
中),然后它会在“numOfPassed >= 2”的情况下,设置result="Y",否则设置result="N",作为会签的结果。
一旦监听器设置result="Y"
,就会触发会签结束(见“Completion condition”填写的条件)。否则,就等到全部都审批完,最终必然是result="N"
。result
变量作为总的会签结果("Y"-通过,"N"-不通过),再流转到排他网关(exclusive gateway)进行判断分流。
下图是为会签任务添加监听,事件选择 complete(即任务完成时触发),如下图:
经过单元测试,上述流程符合预期,实现了按数量通过。只要有2个审批人通过,会签就结束并认为通过,然后经过排他网关的“通过”分支,再来到“记录”并结束整个流程。若未达到指定数量的审批人通过,将通过排他网关的“拒绝”分支返回到“申请”任务。
# 会签示例4:按比例通过
下面以一个请假申请流程为例。拟提出申请后,由5人并行会签。一旦其中任意3个审批人通过(>50%)后,会签将结束并认为通过;否则,等全部审批人都完成后,会签认为不通过。会签通过后,再记录到系统中。此请假流程的设计如下图:
示例包括如下源文件:
- 流程定义:
ApplyHoliday4.bpmn
- 监听器:
- 用于申请的监听器:
ApplyTask4Listener.java
,它用于将会签的审批人员设置进入到流程变量中。 - 用于会签的监听器:
MultiSign4Listener.java
,它用于审批人通过/拒绝的计数,并判断和设置最终的会签状态。
- 用于申请的监听器:
- 单元测试:
ApplyHoliday4Test.java
此流程和“会签示例3:按数量通过”的设计大部分相似,主要是在监听器里计算与判断条件(之前是个数,现在是百分比)有不同。