使用流程设计器、流程符号, 画出流程图
说明:流程定义文档有两部分组成:
bpmn文件
流程规则文件。在部署后,每次系统启动时都会被解析,把内容封装成流程定义放入项目缓存中。Activiti框架结合这个xml文件自动管理流程,流程的执行就是按照bpmn文件定义的规则执行的,bpmn文件是给计算机执行用的。
展示流程图的图片
在系统里需要展示流程的进展图片,图片是给用户看的。
把流程的资源文件, 进行部署
@Autowired
RepositoryService repositoryService;
/**
* 流程部署(classpath路径加载文件)
*/
@Test
public void testDeployment(){
Deployment deploy = repositoryService.createDeployment()
.name("出差申请流程")
.addClasspathResource("bpmn/evection.bpmn")
.addClasspathResource(("bpmn/evection.png"))
.deploy();
System.out.println(deploy.getId() + " " + deploy.getName());
}
这一步在数据库中将操作三张表:
act_re_deployment(部署对象表)
存放流程定义的显示名和部署时间,每部署一次增加一条记录。
2. act_re_procdef(流程定义表)
存放流程定义的属性信息,部署每个新的流程定义都会在这张表中增加一条记录。
注意:当流程定义的key相同的情况下,使用的是版本升级
将.bpmn和.png压缩成zip格式的文件,使用zip的输入流作部署流程定义
/**
* 流程部署 (zip文件)
*/
@Test
public void testDeploymentByZip() throws IOException {
// 获得上传文件的出入流程
InputStream in = new ClassPathResource("bpmn/evection.zip").getInputStream();
// InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("bpmn/leave.zip");
ZipInputStream zipInputStream = new ZipInputStream(in);
// 获取仓库服务, 从类路径下完成部署
Deployment deployment = repositoryService.createDeployment()
.name("出差流程")
.addZipInputStream(zipInputStream)
.deploy();
System.out.println(deployment.getId() + " " + deployment.getName());
}
/**
* 查询流程定义
*/
@Test
public void testDeploymentQuery(){
List myEvection = repositoryService.createDeploymentQuery()
.processDefinitionKey("myEvection")
.orderByDeploymenTime().desc()
.list();
for (Deployment deployment : myEvection) {
System.out.println(deployment);
}
}
说明
创建流程定义查询对象,可以在ProcessDefinitionQuery上设置查询的相关参数
调用ProcessDefinitionQuery对象的list方法,执行查询,获得符合条件的流程定义列表
由运行结果可以看出:
Key和Name的值为:bpmn文件process节点的id和name的属性值
key属性被用来区别不同的流程定义。
带有特定key的流程定义第一次部署时,version为1。之后每次部署都会在当前最高版本号上加1
Id的值的生成规则为:{processDefinitionKey}:{processDefinitionVersion}:{generated-id}, 这里的generated-id是一个自动生成的唯一的数字
重复部署一次,deploymentId的值以一定的形式变化
/**
* 删除流程部署信息
*/
@Test
public void testDeleteDeployment(){
Deployment deployment = repositoryService.createDeploymentQuery()
.processDefinitionKey("myEvection")
.orderByDeploymenTime().desc()
.list().get(0);
if(deployment!=null){
// 普通删除, 如果当前规则下有正在执行的流程, 则抛出异常
// repositoryService.deleteDeployment(deployment.getId());
// 级联删除, 会删除和当前规则相关的所有信息, 正在执行的信息, 也包括历史信息
repositoryService.deleteDeployment(deployment.getId(), true);
}
}
说明:
因为删除的是流程定义,而流程定义的部署是属于仓库服务的,所以应该先得到RepositoryService
如果该流程定义下没有正在运行的流程,则可以用普通删除。如果是有关联的信息,用级联删除。项目开发中使用级联删除的情况比较多,删除操作一般只开放给超级管理员使用。
/**
* 启动流程
*/
@Test
public void startProcessDemo(){
ProcessInstance p = runtimeService.startProcessInstanceByKey("myEvection");
System.out.println("PId:"+p.getId()+", activitiId:"+p.getActivityId()+", PDId"+p.getProcessDefinitionId());
}
说明:
操作三张表 act_ru_execution
, act_ru_task
, act_ru_identitylink
act_ru_execution表,#正在执行的执行对象表 任务结束的之前只有 一个 变化的字段是act_id
act_ru_task表 #运行时任务节点表
act_ru_identitylink表 # 任务参与者数据表。主要存储当前节点参与者的信息。
查询出流程定义文档。主要查的是图片,用于显示流程用。
/**
* 获取流程定义文档
*/
@Test
public void viewImage() throws IOException {
// 从仓库中找出需要展示的文件
String deploymentId = repositoryService.createDeploymentQuery()
.processDefinitionKey("myEvection")
.orderByDeploymenTime().desc()
.list().get(0)
.getId();
if (StringUtils.isNotBlank(deploymentId)){
List<String> names = repositoryService.getDeploymentResourceNames(deploymentId);
String imageName = null;
for (String name : names) {
if (name.contains(".png")) {
imageName = name;
}
}
System.out.println("imageName: " + imageName);
if (StringUtils.isNotBlank(imageName)) {
File file = new File("d:/" + imageName);
// 通过部署ID和文件名称得到文件的输入流
InputStream in = repositoryService.getResourceAsStream(deploymentId, imageName);
FileUtils.copyInputStreamToFile(in, file);
}
}
}
说明:
/**
* 查询我的个人任务
*/
@Test
public void queryPersonTask(){
String assignee = "jerry";
List<Task> list = processEngine.getTaskService().createTaskQuery().taskAssignee(assignee).list();
System.out.println("==================["+assignee+"]的个人任务列表=======================");
for (Task task : list) {
System.out.println("name: "+task.getName());
System.out.println("createName: "+task.getCreateTime());
System.out.println("assignee: "+task.getAssignee());
}
}
说明:
附加:
在activiti任务中,主要分为两大类查询任务(个人任务和组任务):
/**
* 办理任务
*/
@Test
public void complete(){
String taskId = "9c501a3c-6535-11ec-851a-005056c00001";
// 完成任务
taskService.complete(taskId);
}
说明:
操作的数据库:
ACT_HI_TASKINST
ACT_HIACTINST
ACT_HI_IDENTITYLINK
ACT_RUN_TASK
ACT_RUN_IDENTITYLINK
ACT_RU_EXECUTION UPDATE where ID_ = ? and REV_ = ? 更新REV_ (乐观锁)ACT_ID_(节点实例ID)
ACT_HI_ACTINST UPDATE where ID_ = ? 添加结束时间
ACT_HI_TASKINST UPDATE where ID_ = ? 添加结束时间
ACT_RU_TASK DELETE where ID_ = ? and REV_ = ? 删除完成的任务
通过执行人完成任务
/**
* 通过执行人完成任务
*/
@Test
public void completeByAssignee(){
String assignee = "zhang";
Task task = taskService.createTaskQuery().taskAssignee(assignee).singleResult();
System.out.println("==================["+assignee+"]的个人任务列表=======================");
System.out.println("name: "+task.getName());
System.out.println("createName: "+task.getCreateTime());
System.out.println("assignee: "+task.getAssignee());
String taskId = task.getId();
taskService.complete(taskId);
}
/**
* 查询流程状态
*/
@Test
public void queryProcessState(){
// 通过流程实例ID查询流程实例
String processInstanceId = "54c41798-6532-11ec-a294-005056c00001";
ProcessInstance pi = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();// 返回唯一的结果集
if (pi != null) {
log.info("当前流程在: {}", pi.getActivityId());
} else {
log.info("流程已结束");
}
}
使用 HistoryService
/**
* 查看历史信息
*/
@Test
public void findHistoryInfo() {
List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
.processInstanceId("45001")
.orderByHistoricActivityInstanceStartTime()
.asc()
.list();
for (HistoricActivityInstance historicActivityInstance : list) {
System.out.println(historicActivityInstance.getActivityName());
System.out.println(historicActivityInstance.getAssignee());
System.out.println("----------------------");
}
}
流程实例(ProcessInstance)代表流程定义的执行实例。
一个流程实例包括了所有的运行节点。我们可以利用这个对象来了解当前流程实例的进度等信息。
例如:用户或程序按照流程定义内容发起一个流程,这就是一个流程实例。
流程定义和流程实例的图解:
一个流程定义对应多个流程实例,每个流程实例中内容可能有所不同,为区分不同流程实例,这就需要引入一个标识(BusinessKey)进行区分。
实际环境中,除activiti的表外还会有业务表存储业务数据,如下,一个审批流程需要维护两种表
这就需要吧自己的业务表和Activiti表进行关联,才能真正完成实际的业务。
/**
* 添加业务Key 到Activiti的表
*/
@Test
public void addBusinessKey() {
// 启动流程时,添加BusinessKey
ProcessInstance instance = runtimeService.startProcessInstanceByKey("myEvection", "1001");
System.out.println("流程 " + instance.getProcessDefinitionKey() + " 已经启动,ID:" + instance.getId());
}
流程启动第二个参数可以对应业务表的Id。
效果如下,act_ru_execution
表中
某些情况可能由于流程变更需要将当前运行的流程暂停而不是直接删除,流程将不会继续执行。
操作流程定义为挂起状态,该流程定义下边所有流程实例全部暂停:
流程定义为挂起状态该流程定义将不允许启动新的流程实例,同时该流程定义下所有实例将全部挂起暂停执行。
/**
* 全部流程实例挂起、激活
*/
@Test
public void suspendAllProcessInstance() {
// 查询流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("myEvection").singleResult();
// 获取流程定义Id
String processDefinitionId = processDefinition.getId();
// 获取当前流程定义是否都是挂起状态
boolean suspended = processDefinition.isSuspended();
if (!suspended) {
// 如果激活状态,改为挂起状态,参数1:流程定义id,参数2:是否挂起,参数3:挂起时间
repositoryService.suspendProcessDefinitionById(processDefinitionId, true, null);
} else {
// 如果挂起状态,改为激活状态,参数1:流程定义id,参数2:是否激活,参数3:激活时间
repositoryService.activateProcessDefinitionById(processDefinitionId, true, null);
}
}
修改了act_ru_execution
、act_ru_task
表中SUSPENSION_STATE_状态,激活为1,挂起为2
/**
* 单个流程实例挂起、激活
*/
@Test
public void suspendSingleProcessInstance() {
// 操作单个流程定义使用 runtimeService,获取流程实例对象
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId("45001")
.singleResult();
// 获取流程实例Id
String processInstanceId = processInstance.getId();
// 获取流程实例暂停状态
boolean suspended = processInstance.isSuspended();
if (!suspended) {
// 如果激活状态,改为挂起
runtimeService.suspendProcessInstanceById(processInstanceId);
} else {
// 如果挂起状态,改为激活
runtimeService.activateProcessInstanceById(processInstanceId);
}
}
在进行业务流程模型创建时指定固定的任务负责人,如图:
Activiti使用UEL表达式。UEL是java EE6规范的一部分,UEL(Unified Expression Language)即统一表达式语言,Activiti支持两个UEL表达式:UEL-value 和 UEL-method。
如图:
如图:
UserBean 是Spring容器中的一个Bean,表示调用该Bean的getUserId()方法。
再比如:
${ldapService.findManagerForEmployee(emp)}
ldapService是spring容器的一个bean, findManagerForEmployee 是该bean的一个方法, emp是activiti流程变量,emp 作为参数传到ldapService.findManagerForEmployee方法中。
表达式支持解析基础类型、bean、list、array和map,也可作为条件判断。如下:
${order.price>100&& order.price< 250}
1)定义任务分配流程变量
每个节点执行人使用变量形式,修改后bpmn文件如图:
/**
* 启动流程
*/
@Test
public void startProcessDemo(){
// 设定assignee的值,用来替换UEL表达式
Map<String, Object> variables = new HashMap<>();
variables.put("assignee0", "zhangsan");
variables.put("assignee1", "lisi");
variables.put("assignee2", "wang");
variables.put("assignee3", "zhaoliu");
ProcessInstance p = runtimeService.startProcessInstanceByKey("myEvectionUEL",variables);
System.out.println("PId:"+p.getId()+", activitiId:"+p.getActivityId()+", PDId:"+p.getProcessDefinitionId());
}
可以使用监听器来完成很多Acticiti流程的业务。
在本章使用监听器的方式来指定负责人,那么在流程设计时就不需要执行assignee。
任务监听器是发生对应的任务相关事件时执行自定义java逻辑或表达式。
任务
在节点添加监听器,指定触发时机、监听器全路径名称。如图:
创建监听器类,继承TaskListener
package com.example.demo01_activiti.activiti.listener;
public class MyTaskListener implements TaskListener {
/**
* 指定负责人
* @param delegateTask 保存了当前节点信息
*/
@Override
public void notify(DelegateTask delegateTask) {
// 多个节点使用同一个监听器,通过节点名称区分
if(delegateTask.getName().equals("创建出差申请")){
delegateTask.setAssignee("张三");
} else if (delegateTask.getName().equals("经理审批")) {
delegateTask.setAssignee("李四");
}
}
}
流程变量在activiti中是一个非常重要的角色,流程运转有时需要靠流程变量,业务系统和activiti结合时少不了流程变量,流程变量就是activiti在管理工作流时根据管理需要而设置的变量。比如:在出差申请流程流转时如果出差天数大于3天则由总经理审核,否则由人事直接审核,出差天数就可以设置为流程变量,在流程流转时使用。
注意:虽然流程变量中可以存储业务数据可以通过activiti的api查询流程变量从而实现查询业务数据,但是不建议这样使用,因为业务数据查询由业务系统负责,activiti设置流程变量是为了流程执行需要而创建。
流程变量除支持基本类型外。
如果将pojo存储到流程变量中,必须实现序列化接口serializable,为了防止由于新增字段无法反序列化,需要生成serialVersionUID.
流程变量的作用域可以是一个流程实例processlnstance),或一个任务(task),或一个执行实例(execution)
流程变量的默认作用域是流程实例。当一个流程变量的作用域为流程实例时,可以称为global变量
注意:
如:Global变量:userld(变量名)、zhangsan(变量值)
任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大,称为local变量。
Local变量由于在不同的任务或不同的执行实例中,作用域互不影响,变量名可以相同没有影响。Local变量名也可以和global变量名相同,没有影响。
可以在assignee处设置UEL表达式,表达式的值为任务的负责人,比如:${assignee},assignee就是一个流程变量名称。
Activiti获取UEL表达式的值,即流程变量assignee的值,将assignee的值作为任务的负责人进行任务分配
可以在连线上设置UEL表达式,决定流程走向。
比如:${price<10000}。price就是一个流程变量名称,uel表达式结果类型为布尔类型。
如果UEL表达式是true,要决定流程执行走向。
员工创建出差申请单,由部门经理审核,部门经理审核通过后出差3天及以下由人财务直接审批,3天以上先由总经理审核,总经理审核通过再由财务审批。
启动流程时设置变量
/**
* 启动流程时设置变量
*/
@Test
public void startProcessDemo(){
// 设置流程变量
HashMap<String, Object> variables = new HashMap<>();
variables.put("num", 2);
// 启动流程
ProcessInstance p = runtimeService.startProcessInstanceByKey("myEvectionGlobal",variables);
System.out.println("PId:"+p.getId()+", activitiId:"+p.getActivityId()+", PDId:"+p.getProcessDefinitionId());
}
任务办理时设置变量
/**
* 任务办理时设置变量
*/
@Test
public void complete(){
String taskId = "102502";
// 设置流程变量
HashMap<String, Object> variables = new HashMap<>();
variables.put("num", 2);
// 完成任务
taskService.complete(taskId, variables);
}
通过流程实例设置全局变量,该流程实例必须未执行完成。
/**
* 通过流程实例设置变量
*/
@Test
public void setGlobalVariableByExecutionId(){
String executionId = "20601";
// 通过流程实例id设置流程变量
runtimeService.setVariable(executionId,"num",2);
// 通过map设置多个值
HashMap<String, Object> variables = new HashMap<>();
variables.put("num", 2);
runtimeService.setVariables(executionId,variables);
}
通过当前任务设置
/**
* 通过当前任务设置变量
*/
@Test
public void setGlobalVariableByTaskId(){
String taskId = "20601";
// 通过流程实例id设置流程变量
taskService.setVariable(taskId,"num",2);
// 通过map设置多个值
HashMap variables = new HashMap<>();
variables.put("num", 2);
taskService.setVariables(taskId,variables);
}
也可以通过taskServicel.getVariable()获取流程变量
1、如果UEL表达式中流程变量名不存在则报错。
2、如果UEL表达式中流程变量值为空NULL,流程不按UEL表达式去执行,而流程结束。
3、如果UEL表达式都不符合条件,流程结束
4、如果连线不设置条件,会走flow序号小的那条线
任务办理时设置
任务办理时设置local流程变量,当前运行的流程实例只能在该任务结束前使用,任务结束该变量无法在当前流程实例使用,可以通过查询历史任务查询。
/**
* 任务办理时设置
*/
@Test
public void setGlobalVariableByTaskId(){
String taskId = "20601";
// 通过流程实例id设置流程变量
taskService.setVariableLocal(taskId,"num",2);
taskService.complete(taskId);
}
也可以通过当前任务设置流程变量。
在流程定义中在任务结点的assignee固定设置任务负责人,在流程定义时将参与者固定设置在.bpmn文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性差。
针对这种情况可以给任务设置多个候选人,可以从候选人中选择参与者来完成任务。
在流程图中任务节点的配置中设置candidate-users(候选人),多个候选人之间用逗号分开。
指定候选人,查询该候选人当前的待办任务。
候选人不能立即办理任务。
该组任务的所有候选人都能拾取。
将候选人的组任务,变成个人任务。原来候选人就变成了该任务的负责人。
如果拾取后不想办理该任务?
需要将已经拾取的个人任务归还到组里边,将个人任务变成了组任务。
查询方式同个人任务部分,根据assignee查询用户负责的个人任务。
/**
* 查询组任务
*/
public void findGroupTaskList(){
// 流程定义的key
String key = "testCandidate";
// 任务候选人
String candidateUser = "wangwu";
// 拾取任务
List taskList = taskService.createTaskQuery()
.processDefinitionKey(key)
.taskCandidateUser(candidateUser) // 根据候选人查询任务。
.list();
}
候选人员拾取组任务后该任务变为自己的个人任务。
/**
* 拾取组任务
*/
public void claimTask(){
// 要拾取的任务Id
String taskId = "6302";
// 任务候选人id
String userId = "lisi";
// 拾取任务
// 即使该用户不是候选人也能拾取(建议拾取时校验是否有资格)
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskCandidateUser(userId)
.singleResult();
if(task != null){
// 拾取任务
taskService.claim(taskId, userId);
System.out.printf("任务拾取成功");
}
}
如果个人不想办理该组任务,可以归还组任务,归还后该用户不再是该任务的负责人。
/**
* 归还组任务,由个人任务变为组任务,还可以进行任务交接
*/
public void setAssigneeToGroupTask(){
// 要拾取的任务Id
String taskId = "6302";
// 任务负责人id
String assignee = "lisi";
// 查询任务
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(assignee)
.singleResult();
if(task != null){
// 归还任务,就是把负责人设置为空
taskService.setAssignee(taskId,null);
}
}
/**
* 任务交接
*/
public void setAssigneeToCandidateUser(){
// 要拾取的任务Id
String taskId = "6302";
// 任务负责人id
String assignee = "lisi";
// 将任务交给其他候选人办理该任务
String candidateUser = "zhangsan";
// 查询任务
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(assignee)
.singleResult();
if(task != null){
// 任务交接给其他人
taskService.setAssignee(taskId,candidateUser);
}
}
网关用来控制流程的流向
排他网关,用来在流程中实现决策。当流程执行到这个网关,所有分支都会判断条件是否为true,如果为true则执行该分支,
注意:排他网关只会选择一个为true的分支执行。如果有两个分支条件都为true,排他网关会选择id值较小的一条分支去执行。
为什么要用排他网关?
不用排他网关也可以实现分支,如:在连线的condition条件上设置分支条件。
在连线设置condition条件的缺点:如果条件都不满足,流程就结束了(是异常结束)。
如果使用排他网关决定分支的走向,如下:
如果从网关出去的线所有条件都不满足则系统抛出异常。
并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:
fork分支:
并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
join汇聚:
所有到达并行网关,在此等待的进入分支,直到所有进入顺序流的分支都到达以后,流程就会通过汇聚网关。
注意,如果同一个并行网关有多个进入和多个外出顺序流,它就同时具有分支和汇聚功能。这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。
与其他网关的主要区别是,并行网关不会解析条件。即使顺序流中定义了条件,也会被忽略。
例子:
包含网关可以看做是排他网关和并行网关的结合体。和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。
包含网关的功能是基于进入和外出顺序流的:
|分支:
所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行,会为每个顺序流创建一个分支。
|汇聚:
所有并行分支到达包含网关,会进入等待状态,直到每个包含流程token的进入顺序流的分支都到达。这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。在汇聚之后,流程会穿过包含网关继续执行。
定义流程:
注意:通过包含网关的每个分支的连线上设置condition条件。