1. 什么是工作流
工作流:就是通过计算机对业务流程自动化执行进行管理。
它主要解决的是:使在多个参与者之间按照某种预定义的规则自动进行传递文档、信息或任务的过程。
例如:员工的请假,出差,外出采购,合同审核等等,这些过程,都是一个工作流。
2. 为什么要使用工作流
不使用工作流:
对于工作流的处理,如果采用原始的方式,我们需要拿着各种文件到各个负责人那里去签字,需要在多个部门之间不断审批,这种方式费时费力。
例如:填写请假单->部门经理审批->总经理审批->人事备案。
要实现上述的流程,我们自己可以通过字段标识来实现这个审批效果,在业务表中加个字段,比如填写请假单用1标识,部门经理用2标识,总经理用3标识,人事备案用4标识,好像看起来没啥问题,也实现了审批效果。
可是一旦我们的流程出现了变化,这个时候我们就需要改动我们的代码了,这显然是不可取的,特别麻烦。
使用工作流:
工作流引擎实现了一个规范,这个规范要求我们的流程管理与状态字段无关,始终都是读取业务流程图的下一个节点。
当业务更新的时候我们只需要更新业务流程图就行了。这就实现了业务流程改变,不用修改代码。
3. 常见的工作流
主流的框架有:Activiti、jBPM、Camunda 、Flowable
4. Activiti介绍
activiti是一个工作流引擎,可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN进行定义,业务流程按照预先定义的流程进行执行。
5. BPMN介绍
BPMN:即业务流程模型和符号,是一套标准的业务流程建模符号,使用 BPMN 提供的符号可以创建业务流程。Activit 就是使用 BPMN 进行流程建模、流程执行管理的。
BPMN 使用一些符号来明确业务流程设计流程图的一套符号规范,能增进业务建模时的沟通效率。
6. Activty使用流程
- 引入依赖
- 绘制流程模型
- 流程部署(使用 activiti 提供的 api 向 activiti 中部署.bpmn 文件)
- 启动一个流程实例(启动一个流程实例就好比new一个java对象)
- 用户查询待办任务
- 用户办理任务
- 流程结束
7. Activty入门实战体验
7.1 添加依赖
<!--引入activiti的springboot启动器 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M6</version>
<exclusions>
<exclusion>
<artifactId>mybatis</artifactId>
<groupId>org.mybatis</groupId>
</exclusion>
</exclusions>
</dependency>
7.2 添加配置文件
spring:
activiti:
#false:默认,数据库表不变,但是如果版本不对或者缺失表会抛出异常(生产使用)
#true:表不存在,自动创建(开发使用)
#create_drop: 启动时创建,关闭时删除表(测试使用)
#drop_create: 启动时删除表,在创建表 (不需要手动关闭引擎)
database-schema-update: true
#监测历史表是否存在,activities7默认不开启历史表
db-history-used: true
#none:不保存任何历史数据,流程中这是最高效的
#activity:只保存流程实例和流程行为
#audit:除了activity,还保存全部的流程任务以及其属性,audit为history默认值
#full:除了audit、还保存其他全部流程相关的细节数据,包括一些流程参数
history-level: full
#校验流程文件,默认校验resources下的process 文件夹的流程文件
check-process-definitions: true
7.3 启动项目
启动项目,即可生成项目数据库表
7.4 数据库表介绍
Activiti 的运行支持必须要有这 25 张表的支持。
表的命名规则和作用介绍:
Activiti 的表都以 act_ 开头,紧接着是表示表的用途的两个字母标识,也和 Activiti 所提供的服务的 API 对应。
- ACT_RE:RE 表示 repository,这个前缀的表包含了流程定义和流程静态资源 (图片、规则、等等)
- ACT_RU:RU 表示 runtime,这些表运行时,会包含流程实例、任务、变量、异步任务等流程业务进行中的数据。Activiti 只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。
- ACT_HI: HI 表示 history,这些表包含一些历史数据,比如历史流程实例、变量、任务等等
- ACT_GE:GE 表示 general,通用数据
Activiti常用Service服务接口介绍:
-
RepositoryService
Activiti 的资源管理类,该服务负责部署流程定义,管理流程资源。
在使用 Activiti 时,一开始需要先完成流程部署,即将使用建模工具设计的业务流程图通过 RepositoryService 进行部署。 -
RuntimeService
Activiti 的流程运行管理类,该服务用于开始一个新的流程实例,获取关于流程执行的相关信息。
流程定义用于确定一个流程中的结构和各个节点间行为,而流程实例则是对应的流程定义的一个执行,可以理解为 Java 中类和对象的关系。 -
TaskService
Activiti 的任务管理类,用于处理业务运行中的各种任务。
例如查询分给用户或组的任务、创建新的任务、分配任务、确定和完成一个任务。 -
HistoryService
Activiti 的历史管理类,该服务可以查询历史信息。在执行流程工程中,引擎会保存很多数据。
比如流程实例启动时间、任务的参与者、完成任务的时间、每个流程实例的执行路径等等。这个服务主要通过查询功能来获得这些数据。 -
ManagementService
Activiti 的引擎管理类,该服务提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用。
主要用于 Activiti 系统的日常维护。
7.4 流程定义部署
部署的方法有各种各样的。
流程定义部署后,会操作activiti的3张表如下:
act_re_deployment 流程定义部署表,每部署一次增加一条记录
act_re_procdef 流程定义表,部署每个新的流程定义都会在这张表中增加一条记录
act_ge_bytearray 流程资源表
7.5 启动流程实例
@Autowired
private RuntimeService runtimeService;
@Test
public void startUpProcess() {
//创建流程实例,我们需要知道流程定义的key
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia");
//输出实例的相关信息
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
System.out.println("当前活动Id:" + processInstance.getActivityId());
}
7.4的 流程定义:将bpmn文件放到activiti的三张表中,好比是java中的一个类。
7.5的 流程实例:好比是java中的一个对象
(一个流程定义可以对应多个流程实例)
7.6 查询任务
@Autowired
private TaskService taskService;
/**
* 查询当前个人待执行的任务
*/
@Test
public void findPendingTaskList() {
//任务负责人
String assignee = "zhangsan";
List<Task> list = taskService.createTaskQuery()
.taskAssignee(assignee)//只查询该任务负责人的任务
.list();
for (Task task : list) {
System.out.println("流程实例id:" + task.getProcessInstanceId());
System.out.println("任务id:" + task.getId());
System.out.println("任务负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());
}
}
每个节点都配置了Assignee,流程启动后,任务的负责人就可以查询自己当前需要处理的任务,查询出来的任务都是该用户的待办任务。
7.7 处理任务
/**
* 完成任务
*/
@Test
public void completTask(){
Task task = taskService.createTaskQuery()
.taskAssignee("zhangsan") //要查询的负责人
.singleResult();//返回一条
//完成任务,参数:任务id
taskService.complete(task.getId());
}
完成任务后,任务自动到下一个节点
7.8 查询已处理任务
@Autowired
private HistoryService historyService;
/**
* 查询已处理历史任务
*/
@Test
public void findProcessedTaskList() {
//张三已处理过的历史任务
List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().taskAssignee("zhangsan").finished().list();
for (HistoricTaskInstance historicTaskInstance : list) {
System.out.println("流程实例id:" + historicTaskInstance.getProcessInstanceId());
System.out.println("任务id:" + historicTaskInstance.getId());
System.out.println("任务负责人:" + historicTaskInstance.getAssignee());
System.out.println("任务名称:" + historicTaskInstance.getName());
}
}
8. 流程实例详解
什么是流程实例:
流程定义ProcessDefinition,
流程实例ProcessInstance
他们是Activiti重要的概念,类似于Java类和Java实例的关系。
启动一个流程实例表示开始一次业务流程的运行。
比如员工请假流程部署完成,如果张三要请假就可以启动一个流程实例。
如果李四也要请假,则也启动一个流程实例。
两个流程的执行互相不影响,就好比定义一个 java 类,实例化两个对象一样
如何获取流程定义:
获取流程定义的方法,在Activity中,提供了多种查询方法。
如,你可以使用如下方式来获取:
// 创建流程定义查询的对象
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
// 通过查询对象构建指定要查找的流程定义的名称
ProcessDefinition latestProcessDefinition = processDefinitionQuery.processDefinitionName("你的流程标识").latestVersion().singleResult();
在流程定义(ProcessDefinition)中,常用的属性包括以下内容:
ID(id): 流程定义的唯一标识符,通常是一个字符串或UUID。
Key(key): 流程定义的唯一标识键,通常是一个字符串。在启动流程实例时,可以使用流程定义的key来指定要启动的流程。
名称(name): 流程定义的名称,通常是一个描述性的字符串,用于标识流程的用途或目的。
版本号(version): 流程定义的版本号,用于区分不同版本的同一流程定义。当对流程定义进行更新时,版本号会递增。
部署ID(deploymentId): 流程定义所属的部署的唯一标识符。每次部署新的流程定义时,会生成一个新的部署ID。
资源名称(resourceName): 流程定义的XML资源文件的名称,通常以.bpmn或.bpmn20.xml为扩展名。这个文件包含了流程定义的描述信息。
部署时间(deploymentTime): 流程定义被部署的时间戳,记录了流程定义的创建时间。
描述(description): 流程定义的描述信息,通常提供了更详细的说明关于流程定义的用途和功能。
如何启动流程实例:
启动流程实例,在Activity中,提供了多种重载方法。
以下是 runtimeService 的部分方法:
startProcessInstanceByKey(): 通过流程定义的键(Key)来标识要启动的流程。
startProcessInstanceById(): 通过流程定义的ID来标识要启动的流程。
startProcessInstanceByMessage(): 根据流程定义中的消息启动新流程实例。
如何挂起/激活流程实例:
// 全部流程实例挂起
// 操作流程定义为挂起状态时:
// 该流程定义将不允许启动新的流程实例,同时该流程定义下所有的流程实例将全部挂起暂停执行。
@Test
public void suspendProcessInstance() {
ProcessDefinition qingjia = repositoryService.createProcessDefinitionQuery().processDefinitionKey("qingjia").singleResult();
// 获取到当前流程定义是否为暂停状态 suspended方法为true是暂停的,suspended方法为false是运行的
boolean suspended = qingjia.isSuspended();
if (suspended) {
// 暂定,那就可以激活
// 参数1:流程定义的id 参数2:是否激活 参数3:时间点
repositoryService.activateProcessDefinitionById(qingjia.getId(), true, null);
System.out.println("流程定义:" + qingjia.getId() + "激活");
} else {
repositoryService.suspendProcessDefinitionById(qingjia.getId(), true, null);
System.out.println("流程定义:" + qingjia.getId() + "挂起");
}
}
// 单个流程实例挂起
// 操作流程实例对象为挂起状态时:
// 针对单个流程执行挂起操作,某个流程实例挂起则此流程不在继续执行,完成该流程实例的当前任务将报异常;
@Test
public void SingleSuspendProcessInstance() {
String processInstanceId = "8bdff984-ab53-11ed-9b17-f8e43b734677";
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
//获取到当前流程定义是否为暂停状态 suspended方法为true代表为暂停 false就是运行的
boolean suspended = processInstance.isSuspended();
if (suspended) {
runtimeService.activateProcessInstanceById(processInstanceId);
System.out.println("流程实例:" + processInstanceId + "激活");
} else {
runtimeService.suspendProcessInstanceById(processInstanceId);
System.out.println("流程实例:" + processInstanceId + "挂起");
}
}
9. 任务分配
任务分配:就是将任务分配给指定的负责人。
三种方式:
- 固定分配
- UEL表达式分配
- 监听器分配
9.1 固定分配
业务流程建模时指定固定的任务负责人,如:Assignee:zhangsan/lisi
9.2 表达式分配
activiti支持两个UEL表达式:UEL-value和UEL-method。
UEL-value方式:
/**
* 启动流程实例
*/
@Test
public void startUpProcess01() {
Map<String, Object> variables = new HashMap<>();
variables.put("assignee1","zhangsan");
variables.put("assignee2","lisi");
//创建流程实例,我们需要知道流程定义的key
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia01", variables);// 启动实例的方法跟之前的方法基本一致,唯一的不同是在启动时,添加了一个参数
//输出实例的相关信息
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
}
UEL-method 方式:
// userBean 是 spring 容器中的一个 bean,表示调用该 bean 的 getUsername(int id)方法。
// 经理审批:${userBean.getUsername(1)}
// 人事审批:${userBean.getUsername(2)}
@Component
public class UserBean {
public String getUsername(int id) {
if(id == 1) {
return "zhangsan";
}
if(id == 2) {
return "lisi";
}
return "admin";
}
}
@Test
public void deployProcess02() {
// 流程部署
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("process/jiaban02.bpmn20.xml")
.name("加班申请流程")
.deploy();
System.out.println(deploy.getId());
System.out.println(deploy.getName());
}
/**
* 启动流程实例
*/
@Test
public void startUpProcess02() {
//创建流程实例,我们需要知道流程定义的key
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia02");
//输出实例的相关信息
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
}
//启动流程实例,就会调用bean方法,参数为:1,经理审批后,接着调用bean方法,参数为:2
9.3 监听器分配
任务监听器:是发生对应的任务相关事件时执行自定义 java 逻辑 或表达式。
使用监听器的方式来指定负责人,那么在流程设计时就不需要指定assignee。
监听器事件选项:
1. Create:任务创建后触发
2. Assignment:任务分配后触发
3. Delete:任务完成后触发
4. All:所有事件发生都触发
实现:
创建一个任务分配的监听器类,实现 TaskListener 接口,重写 public void notify(DelegateTask delegateTask) 方法。
notify方法是任务监听器中的一个方法,用于在特定的任务节点执行时触发相应的操作。
参数DelegateTask是任务执行时的代理对象,可以通过该对象获取和设置任务的相关属性。
/**
* 在这里,通过调用delegateTask.getName()获取任务节点的名称,然后根据名称来设置任务的负责人。
* 所以,当任务节点的名称为"经理审批"时,任务负责人将被设置为"zhangsan",当任务节点的名称为"人事审批"时,任务负责人将被设置为"lisi"。
*/
public class MyTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
if(delegateTask.getName().equals("经理审批")){
//这里指定任务负责人
delegateTask.setAssignee("zhangsan");
} else if(delegateTask.getName().equals("人事审批")){
//这里指定任务负责人
delegateTask.setAssignee("lisi");
}
}
}
@Test
public void deployProcess03() {
// 流程部署
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("process/jiaban03.bpmn20.xml")
.name("加班申请流程")
.deploy();
System.out.println(deploy.getId());
System.out.println(deploy.getName());
}
/**
* 启动流程实例
*/
@Test
public void startUpProcess03() {
//创建流程实例,我们需要知道流程定义的key
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban03");
//输出实例的相关信息
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
}
// 启动流程实例,就会调用MyTaskListener监听方法
10. 流程变量
10.1 流程变量概念
流程运转有时需要靠流程变量,业务系统和 activiti结合时少不了流程变量,流程变量就是 activiti 在管理工作流时根据管理需要而设置的变量。
比如:在请假申请流程流转时如果请假天数大于2 天则由总经理审核,否则由部门经理直接审核, 请假天数就可以设置为流程变量,在流程流转时使用。
10.2 流程变量的作用域
流程变量的作用可以是一个流程实例,但也可以是一个任务(task)或是一个执行实例。
globa变量:
globa变量:当一个流程变量的作用域为流程实例时,可以称为 global 变量。
流程变量的默认作用域是流程实例。
global 变量中变量名不允许重复,设置相同名称的变量,后设置的值会覆盖先设置的变量值。
local变量:
local 变量:任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大, 称为 local 变量。
Local 变量由于在不同的任务或不同的执行实例中,所以,作用域互不影响,变量名可以相同没有影响。
10.3 流程变量的使用方法
流程变量可以用来实现任务分配,也可以在任务与任务之间的连线上使用 UEL表达式 来实现流程的走向。
比如${ day > 2 }和${day <= 2},day就是一个流程变量名称,UEL表达式的执行结果是布尔类型。
10.4 设置globa变量
在启动流程时设置流程变量,变量的作用域是整个流程实例。
@Test
public void startUpProcess() {
// ============ 通过 Map<key,value> 设置流程变量,map 中可以设置多个变量,这个 key 就是流程变量的名字 ================
Map<String, Object> variables = new HashMap<>();
variables.put("assignee1", "zhangsan");
variables.put("assignee2", "lisi");
//创建流程实例,我们需要知道流程定义的key
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia", variables);
//输出实例的相关信息
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
}
在任务办理时设置流程变量,它的作用域是整个流程实例。
@Test
public void completTask() {
Task task = taskService.createTaskQuery()
.taskAssignee("zhangsan") //要查询的负责人
.singleResult();//返回一条
// ============ 如果设置的流程变量的 key 在流程实例中已存在相同的名字则后设置的变量替换前边设置的变量。 ==============
Map<String, Object> variables = new HashMap<>();
variables.put("assignee2", "zhao");
//完成任务,参数:任务id
taskService.complete(task.getId(), variables);
}
通过流程实例 id 设置全局变量,它的作用域是整个流程实例
@Test
public void processInstanceIdSetVariables() {
// ============== 通过流程实例 id 设置全局变量, **它的作用域是整个流程实例** ,该流程实例必须未执行完成。 ========
Map<String, Object> variables = new HashMap<>();
variables.put("assignee2", "wang");
runtimeService.setVariables("1c347a90-82c6-11ed-96ca-7c57581a7819", variables);
}
10.5 设置local变量
local 流程变量的作用域只在当前任务节点下可用。
任务办理时设置local流程变量,当前运行的流程实例只能在该任务结束前使用,任务结束该变量无法在当前流程实例使用
@Test
public void completLocalTask() {
Task task = taskService.createTaskQuery()
.taskAssignee("zhangsan") //要查询的负责人
.singleResult();//返回一条
// 设置local变量,作用域为该任务
taskService.setVariableLocal(task.getId(),"assignee2","li");
// 查看local变量
System.out.println(taskService.getVariableLocal(task.getId(), "assignee2"));
//完成任务,参数:任务id
taskService.complete(task.getId());
}
11. 网关
网关:用来控制流程的流向,通常会和流程变量一起使用。
11.1 排他网关
排他网关:只有一条路径会被选择。
11.2 并行网关
并行网关:所有路径会被同时选择。
与排他网关的主要区别是,并行网关不会解析条件。即使顺序流中定义了条件,也会被忽略。
场景:
请假申请开始,需要部门经理和总经理都审批,两者没有前后需要两个人全部审批才能进入下个节点人事审批。这个时候就需要并行网关
11.3 包含网关
包容网关:可以同时执行多条线路,也可以在网关上设置条件,可以看做是排他网关和并行网关的结合体。
场景:
请假申请大于等于2天需要由部门总经理审批,小于2天由部门经理审批,请假申请必须经过人事经理审批。这个时候就需要包含网关