续接上一篇文章,在上一篇文章中,我们完成了activiti7基础的学习包括25张表、类关系图、流程符号、画流程图、部署流程、启动流程实例、任务查询等,并且使用activiti7提供的API完成了请假申请的流程流转。明白了流程引擎是如何工作的,要想完成企业级的开发,这些知识还是远远不够的,在这一篇文章,我们会深入activiti7的学习,继续探索流程引擎更高级的知识。
对于activiti
流程引擎,除了定义流程、启动流程实例、任务信息查询、完成任务等这些操作外,我们还可以做一些删除、资源下载等一些操作。
查询流程相关信息,包括流程定义、流程部署、流程定义的版本等,注意流程定义信息主要在ACT_RE_PROCDEF
,如果流程定义的key
一样,启动流程实例时,会选择版本号最大的那个定义信息来启动。例如
@Test
public void test() {
// 1.获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 2.获取ProcessDefinitionQuery
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
String processDefinitionKey = "leave";
ProcessDefinition processDefinition = processDefinitionQuery.processDefinitionKey(processDefinitionKey).latestVersion().singleResult();
// 3.输出流程定义信息
logger.info("流程定义id:" + processDefinition.getId());
logger.info("流程定义name:" + processDefinition.getName());
logger.info("流程定义key:" + processDefinition.getKey());
logger.info("流程定义version:" + processDefinition.getVersion());
}
删除流程也需要特别注意,我们使用流程引擎需要定义或部署流程、启动流程实例等,这些数据会写到多张表里面去,删除流程时候要删除那些数据??
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void deleteTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("leave").list();
// 获取流程定义ID
List<String> definitionIdList = definitionList.stream().map(ProcessDefinition::getDeploymentId).collect(Collectors.toList());
logger.info(definitionIdList.toString());
// 通过流程定义ID删除
// void deleteDeployment(String deploymentId, boolean cascade); cascade 是否级联删除
definitionIdList.forEach(item -> repositoryService.deleteDeployment(item, true));
}
}
删除说明:
cascade=true
)ACT_RE_PROCDEF、ACT_RE_DEPLOYMENT、ACT_GE_BYTEARRAY
表关联的数据进行删除我们的流程资源已经上传到数据库(包括bpmn、png等文件)这些文件会以二进制的形式保存到数据库字段,当然我们也可以对这些资源进行操作,例如读到内存里进行下载等一些IO操作。
这里我们主要以下载为例来介绍流程资源的IO操作,针对流程资源的下载我们可以使用activiti
提供的API,也可以自己写下载代码(但是需要解决jdbc
对blob类型,clob类型字段的转换问题)
例如下载至磁盘:
/**
* @description:
* @author: laizhenghua
* @date: 2022/12/31 12:53
*/
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void downloadTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("leave").list();
definitionList.forEach(item -> {
String deploymentId = item.getDeploymentId();
String resourceName = item.getResourceName();
InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
// 下载
File file = new File(String.format("src/main/resources%s%s.xml", File.separator, item.getKey() + "_" + item.getVersion()));
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
write(inputStream, fileOutputStream);
fileOutputStream.flush();
} catch (IOException e) {
logger.error(e.getMessage());
e.printStackTrace();
} finally {
IOUtils.close(fileOutputStream);
}
IOUtils.close(inputStream);
});
}
public void write(InputStream inputStream, OutputStream outputStream) {
byte[] buffer = new byte[4096];
try {
int count = inputStream.read(buffer, 0, buffer.length);
while (count != -1) {
outputStream.write(buffer, 0, count);
count = inputStream.read(buffer, 0, buffer.length);
}
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
我们都知道流程定义相当于一个模板,启动流程实例的时候,通过这个模板启动,因此流程定义与流程实例是一对多的关系。
为了方便区分流程实例,以便做任务查询、完成任务等操作,可用一个字段BUSINESS_KEY_
标识流程实例,即启动流程实例指定这个businessKey
,就可很方便的区分流程实例,对查询个人任务等有极大的意义,例如:
/**
* @description:
* @author: laizhenghua
* @date: 2022/12/31 12:53
*/
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void startInstanceTest() {
RuntimeService runtimeService = processEngine.getRuntimeService();
// 以请假申请流程为例 businessKey就可以是员工的工号或者是学生的学号
// 老王工号=2017001
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave", "2017001");
String processDefinitionId = processInstance.getProcessDefinitionId(); // 流程定义ID
String processInstanceId = processInstance.getProcessInstanceId(); // 流程实例ID
String id = processInstance.getId(); // taskId
String businessKey = processInstance.getBusinessKey(); // 业务/流程标识
logger.info(String.format("流程定义ID=[%s]", processDefinitionId));
logger.info(String.format("流程实例ID=[%s]", processInstanceId));
logger.info(String.format("taskID=[%s]", id));
logger.info(String.format("业务/流程标识=[%s]", businessKey));
}
}
启动流程实例,加上businessKey
后,有字段BUSINESS_KEY_
的表都会写入这个字段值,例如:
SELECT * FROM ACT_RU_EXECUTION E WHERE E.BUSINESS_KEY_ = '2017001'
SELECT * FROM ACT_RU_TASK T WHERE T.BUSINESS_KEY_ = '2017001'
SELECT * FROM ACT_HI_PROCINST P WHERE P.BUSINESS_KEY_ = '2017001'
某些情况可能由于流程变更需要将当前运行的流程暂停而不是直接删除,流程暂停后将不会继续执行。
1、通流程定义ID挂起所有流程即操作流程定义为挂起状态,该流程定义下边所有的流程实例全部暂停,流程定义为挂起状态该流程定义将不允许启动新的流程实例,同时该流程定义下所有的流程实例将全部挂起暂停执行。
/**
* @description:
* @author: laizhenghua
* @date: 2022/12/31 12:53
*/
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void suspendInstanceTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
List<ProcessDefinition> definitionList = repositoryService.createProcessDefinitionQuery().processDefinitionKey("leave").list();
definitionList.forEach(item -> {
if (item.isSuspended()) {
// 挂起即激活
// void activateProcessDefinitionById(String processDefinitionId, boolean activateProcessInstances, Date activationDate);
// processDefinitionId 流程定义ID
// activateProcessInstances 是否激活流程实例
// activationDate 激活时间
repositoryService.activateProcessDefinitionById(item.getId(), true, new Date());
} else {
// 挂起
repositoryService.suspendProcessDefinitionById(item.getId(), true, new Date());
}
});
}
}
2、单个流程实例挂起即操作流程实例对象,针对单个流程执行挂起操作,某个流程实例挂起则此流程不再继续执行,完成该流程实例的当前任务将报异常。
/**
* @description:
* @author: laizhenghua
* @date: 2022/12/31 12:53
*/
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void suspendInstanceTest() {
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceBusinessKey("2017001").singleResult();
if (processInstance.isSuspended()) {
logger.info(String.format("ProcessInstance[id=%s] is activate", processInstance.getProcessInstanceId()));
runtimeService.activateProcessInstanceById(processInstance.getProcessInstanceId());
} else {
logger.info(String.format("ProcessInstance[id=%s] is suspended", processInstance.getProcessInstanceId()));
runtimeService.suspendProcessInstanceById(processInstance.getProcessInstanceId());
}
}
}
尝试完成挂起的任务:
@Test
public void completeTaskTest() {
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery().processInstanceBusinessKey("2017001").singleResult();
boolean suspended = task.isSuspended();
if (suspended) {
logger.info(String.format("Task[id=%s] is suspended", task.getId()));
}
taskService.complete(task.getId());
}
抛出异常
我们都知道,每个流程都有参与者,也就是ASSIGNEE_
字段。在前面基础知识学习中我们也有指定Assignee
,这也是固定分配写法:在进行业务流程建模时指定固定的任务负责人,每一个任务将按照bpmn的配置去分配任务负责人。但是在实际开发中这个变量值一定不会写死,流程的参与者不可能一直固定不变。
因此实际开发中,我们应该使用站位符(即表达式)指定流程参与者,动态的分配任务负责人。Activiti
支持两个UEL表达式分别是UEL-value
和UEL-method
,UEL 是Java EE6
规范的一部分, UEL(Unified Expression Language)即统一表达式语言。
1、UEL-value
,如下图
另外表达式还可以写为${user.assignee}
的形式,和Assignee一样,user也是activiti的一个流程变量,user.assignee
表示通过user的getter方法获取值。例如:
/**
* @description:
* @author: laizhenghua
* @date: 2022/12/31 12:53
*/
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void processDeploymentTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deploy = repositoryService.createDeployment().addClasspathResource("bpmn/leave.bpmn")
.name("出差申请(UEL)").deploy();
logger.info(String.format("流程定义ID=%s", deploy.getId()));
}
@Test
public void processStart() {
RuntimeService runtimeService = processEngine.getRuntimeService();
Map<String, Object> variables = new HashMap<>();
variables.put("assignee0", "alex");
variables.put("assignee1", "zs");
variables.put("assignee2", "ls");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave_uel", variables);
logger.info(String.format("流程实例ID=%s", processInstance.getProcessInstanceId()));
}
}
执行成功后,可以在ACT_RU_VARIABLE
表中看到刚才map中的数据
SELECT * FROM ACT_RU_VARIABLE V WHERE V.PROC_INST_ID_ = '412923c2-89be-11ed-ad64-080058000005'
2、UEL-method
,如下图
UserBean
是spring容器中的一个bean
实例,表示调用该bean
的getUserName()
方法。
3、UEL-value
和UEL-method
相结合
再比如:
${userService.getUserByLoginName(username)}
/*
userService spring容器的一个bean实例
getUserByLoginName 该bean的一个方法
username 可以是activiti的流程变量 做为参数传到getUserByLoginName()方法中
*/
除了上面两种固定分配和表达式分配任务负责人外,Activiti
还可以使用监听器分配流程参与者,监听器不仅可以分配流程参与者,还可以完成很多Activiti
流程的业务。
/*
Event的选项包含:
Create 任务创建后触发
Assignment 任务分配后触发
Delete 任务完成后触发
All 所有事件发生都触发
*/
所谓监听器就是发生对应的任务相关事件执行自定义的Java逻辑或表达式,使用监听器后就不需要再指定Assignee
了,完全可以在监听器中设置Assignee
,例如:
1、创建自定义的监听器,必须实现org.activiti.engine.delegate.TaskListener
接口:
/**
* @description:
* @author: laizhenghua
* @date: 2023/1/1 19:10
*/
public class ActTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
if ("create".equals(delegateTask.getEventName()) && "班长审批".equals(delegateTask.getName())) {
delegateTask.setAssignee("alex");
}
}
}
2、流程修改(添加监听器):
3、定义流程
@Test
public void processDeploymentTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deploy = repositoryService.createDeployment().addClasspathResource("bpmn/test.bpmn")
.name("出差申请(UEL)").deploy();
logger.info(String.format("流程定义ID=%s", deploy.getId()));
}
4、启动流程,输出监听器为我们设置的Assignee
@Test
public void processStart() {
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("test", "2023");
logger.info(String.format("流程实例ID=%s", processInstance.getProcessInstanceId()));
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery().processInstanceBusinessKey("2023").singleResult();
logger.info(String.format("当前环节参与者=%s", task.getAssignee()));
}
流程变量在Activiti
中是一个非常重要的角色,流程运转需要流程变量,业务系统和Activiti
结合时少不了流程变量的参与,流程变量就是Activiti
在管理工作流是根据管理需要而设置的变量。
比如说:出差报销流程,如果报销金额大于1W,则由总裁办审核,否则由人事直接审核,报销金额就可以设置为流程变量,在流程运转时使用,从而丰富我们的流程。
那么既然是变量,在Java
中肯定是有类型的,在Activiti
中又支持那些类型变量呢?
Type | Description |
---|---|
String | 与Java数据类型一致 |
Integer | 与Java数据类型一致 |
short | 与Java数据类型一致 |
Long | 与Java数据类型一致 |
Double | 与Java数据类型一致 |
Date | 与Java数据类型一致 |
Binary |
二进制数据 |
Serializable |
Java对象 |
如果将实体存储到流程变量中,必须实现序列化接口Serializable
,为了防止由于新增字段而无法反序列化,还需要生成SerialVersionUID
。
在Activiti
中流程变量的作用域可以是一个流程实例(ProcessInstance)、一个任务(Task)、或一个执行实例(Execution)。
当然不同的作用域也有不同的叫法。
1、Global
变量
流程变量的默认作用域是流程实例。当一个流程变量的作用域为流程实例时,可以称为Global
变量。需要注意的是:Global
变量中变量名不允许重复,如果设置了相同名称的变量,后设置的值会覆盖前面设置的变量值。
2、Local
变量
任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大,称为Local
变量。Local
变量由于在不同的任务或不同的执行实例中,作用域互不影响,变量名可以相同不会造成啥影响。
1、在属性上使用UEL
表达式
UEL
前面我们也有介绍到,比如:${assignee},assignee就是一个流程变量。
2、在连线上使用UEL
表达式
可以在连线上设置UEL
表达式,决定流程的走向。比如:${price < 10000},price就是一个流程变量名称,UEL
表达式结果类型为布尔类型。
3、使用示例
需要注意的是一个环节连两条线时,两条线的Condition
都不能为空。
如上图报销流程,假设报销金额超过10000的时候需要运营总监审核,这时候又该如何实现呢?
/**
* @description:
* @author: laizhenghua
* @date: 2022/12/31 12:53
*/
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void globalVariableTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
// 报销流程定义/部署
Deployment deploy = repositoryService.createDeployment().key("test").name("报销申请")
.addClasspathResource("bpmn/test.bpmn").deploy();
String key = deploy.getKey();
logger.info(String.format("报销流程ID=%s", deploy.getId()));
// 设置流程变量
Map<String, Object> variables = new HashMap<>();
variables.put("money", 12000); // 报销金额
variables.put("assignee0", "fqz"); // 发起者
variables.put("assignee1", "zjl"); // 总经理
variables.put("assignee2", "yyzj"); // 运营总监
variables.put("assignee3", "cw"); // 财务
// 启动流程
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, "bx", variables);
logger.info(String.format("流程实例ID=%s", processInstance.getProcessInstanceId()));
}
}
完成任务(一直执行完成任务代码,直至走完所有环节):
@Test
public void completeTest() {
String processInstanceId = "0082a793-98d2-11ed-9d75-080058000005";
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
if (task != null) {
taskService.complete(task.getId());
} else {
logger.info(String.format("<---- process [processInstanceId=%s] ---->", processInstanceId));
}
}
流程全生命周期查询:
SELECT
AI.PROC_DEF_ID_ AS 流程定义ID,
AI.PROC_INST_ID_ AS 流程实例ID,
AI.TASK_ID_ AS 任务ID,
AI.ACT_NAME_ AS 环节名称,
AI.ACT_TYPE_ AS 环节类型,
AI.ASSIGNEE_ AS 环节负责人,
AI.START_TIME_ AS 开始时间,
AI.END_TIME_ AS 结束时间
FROM ACT_HI_ACTINST AI WHERE AI.PROC_INST_ID_ = '0082a793-98d2-11ed-9d75-080058000005' ORDER BY AI.START_TIME_ ASC
以上是流程实例启动时指定流程变量,除了这种方式外Activiti
还可以在任务办理时、当前流程实例(流程未结束)、当前任务设置流程变量。
任务办理时设置变量
在完成任务时设置流程变量,改流程变量只有在该任务完成后其他节点才可以使用该变量,它的作用域是整个流程实例,如果设置的流程变量的key
在流程实例中存在相同的名字则后设置的变量会替换前面设置的变量。
我们还是以上面流程为例(流程实例ID=30005,下一环节是总经理且办理人是zjl
,对应assignee1
变量):
完成任务时我们改变assignee1
的变量值,例如:
@Test
public void completeTaskTest() {
String processInstanceId = "30005";
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
if (task != null) {
// 重新指定流程变量
Map<String, Object> variables = new HashMap<>();
variables.put("assignee1", "zjl666"); // 总经理
taskService.complete(task.getId(), variables, true); // 任务办理时重新指定变量(其他节点生效)
} else {
logger.info(String.format("<---- process [processInstanceId=%s] is end ---->", processInstanceId));
}
}
我们再来看总经理环节的任务办理人:
通过当前流程实例设置变量
在Activiti
中也可以通过流程实例设置全局变量,前提是当前流程实例未结束,例如:
@Test
public void processTest() {
String processInstanceId = "30005";
RuntimeService runtimeService = processEngine.getRuntimeService();
String variableName = "assignee1";
Object variable = runtimeService.getVariable(processInstanceId, variableName); // 根据key获取流程变量
logger.info(String.format("设置前: [%s=%s]", variableName, variable));
runtimeService.setVariable(processInstanceId, variableName, "zjl001"); // 重新设置变量
variable = runtimeService.getVariable(processInstanceId, variableName);
logger.info(String.format("设置后: [%s=%s]", variableName, variable));
}
当然通过RuntimeService
操作流程变量的API
不止这些,可自行了解。
通过当前任务设置变量
例如现有taskId=32509
的一条任务
@Test
public void taskTest() {
String taskId = "32509"; // taskId必须有效即在 ACT_RU_TASK 表中存在
TaskService taskService = processEngine.getTaskService();
String variableName = "assignee1";
Object variable = taskService.getVariable(taskId, variableName);
logger.info(String.format("设置前: [%s=%s]", variableName, variable));
taskService.setVariable(taskId, variableName, "zjl7777");
variable = taskService.getVariable(taskId, variableName);
logger.info(String.format("设置后: [%s=%s]", variableName, variable));
}
与RuntimeService
类似,操作例程变量的API
不止是这个,一次可设置多个变量等,可自行了解。
注意事项:
根据以往的学习来看,如果任务节点的ASSIGNEE_
值写死,也就是说流程定义时将参与者固定设置在.bpmn
文件中,如果临时负责人变更则需要修改流程定义,需要重新启动流程实例,这显然是不合理的,也不可能这样。
针对这种情况Activiti
可以给任务设置多个候选人(最终只能由一个人来完成此任务),可以从候选人中选择某个参与者来完成任务,例如(设置candidateUsers
属性就是指定候选人,多个候选人之间使用英文逗号隔开):
设置好后,我们再来看流程引擎是如何运转的
1、请假流程定义并启动流程实例
/**
* @description:
* @author: laizhenghua
* @date: 2022/12/31 12:53
*/
@SpringBootTest
public class ActTest {
private Logger logger = LoggerFactory.getLogger(ActTest.class);
@Autowired
private ProcessEngine processEngine;
@Test
public void candidateTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
// 请假流程定义
Deployment deploy = repositoryService.createDeployment().key("test").name("请假申请")
.addClasspathResource("bpmn/test.bpmn").deploy();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploy.getId()).singleResult();
String definitionId = processDefinition.getId();
logger.info(String.format("请假流程ID=%s", definitionId));
// 启动流程
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceById(definitionId, "qj");
logger.info(String.format("请假流程实例ID=%s", processInstance.getProcessInstanceId()));
}
}
2、候选人任务查询
@Test
public void completeTaskTest() {
String processInstanceId = "47505"; // 流程实例ID
TaskService taskService = processEngine.getTaskService();
// 注意这里使用bzr1或bzr2都能查出此任务
List<Task> list = taskService.createTaskQuery().processInstanceId(processInstanceId)
.taskCandidateUser("bzr1").list();
logger.info(list.toString());
}
踩坑指南:如果activiti-spring-boot-starter
版本是7.x +
版本的,因为默认集成了security
,因此security
上下文中还需有查询指定的用户也就是bzr1
和bzr2
(用户名相等即可即username=bzr1)否则会抛出Cause: org.springframework.security.core.userdetails.UsernameNotFoundException: bzr1
异常,解决方案如下:
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
String[][] usersGroupsAndRoles = {{"system", "password", "ROLE_ACTIVITY_USER"},
{"bzr1", "password", "ROLE_ACTIVITY_USER"},
{"bzr2", "password", "ROLE_ACTIVITY_USER"},
{"admin", "password", "ROLE_ACTIVITY_ADMIN"}
};
for (String[] user : usersGroupsAndRoles) {
List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]),
authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
}
return inMemoryUserDetailsManager;
}
3、拾取任务
@Test
public void completeTaskTest() {
String processInstanceId = "47505"; // 流程实例ID
String userId = "bzr1";
TaskService taskService = processEngine.getTaskService();
// 注意这里使用bzr1或bzr2都能查出此任务
List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId)
.taskCandidateUser(userId).list();
logger.info(taskList.toString());
// 拾取任务(API为claim())
taskList.forEach(task -> {
taskService.claim(task.getId(), userId);
});
// 查询当前任务负责人
Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
logger.info(String.format("Assignee=%s", task.getAssignee()));
// 拾取成功后验证bzr2是否还可以查询到当前任务
task = taskService.createTaskQuery().processInstanceId(processInstanceId).taskCandidateUser("bzr2").singleResult();
assert task != null;
logger.info(String.format("bzr2查到一任务,任务id=%s", task.getId()));
}
其中一个拾取任务成功后,另外一个人就查询不到此任务了。
前面流程变量我们已经可以实现通过连线的方式去控制流程的走向,显然网关也是做这个事情,只是网关可以实现更灵活的流程控制如可以几个分支都走,也可以几个分支都走完以后统一的结束,也可以选择走某一条分支这都是连线达不到的效果。
排他网关:
exclusive gateway
经常使用流程变量决定流程下一步要选择的路径如下图,排他网关使用也很简单,一般需要配合流程变量,判断连线分支是否为true
,需要注意的是不用排他网关也能实现此场景的分支控制,只是使用排他网关后即使条件都不满足也会默认走第一条分支,而condition
条件如果条件都不满足,流程就异常结束了。
并行网关允许将流程分成(fork)多条分支,也可以把多条分支汇聚(join)到一起,并行网关的功能是基于进入和外出顺序流的。因此使用并行网关也有前提条件,就是说流程必须基于出口顺序流和入口顺序流。
分支(fork): 并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
合并(join): 所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。
注意:
如果并行网关同时具有入口顺序流和出口顺序流,并行网关会先执行入口顺序流,然后执行再分裂成多条可以执行的路径。
并行网关不执行计算条件。并行网关上面的计算条件会被忽略。
并行网关分支和合并是同时存在的,就是说一个至少有一个分支和一个合并。但是网关是可以不平衡的,分支和合并的数量可以不一致。
如下图,并行网关内部是一个加号。
如下流程图,流程引擎又该如何运转呢?
1、启动流程实例
@Test
public void gatewayTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
// 预审流程定义
Deployment deploy = repositoryService.createDeployment().key("test").name("预审").addClasspathResource("bpmn/test.bpmn").deploy();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploy.getId()).singleResult();
String definitionId = processDefinition.getId();
logger.info(String.format("预审流程ID=%s", definitionId));
// 启动流程
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceById(definitionId, "ys");
logger.info(String.format("预审流程实例ID=%s", processInstance.getProcessInstanceId()));
}
2、得到流程实例id
后我们完成窗口环节的任务,观察窗口后的环节会有几条任务?
@Test
public void completeTaskTest() {
String processInstanceId = "75005";
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
logger.info(String.format("activityName=%s", task.getName()));
taskService.complete(task.getId()); // 完成窗口环节任务
List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list();
taskList.forEach(taskItem -> {
logger.info(String.format("activityName=%s,taskId=%s,assignee=%s", taskItem.getName(), taskItem.getId(), taskItem.getAssignee()));
});
}
输出情况(窗口任务完成后,生成了3条任务)
并行网关在业务应用中常用于会签任务,会签任务即多个参与者共同办理的任务。也就是说A部门会审、B部门会审、C部门会审任务全部完成后,才可以走到下一环节,可自行测试下,包括数据库中到底操作了那些表、那些数据。
什么是包容网关?
包容网关(Inclusive Gateway)就是并行网关(Parallel Gateway)和排他网关(Exclusive Gateway)的组合。可以在出口顺序流上定义条件,包容网关会计算它们。然而主要的区别是,包容网关与并行网关一样,可以选择多于一条(出口)顺序流。
包容网关、并行网关和排他网关的异同:
同:
异:
排他网关只有一条分支被执行,如果有多条符合条件的分支,流程会默认走第一条。并行网关至少有一条分支被执行,而且所有的分支都会被执行。包容网关有多条或者一条分支会被执行。
包容网关包括了并行网关和排他网关的所有功能。
使用场合:
不确定分支的情形下面。
例如:
审批部门领导根据不同的条件确定。
审批的会签根据不同的条件确定。
分支(fork):所有出口顺序流都会被计算,对于计算为true的分支都会被执行。
聚合(join):所有到达包容网关的并行执行,都会在网关处等待,直到每一条具有流程标志的入口顺序流,都有一个执行到达。这是与并行网关的重要区别。换句话说,包容网关只会等待将会被执行的入口顺序流。在合并后,流程穿过合并并行网关继续。
包容网关测试:如下图,给定一个流程变量xs,包容网关的执行逻辑为xs < 100走A部门会审和B部门会审,xs > 100走B部门会审和C部门会审。
1、流程定义
@Test
public void gatewayTest() {
RepositoryService repositoryService = processEngine.getRepositoryService();
// 预审流程定义
Deployment deploy = repositoryService.createDeployment().key("test").name("预审").addClasspathResource("bpmn/test.bpmn").deploy();
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploy.getId()).singleResult();
String definitionId = processDefinition.getId();
logger.info(String.format("预审流程ID=%s", definitionId));
// 设置流程变量
Map<String, Object> variables = new HashMap<>();
variables.put("xs", 99);
// 启动流程
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceById(definitionId, "ys", variables);
logger.info(String.format("预审流程实例ID=%s", processInstance.getProcessInstanceId()));
}
2、完成任务(拿到流程实例id
后先完成窗口环节任务,在完成余下环任务)
@Test
public void completeTaskTest() {
String processInstanceId = "82505";
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
logger.info(String.format("activityName=%s", task.getName()));
taskService.complete(task.getId()); // 完成窗口环节任务
boolean isEnd = false;
while (!isEnd) {
List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list();
if (CollectionUtils.isEmpty(taskList)) {
isEnd = true;
} else {
taskList.forEach(taskItem -> {
logger.info(String.format("activityName=%s,taskId=%s,assignee=%s", taskItem.getName(), taskItem.getId(), taskItem.getAssignee()));
taskService.complete(taskItem.getId());
});
}
}
}
3、再来看当xs=99时,流程流转是否按照我们预想的一样。