文档
Activiti 用户手册 (jeecg.com)
https://g.yuque.com/mrdeer/activiti_note/
http://doc.javaex.cn/activiti
项目中流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 我的申请-applyList 发起申请:选择流程「启用且有权限」,填写流程关联表单数据。保存表单数据+业务申请表 待提交申请 提交: 编辑: 编辑表单数据 删除: 删除表单数据、删除业务申请数据 处理中 撤回 查看进度 表单数据 结束或撤回 已撤回:重新申请、编辑 已结束:审批历史、表单数据、审批历史 我的待办-todoManage 我的已办-doneManage
运行中的流程-processInsManage 挂起、激活、事件信息、操作过程、删除 已结束流程-processFinishManage 事件信息、操作过程、删除
|
子流程
内嵌子流程
子流程(Sub-process)是一个包含其他节点,网关,事件等等的节点。 它自己就是一个流程,同时是更大流程的一部分。 子流程是完全定义在父流程里的 (这就是为什么叫做内嵌子流程)
子流程有两种主要场景:
- 子流程可以使用继承式建模。 很多建模工具的子流程可以折叠, 把子流程的内部细节隐藏,显示一个高级别的端对端的业务流程总览。
- 子流程会创建一个新的事件作用域。 子流程运行过程中抛出的事件,可以被子流程边缘定义的 边界事件捕获, 这样就可以创建一个仅限于这个子流程的事件作用范围。
使用子流程要考虑如下限制:
- 子流程只能包含一个空开始事件, 不能使用其他类型的开始事件。子路程必须 至少有一个结束节点。注意,BPMN 2.0 规范允许忽略子流程的 开始和结束节点,但是当前 activiti 的实现并不支持。
- 顺序流不能跨越子流程的边界。
事件子流程
启动事件
定时开始事件
描述
定时开始事件用来在指定的时间创建流程实例。 它可以同时用于只启动一次的流程 和应该在特定时间间隔启动多次的流程。
注意:子流程不能使用定时开始事件。
注意:定时开始事件在流程发布后就会开始计算时间。 不需要调用 startProcessInstanceByXXX,虽然也而已调用启动流程的方法, 但是那会导致调用 startProcessInstanceByXXX 时启动过多的流程。
注意:当包含定时开始事件的新版本流程部署时, 对应的上一个定时器就会被删除。这是因为通常不希望自动启动旧版本流程的流程实例。
图形标记
定时开始事件显示为了一个圆圈,内部是一个表。

XML 内容
定时开始事件的 XML 内容是普通开始事件的声明,包含一个定时定义子元素。 请参考定时定义 查看配合细节。
示例:流程会启动 4 次,每次间隔 5 分钟,从 2011 年 3 月 11 日,12:13 开始计时。
1 2 3 4 5
| <startEvent id="theStart"> <timerEventDefinition> <timeCycle>R4/2011-03-11T12:13/PT5M</timeCycle> </timerEventDefinition> </startEvent>
|
示例:流程会根据选中的时间启动一次。
1 2 3 4 5
| <startEvent id="theStart"> <timerEventDefinition> <timeDate>2011-03-11T12:13:14</timeDate> </timerEventDefinition> </startEvent>
|
消息开始事件
描述
消息开始事件可以用其使用一个命名的消息来启动流程实例。 这样可以帮助我们使用消息名称来选择正确的开始事件。
在发布包含一个或多个消息开始事件的流程定义时,需要考虑下面的条件:
- 消息开始事件的名称在给定流程定义中不能重复。流程定义不能包含多个名称相同的消息开始事件。 如果两个或以上消息开始事件应用了相同的事件,或两个或以上消息事件引用的消息名称相同,activiti 会在发布流程定义时抛出异常。
- 消息开始事件的名称在所有已发布的流程定义中不能重复。 如果一个或多个消息开始事件引用了相同名称的消息,而这个消息开始事件已经部署到不同的流程定义中, activiti 就会在发布时抛出一个异常。
- 流程版本:在发布新版本的流程定义时,之前订阅的消息订阅会被取消。 如果新版本中没有消息事件也会这样处理。
启动流程实例,消息开始事件可以使用 下列 RuntimeService
中的方法来触发:
1 2 3
| ProcessInstance startProcessInstanceByMessage(String messageName); ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables); ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey, Map<String, Object< processVariables);
|
异常事件
- 异常事件不能独立存在,必须是其他事件的子流程
数据库表详解
表 |
意义 |
备注 |
ACT_EVT_LOG |
事件处理日志 |
|
ACT_GE_BYTEARRAY |
二进制数据表 |
存储流程定义相关的部署信息。即流程定义文档的存放地。每部署一次就会增加两条记录,一条是关于 bpmn 规则文件的,一条是图片的(如果部署时只指定了 bpmn 一个文件,activiti 会在部署时解析 bpmn 文件内容自动生成流程图)。两个文件不是很大,都是以二进制形式存储在数据库中。 |
ACT_GE_PROPERTY |
主键生成表 |
主张表将生成下次流程部署的主键 ID。 |
ACT_HI_ACTINST |
历史节点表 |
只记录 usertask 内容,某一次流程的执行一共经历了多少个活动 |
ACT_HI_ATTACHMENT |
历史附件表 |
|
ACT_HI_COMMENT |
历史意见表 |
|
ACT_HI_DETAIL |
历史详情表,提供历史变量的查询 |
流程中产生的变量详细,包括控制流程流转的变量等 |
ACT_HI_IDENTITYLINK |
历史流程人员表 |
|
ACT_HI_PROCINST |
历史流程实例表 |
|
ACT_HI_TASKINST |
历史任务实例表 |
一次流程的执行一共经历了多少个任务 |
ACT_HI_VARINST |
历史变量表 |
|
ACT_PROCDEF_INFO |
|
|
ACT_RE_DEPLOYMENT |
部署信息表 |
存放流程定义的显示名和部署时间,每部署一次增加一条记录 |
ACT_RE_MODEL |
流程设计模型部署表 |
流程设计器设计流程后,保存数据到该表 |
ACT_RE_PROCDEF |
流程定义数据表 |
存放流程定义的属性信息,部署每个新的流程定义都会在这张表中增加一条记录。注意:当流程定义的 key 相同的情况下,使用的是版本升级 |
ACT_RU_EVENT_SUBSCR |
throwEvent,catchEvent 时间监听信息表 |
|
ACT_RU_EXECUTION |
运行时流程执行实例表 |
历史流程变量 |
ACT_RU_IDENTITYLINK |
运行时流程人员表 |
主要存储任务节点与参与者的相关信息 |
ACT_RU_INTEGRATION |
|
|
ACT_RU_JOB |
运行时定时任务数据表 |
|
ACT_RU_TIMER_JOB |
|
|
ACT_RU_SUSPENDED_JOB |
|
|
ACT_RU_TASK |
运行时任务节点表 |
|
ACT_RU_TIMER_JOB |
|
|
ACT_RU_VARIABLE |
运行时流程变量数据表 |
通过 JavaBean 设置的流程变量,在 act_ru_variable 中存储的类型为 serializable,变量真正存储的地方在 act_ge_bytearray 中。 |
ACT_ID_GROUP |
用户组信息表 |
已废弃 |
ACT_ID_INFO |
用户扩展信息表 |
已废弃 |
ACT_ID_MEMBERSHIP |
用户与用户组对应信息表 |
已废弃 |
ACT_ID_USER |
用户信息表 |
已废弃 |
UEL 表达式
- activiti 支持两个 UEL 表达式:UEL-value 和 UEL-method
- 用户任务节点:受理人、候选人、候选人组;流转条件表达式
UEL-value
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // 一般流程变量 ${acceptor}
// user 为流程变量,对象类型,acceptor 为成员变量,且提供了 getter 方法。实现了序列化 ${user.acceptor}
// userBean 为 Spring Bean 对象, 调用 getName() 方法获取 ${userBean.getName()}
// userBean 为 Spring Bean 对象,userId 为流程变量,getAcceptor 为 userBean 的方法 ${userBean.getAcceptor(userId)}
// 网关条件表达式:一般流程变量 // 单个流程变量 ${flag == true} ${flag == 1 || flag == 2} ${flag == "1" || flag == "2"} // 多个流程变量共同决定:流程中必须设置流程变量,即使为null,否则报错 ${flag1 == "1" && flag2 == "2"}
// 网关条件表达式:流程变量 user 为对象 ${user.name == "zhangsan"} ${order.price > 100 && order.price < 250}
|
uel-value 方式设置条件表达式,支持 String、int、boolean、Object 对象(序列化的),Map,List,Array
uel-method
1
| ${userBean.getAcceptor(userId)}
|
设置节点的执行人为 ${userBean.getAcceptor (userId)} ,其中 userBean 是我们注入到 spring 中的一个类对象,userId 是我们设置的流程变量
https://blog.csdn.net/simplemurrina/article/details/79635085
流程模型
使用RepsoitoryService
接口
创建、部署、查询、删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
| package boot.deer.note.process.deployment;
import java.io.InputStream; import java.util.List; import java.util.zip.ZipInputStream;
import org.activiti.engine.RepositoryService; import org.activiti.engine.repository.Deployment; import org.activiti.engine.repository.DeploymentBuilder; import org.activiti.engine.repository.DeploymentQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil;
public class ProcessDeploymentNote extends SpringBootActivitiApplicationTests {
@Autowired private RepositoryService repositoryService;
@Test public void processDeploymentUseClasspathResource() { DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
String bpmnResourcePath = "processes/note/leave/LeaveProcess.bpmn"; String pngResourcePath = "processes/note/leave/LeaveProcess.png"; String processName = "请假流程";
Deployment deployment = deploymentBuilder.name(processName).addClasspathResource(bpmnResourcePath) .addClasspathResource(pngResourcePath).deploy();
System.out.println("流程部署成功,流程部署ID:" + deployment.getId()); }
@Test public void processDeploymentUseZipResource() { String zipPath = "/processes/note/leave/LeaveProcess.zip";
InputStream resourceAsStream = this.getClass().getResourceAsStream(zipPath);
Deployment deployment = repositoryService.createDeployment().name("请假流程") .addZipInputStream(new ZipInputStream(resourceAsStream)).deploy();
System.out.println("流程部署成功,流程部署ID:" + deployment.getId()); }
@Test public void processDeploymentQuery() { DeploymentQuery deploymentQuery = repositoryService.createDeploymentQuery();
List<Deployment> deployments = deploymentQuery
.list();
deployments.forEach(deployment -> { System.out.println("流程部署 ID: " + deployment.getId()); System.out.println("流程部署 Name: " + deployment.getName()); System.out.println("流程部署 Time: " + DateUtil.dateTime2String(deployment.getDeploymentTime())); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); }
@Test public void processDeploymentDelete() { String deploymentId = "2501";
repositoryService.deleteDeployment(deploymentId, true); System.out.println("删除成功"); } }
|
流程定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
| package boot.deer.note.process.definition;
import java.util.List;
import org.activiti.engine.RepositoryService; import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.repository.ProcessDefinitionQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests;
public class ProcessDefinitionNote extends SpringBootActivitiApplicationTests {
@Autowired private RepositoryService repositoryService;
@Test public void processDefinitionQuery() { ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> processDefinitions = processDefinitionQuery
.list();
processDefinitions.forEach(definition -> { System.out.println("流程部署 ID: " + definition.getDeploymentId()); System.out.println("流程定义 ID: " + definition.getId()); System.out.println("流程定义 Key: " + definition.getKey()); System.out.println("流程定义 名称: " + definition.getName()); System.out.println("流程定义 版本:" + definition.getVersion()); System.out.println("流程资源 BPMN 文件名:" + definition.getResourceName()); System.out.println("流程资源 PNG 文件名:" + definition.getDiagramResourceName()); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); }
@Test public void processDefinitionQueryFinal() { ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> processDefinitions = processDefinitionQuery.latestVersion().list();
processDefinitions.forEach(definition -> { System.out.println("流程部署 ID: " + definition.getDeploymentId()); System.out.println("流程定义 ID: " + definition.getId()); System.out.println("流程定义 Key: " + definition.getKey()); System.out.println("流程定义 名称: " + definition.getName()); System.out.println("流程定义 版本:" + definition.getVersion()); System.out.println("流程资源 BPMN 文件名:" + definition.getResourceName()); System.out.println("流程资源 PNG 文件名:" + definition.getDiagramResourceName()); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); }
@Test public void processDefinitionQueryView() { String viwePath = System.getProperty("user.dir") + "\\view";
File folder = new File(viwePath); if (!folder.exists()) folder.mkdir();
viwePath += "\\LeaveProcess.png"; System.out.println(viwePath);
File file = new File(viwePath);
String processDefinitionId = "LeaveProcess:1:4"; try (InputStream inputStream = repositoryService.getProcessDiagram(processDefinitionId); BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file))) { int len; byte[] buf = new byte[1024]; while ((len = inputStream.read(buf)) != -1) { outputStream.write(buf, 0, len); outputStream.flush(); } System.out.println("视图读取完成"); } catch (Exception e) { e.printStackTrace(); System.out.println("视图读取失败"); } } }
|
挂起、激活流程定义
1 2 3 4 5 6 7
| repositoryService.suspendProcessDefinitionByKey("scm"); repositoryService.suspendProcessDefinitionById(id, true, new Date());
repositoryService.activateProcessDefinitionById(id, true, new Date()); repositoryService.activateProcessDefinitionByKey("scm", true, new Date());
|
流程实例
- 使用
RuntimeService
接口
- 启动流程我们可以使用”流程定义 ID 或者流程定义 Key 进行启动。更推荐使用 key 的方式进行启动,如果在一个流程有多个版本的情况下(因为有版本升级),使用 key 的方式启动,依然启动的是最新的版本流程
- 通过流程实例是查询不到执行实例
- 查询执行实例,会把「流程实例」也查询出来了
流程实例-启动、查询、指定发起人
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
| package boot.deer.note.process.start;
import java.util.List;
import org.activiti.engine.RuntimeService; import org.activiti.engine.runtime.ProcessInstance; import org.activiti.engine.runtime.ProcessInstanceQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil;
public class ProcessInstanceNote extends SpringBootActivitiApplicationTests {
@Autowired private RuntimeService runtimeService;
@Test public void startProcessByDefinitionId() { String processDefinitionId = "LeaveProcess:1:4"; identityService.setAuthenticatedUserId(loginUser.getUsername()); ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinitionId); System.out.println("流程启动成功,流程实例ID:" + processInstance.getProcessInstanceId()); }
@Test public void startProcessByKey() { String processDefinitionKey = "LeaveProcess"; identityService.setAuthenticatedUserId(loginUser.getUsername()); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey); System.out.println("流程启动成功,流程实例ID:" + processInstance.getProcessInstanceId()); }
@Test public void processInstanceQuery() { ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery();
List<ProcessInstance> processInstances = processInstanceQuery
.list();
processInstances.forEach(processInstance -> { System.out.println("执行实例 ID: " + processInstance.getId()); System.out.println("流程实例 ID: " + processInstance.getProcessInstanceId()); System.out.println("流程定义 ID: " + processInstance.getDeploymentId()); System.out.println("流程启动时间: " + DateUtil.dateTime2String(processInstance.getStartTime())); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); } }
|
挂起、激活流程实例
1 2 3 4 5
| runtimeService.suspendProcessInstance("piid");
runtimeService.activateProcessInstanceById(id);
|
执行实例-查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package boot.deer.note.process.start;
import java.util.List;
import org.activiti.engine.RuntimeService; import org.activiti.engine.runtime.Execution; import org.activiti.engine.runtime.ExecutionQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests;
public class ExecutionNote extends SpringBootActivitiApplicationTests {
@Autowired private RuntimeService runtimeService;
@Test public void executionQuery() { ExecutionQuery executionQuery = runtimeService.createExecutionQuery();
List<Execution> executions = executionQuery
.list();
executions.forEach(execution -> { System.out.println("执行实例ID: " + execution.getId()); System.out.println("执行实例Name: " + execution.getName()); System.out.println("父节点ID: " + execution.getParentId()); System.out.println("流程实例ID: " + execution.getProcessInstanceId()); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); } }
|
活动或任务
- 使用 TaskService 接口
- 整个流程结束后,在「ACT_RU_」表中的数据就被移除,存档在历史表中
个人任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
| package boot.deer.note.task.assignee;
import java.util.List;
import org.activiti.engine.TaskService; import org.activiti.engine.task.Task; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil;
public class AssigneeTaskNote extends SpringBootActivitiApplicationTests {
@Autowired private TaskService taskService;
@Test public void assigneeTaskQuery() { List<Task> tasks = taskService.createTaskQuery()
.list(); List<Task> tasks = taskService.createNativeTaskQuery() .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T WHERE T.NAME_ = #{taskName}") .parameter("taskName", "gonzoTask") .list();
long count = taskService.createNativeTaskQuery() .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T1, " + managementService.getTableName(VariableInstanceEntity.class) + " V1 WHERE V1.TASK_ID_ = T1.ID_") .count();
tasks.forEach(task -> { System.out.println("任务ID: " + task.getId()); System.out.println("流程实例ID: " + task.getProcessInstanceId()); System.out.println("执行实例ID: " + task.getExecutionId()); System.out.println("任务办理人: " + task.getAssignee()); System.out.println("任务Name: " + task.getName()); System.out.println("任务创建时间: " + DateUtil.dateTime2String(task.getCreateTime())); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); }
@Test public void completeTaskById() { String taskId = "7502"; Map<String, Object> variableMap = new HashMap<>(16); variableMap.put("money", 360);
taskservice.complete(taskId, variableMap);
System.out.println("任务办理成功"); }
@Test public void completeTaskById() { String taskId = "7502"; taskService.delegateTask(taskId, "张三"); taskService.setOwner(taskId, "admin"); } }
|
组任务
- 指定多个候选人,任务在多个人可见,可以由其中一个人进行任务的办理
- 组任务的核心就是任务的拾取,在没有领取之前,就没有具体的办理人
- 任务查询、拾取、回退、候选人新增、删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
| package boot.deer.note.task.group;
import java.util.List;
import org.activiti.engine.TaskService; import org.activiti.engine.task.IdentityLink; import org.activiti.engine.task.Task; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil; import boot.deer.component.util.GlobalUtil;
public class GroupTaskNote extends SpringBootActivitiApplicationTests {
@Autowired private TaskService taskService;
@Test public void taskQueryByAssignee() { String assignee = "userA";
List<Task> tasks = taskService.createTaskQuery().taskAssignee(assignee).list();
if (GlobalUtil.isEmpty(tasks)) System.out.println("当前办理人的任务为空"); else { tasks.forEach(task -> { System.out.println("任务ID: " + task.getId()); System.out.println("流程实例ID: " + task.getProcessInstanceId()); System.out.println("执行实例ID: " + task.getExecutionId()); System.out.println("任务办理人: " + task.getAssignee()); System.out.println("任务Name: " + task.getName()); System.out.println("任务创建时间: " + DateUtil.dateTime2String(task.getCreateTime())); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); } }
@Test public void groupTaskQueryByTaskId() { String taskId = "15005"; List<IdentityLink> identityLinks = taskService.getIdentityLinksForTask(taskId);
identityLinks.forEach(identity -> { System.out.println("用户ID: " + identity.getUserId()); System.out.println("任务ID: " + identity.getTaskId()); System.out.println("任务实例ID: " + identity.getProcessInstanceId()); System.out.println("任务类型: " + identity.getType()); System.out.println("= = = = = = = = = = = = = = = = = = = = = = = ="); }); }
@Test public void taskClaim() { String taskId = "15005"; String userId = "userA";
taskService.claim(taskId, userId); System.out.println(userId + ":任务拾取成功"); }
@Test public void taskRollback() { String taskId = "15005";
taskService.claim(taskId, null); System.out.println("任务回退成功");
}
@Test public void completeTaskById() { String taskId = "15005"; taskService.complete(taskId);
System.out.println("任务办理成功"); }
@Test public void addCrew() { String taskId = "15005"; String userId = "userD"; taskService.addCandidateUser(taskId, userId);
System.out.println(userId + ":成员添加成功"); }
@Test public void deleteCrew() { String taskId = "15005"; String userId = "userD"; taskService.deleteCandidateUser(taskId, userId);
System.out.println("组员删除成功"); } }
|
服务任务
有 4 种方法来声明 java 调用逻辑:
监听类:实现 JavaDelegate 或 ActivityBehavior
1
| activiti:class="org.activiti.MyJavaDelegate"
|
1 2 3 4 5 6 7
| public class ToUppercase implements JavaDelegate { public void execute(DelegateExecution execution) throws Exception { String var = (String) execution.getVariable("input"); var = var.toUpperCase(); execution.setVariable("input", var); } }
|
委托表达式:delegateExpressionBean
是一个实现了 JavaDelegate
接口的 bean, 它定义在实例的 spring 容器中
1
| activiti:delegateExpression="${delegateExpressionBean}"
|
方法表达式
名为 printer
对象上的方法 printMessage
。 第一个参数是 DelegateExecution
,在表达式环境中默认名称为 execution
。 第二个参数传递的是当前流程的名为 myVar
的变量
1 2 3
| activiti:expression="#{printer.printMessage()}" 或者 activiti:expression="#{printer.printMessage(execution, myVar)}"
|
值表达式
split 为 Spring 容器中 bean,ready 为成员变量,存在 getter 方法「无参」
1
| activiti:expression="#{split.ready}"
|
流程变量
- 流程变量可以帮助我们进行动态的注入”办理人”、”组员”等等,同时还可以控制流程的走向,根据变量来判断流程该流出到哪一条分支等等
- 流程变量的属性结构就是Map,key 对应 value 的键值队
- 流程变量可以用在办理人、连接线条件等
- 流程变量我们之前都是存放的时
String
类型的变量,同样的,我们也可以存放不同的类型,java中基本的数据类型,应用数据类型等 。int、long、char、string、Java 对象
- 在数据表中有一个字段就是用来存放数据类型的。如果我们想存放对象,同样也是可以的,但存放的对象必须实现序列化的接口,否则就会报错
流程变量- Process Variable
设置「启动流程/完成任务时」、变量修改、获取变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| package boot.deer.note.variable.type;
import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.zip.ZipInputStream;
import org.activiti.engine.RepositoryService; import org.activiti.engine.RuntimeService; import org.activiti.engine.repository.Deployment; import org.activiti.engine.runtime.ProcessInstance; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests;
public class VariableTypeNote extends SpringBootActivitiApplicationTests {
@Autowired private RepositoryService repositoryService; @Autowired private RuntimeService runtimeService;
@Test public void deploymentProcess() { String zipPath = "/processes/note/leave/LeaveProcess.zip";
InputStream resourceAsStream = this.getClass().getResourceAsStream(zipPath);
Deployment deployment = repositoryService.createDeployment().name("请假流程") .addZipInputStream(new ZipInputStream(resourceAsStream)).deploy();
System.out.println("流程部署成功,流程部署ID:" + deployment.getId()); }
@Test public void startProcessWithVariable() { String processDefinitionKey = "LeaveProcess"; Map<String, Object> variableMap = new HashMap<>(); variableMap.put("days", 5); variableMap.put("description", "约会");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey, variableMap); System.out.println("流程启动成功,流程实例ID:" + processInstance.getProcessInstanceId()); }
@Test public void changeVariable() { String executionId = "37501"; Map<String, Object> variableMap = new HashMap<>(16); variableMap.put("days", 3);
runtimeService.setVariables(executionId, variableMap); System.out.println("变量放置成功"); }
@Test public void objectVariable() { String executionId = "37501"; Map<String, Object> variableMap = new HashMap<>(16);
UserModel user = new UserModel(); user.setName("Mr_Deer"); user.setAge(24);
variableMap.put("user", user);
runtimeService.setVariables(executionId, variableMap); System.out.println("变量放置成功"); }
@Test public void getVariable() { String executionId = "37501";
String daysKey = "days"; String descriptionKey = "description"; String userKey = "user";
Integer daysValue = (Integer) runtimeService.getVariable(executionId, daysKey); String descriptionValue = (String) runtimeService.getVariable(executionId, descriptionKey); UserModel userValue = (UserModel) runtimeService.getVariable(executionId, userKey);
System.out.println("请假天数:" + daysValue); System.out.println("请假理由:" + descriptionValue); System.out.println("用户对象:" + userValue.toString()); } }
|
连线-Sequence Flow
连线就是我们节点与节点直接的连接线,他明确了我们流程的流向。可以通过流程变量控制分支情况中不同流向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| package boot.deer.note.variable.sequence;
import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.zip.ZipInputStream;
import org.activiti.engine.RepositoryService; import org.activiti.engine.RuntimeService; import org.activiti.engine.TaskService; import org.activiti.engine.repository.Deployment; import org.activiti.engine.runtime.ProcessInstance; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests;
public class SequenceFlowNote extends SpringBootActivitiApplicationTests { @Autowired private RepositoryService repositoryService; @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService;
@Test public void processDeployment() { String path = "/processes/note/file/version_2/FileProcess.zip"; InputStream inputStream = this.getClass().getResourceAsStream(path);
String processName = "文件审批流程"; Deployment deployment = repositoryService.createDeployment().name(processName) .addZipInputStream(new ZipInputStream(inputStream)).deploy();
System.out.println("流程部署成功,流程部署ID:" + deployment.getId()); }
@Test public void processStartByKey() { String processDefinitionKey = "FileProcess"; ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey);
System.out.println("流程启动成功,流程实例ID:" + processInstance.getProcessInstanceId()); }
@Test public void completeTaskById() { String taskId = "30003"; taskService.complete(taskId);
System.out.println("任务办理成功"); }
@Test public void completeTaskByIdWithVariable() { String taskId = "27502"; Map<String, Object> variableMap = getVariableMap("isImportant", "重要"); taskService.complete(taskId, variableMap);
System.out.println("任务办理成功"); }
private Map<String, Object> getVariableMap(String key, Object value) { HashMap<String, Object> variableMap = new HashMap<>(16); variableMap.put(key, value);
return variableMap; } }
|
历史记录
- 使用
HistoryService
接口
- 历史流程表存放的全部都是已经完成的”流程实例”、”任务实例”、”流程变量”等等
- 当一条流程执行完毕后,
ACT_RU_*
表中数据都会被抹除,这是因为全部都存放在历史流程表中了 ACT_HI_PROCINST
历史流程 - History Process
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| package boot.deer.note.history.process;
import java.util.List;
import org.activiti.engine.HistoryService; import org.activiti.engine.history.HistoricProcessInstance; import org.activiti.engine.history.HistoricProcessInstanceQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil;
public class HistoryProcessNote extends SpringBootActivitiApplicationTests {
@Autowired private HistoryService historyService;
@Test public void historyProcessQuery() { HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
List<HistoricProcessInstance> historicProcessInstances = historicProcessInstanceQuery
.list();
historicProcessInstances.forEach(processInstance -> { System.out.println("历史流程 ID: " + processInstance.getId()); System.out.println("流程定义 ID: " + processInstance.getDeploymentId()); System.out.println("历史流程开始时间: " + DateUtil.dateTime2String(processInstance.getStartTime())); System.out.println("历史流程结束时间: " + DateUtil.dateTime2String(processInstance.getEndTime())); System.out.println( "流程持续周期时间(:秒): " + DateUtil.milliseconds2Second(processInstance.getDurationInMillis()) + "s"); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); } }
|
历史任务-History Task
查询接口:historyService.createHistoricTaskInstanceQuery()
我们在执行流程任务的时候,每一个任务都会在执行的时候就同步到历史表中了,这也就保证了每条任务的记录都会存在在历史表中,为我们后面查询带来很多的便捷
历史任务的查询表是 ACT_HI_TASKINST
,里面主要的数据是:任务名称、任务办理人、开始时间、结束时间、持续时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| package boot.deer.note.history.task;
import java.util.List;
import org.activiti.engine.HistoryService; import org.activiti.engine.history.HistoricTaskInstance; import org.activiti.engine.history.HistoricTaskInstanceQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil;
public class HistoryTaskNote extends SpringBootActivitiApplicationTests {
@Autowired private HistoryService historyService;
@Test public void historyTaskQuery() { HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
List<HistoricTaskInstance> historicTaskInstances = historicTaskInstanceQuery
.list();
historicTaskInstances.forEach(historyTask -> { System.out.println("历史任务ID:" + historyTask.getId()); System.out.println("历史任务执行实例ID:" + historyTask.getExecutionId()); System.out.println("历史任务实例ID:" + historyTask.getProcessInstanceId()); System.out.println("历史任务定义ID:" + historyTask.getProcessDefinitionId()); System.out.println("历史任务开始时间:" + DateUtil.dateTime2String(historyTask.getStartTime())); System.out.println("历史任务结束时间:" + DateUtil.dateTime2String(historyTask.getEndTime())); System.out.println("历史任务名称:" + historyTask.getName()); System.out.println("历史任务办理人:" + historyTask.getAssignee()); System.out.println( "历史任务办理持续周期时间(:秒):" + DateUtil.milliseconds2Second(historyTask.getDurationInMillis()) + "s"); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); } }
|
历史活动节点 - History Activity
- 查询的表
ACT_HI_ACTINST
,其中基本比较重要的属性:节点ID、任务ID、节点Name、节点类型
- 保存的节点只是我们走过的节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package boot.deer.note.history.activity;
import java.util.List;
import org.activiti.engine.HistoryService; import org.activiti.engine.history.HistoricActivityInstance; import org.activiti.engine.history.HistoricActivityInstanceQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil;
public class HistoryActivityNote extends SpringBootActivitiApplicationTests {
@Autowired private HistoryService historyService;
@Test public void historyActivityQuery() { HistoricActivityInstanceQuery historicActivityInstanceQuery = historyService .createHistoricActivityInstanceQuery();
List<HistoricActivityInstance> activityInstances = historicActivityInstanceQuery
.list();
activityInstances.forEach(activity -> { System.out.println("历史活动节点ID:" + activity.getActivityId()); System.out.println("历史活动节点任务ID:" + activity.getTaskId()); System.out.println("历史活动节点Name:" + activity.getActivityName()); System.out.println("历史活动节点类型:" + activity.getActivityType()); System.out.println("历史活动节点办理人:" + activity.getAssignee()); System.out.println("历史活动节点开始时间:" + DateUtil.dateTime2String(activity.getStartTime())); System.out.println("历史活动节点结束时间:" + DateUtil.dateTime2String(activity.getEndTime())); System.out.println( "历史活动节点办理持续周期时间(:秒):" + DateUtil.milliseconds2Second(activity.getDurationInMillis()) + "s"); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); } }
|
历史流程变量-History Variable
- 主要字段:变量类型、变量名称、变量值、创建时间、最后一次修改时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| package boot.deer.note.history.variable;
import java.util.List;
import org.activiti.engine.HistoryService; import org.activiti.engine.history.HistoricVariableInstance; import org.activiti.engine.history.HistoricVariableInstanceQuery; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;
import boot.deer.SpringBootActivitiApplicationTests; import boot.deer.component.util.DateUtil;
public class HistoryVariableNote extends SpringBootActivitiApplicationTests {
@Autowired private HistoryService historyService;
@Test public void historyVariableQuery() { HistoricVariableInstanceQuery historicVariableInstanceQuery = historyService .createHistoricVariableInstanceQuery();
List<HistoricVariableInstance> variableInstances = historicVariableInstanceQuery
.list();
variableInstances.forEach(variable -> { System.out.println("历史变量ID:" + variable.getId()); System.out.println("历史流程实例ID:" + variable.getProcessInstanceId()); System.out.println("历史变量Name:" + variable.getVariableName()); System.out.println("历史变量值类型:" + variable.getVariableTypeName()); System.out.println("历史变量值:" + variable.getValue()); System.out.println("历史变量创建时间:" + DateUtil.dateTime2String(variable.getCreateTime())); System.out.println("历史变量最后修改时间:" + DateUtil.dateTime2String(variable.getLastUpdatedTime())); System.out.println(" = = = = = = = = = = = = = = = = = = = = = = = = = "); }); } }
|
网关
排他网关 - Exclusive Gateway
什么是排他网管?
排他网关是工作流中网关的一种。其主要的功能就是判断分流,通过不同的流程变量来判断接下改流向哪一条分支(流程肯定是多分支的)
为什么要使用?之前不是已经使用流程变量来控制了吗?
之前的那种控制很不严谨,并且流程图可读性不高,并且存在一些BUG,并不推荐那种使用方式,我们下面的例子就会体现出”排他网管”的优势
- 排他网关就是一个菱形里面有一个X的就是排他网管
- 一个”排他网关”对应一个以上的顺序流
- 由”排他网关”流出的顺序流都有个”conditionExpression:匹配规则”,该匹配规则都是返回boolean
- “排他网关”只会选择一条结果,执行到”排他网关”时,流程引擎会自动检索网关出口,从上到下检索出一个匹配规则返回 true的分支流进行流出
- 如果设置设立”默认连线”时,那么该流出的连线会最后进行匹配,同时也是默认为 true
- 如果没有设置”默认连线”时,同时也没有匹配的规则,则会抛出异常
并行网关 - Parallel Gateway
- 可以同时的、并行执行的多个流程
- 并行网关就是一个菱形的图标中间一个”+”
- 并行网关包括开始和结束两个节点
- 必须两分支的任务都执行完毕才会判定整个流程结束,有一个没有办理完成都不会判定流程结束
流程启动
流程启动和常规的流程启动方式没有什么区别,重点在于数据库中的数据。
我们看到数据库中出现了三条数据,其中后面的两条的 PARENT_ID_
都是第一条的数据。这也就是我们之前说的,在单分支流程的时候,执行实例就等同于流程实例,而在多分支的时候就不能相提并论的。
- PARENT_ID 为空的就是流程实例
- PARENT_ID 非空的则是执行实例
- 他们都是需要依附一个流程实例来存在的
在任务的数据表中,也看到了存在了两条记录,也就意味着流程一旦启动,所有的任务都会同时的执行
总结
- 一个流程中”流程实例”只有一个,”执行实例”会存在多个
- “并行网关”的功能是基于进入和外出的顺序流
分支(fork):
并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
也就是我们流程已启动,就会执行第一个”并发网关”后面的所有顺序流且并发执行
- 汇聚(join):
- 所有到达”并行网关”,在此等候,直到所有进入顺序流的分支都到达后,流程才会通过”汇聚网关”
监听器 - Listener
我们在流程”办理人”的设置中之前有使用过”变量”来注入每个任务的和更改下次任务的办理人,但不过我们之前是在办理任务的时候才去设置变量的,但这样并不方便,因为我们想更关注于业务逻辑,想单独的有个类来去设置办理人,而 Listener 就是帮助我们来解决这个问题的
我们需要先创建一个 Listener,这个类必须实现 TaskListener
这个类,重写 notify
方法即可。 notify
方法块中就是我们需要关于的这个流程变量(这里是办理人)的业务逻辑了
全局的监听器、连线的监听器、节点的监听器
参考
https://g.yuque.com/torey-4fmbn/hhfum8/wxcr8b
全局监听器「执行监听器」
- 可以捕获的事件有:
- 流程实例的启动和结束
- 选中一条连线
- 节点的开始和结束
- 网关的开始和结束
- 中间事件的开始和结束
- 开始时间结束或结束事件开始
- 实现的接口是
org.activiti.engine.delegate.ExecutionListener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| package io.renren.modules.activiti.Score;
import org.activiti.engine.delegate.DelegateExecution; import org.activiti.engine.delegate.ExecutionListener;
public class ScoreExecutionListener implements ExecutionListener {
@Override public void notify(DelegateExecution execution) throws Exception { String eventName = execution.getEventName(); System.out.println(eventName); if ("start".equals(eventName)) { System.out.println("start========="); }else if ("end".equals(eventName)) { System.out.println("end========="); } else if ("take".equals(eventName)) { System.out.println("take========="); } } }
String getId(); String getProcessInstanceId(); String getEventName();
String getBusinessKey();
String getProcessBusinessKey();
String getProcessDefinitionId();
String getParentId();
String getCurrentActivityId();
String getCurrentActivityName();
String getTenantId();
EngineServices getEngineServices();
|
节点监听器「任务监听器」
- 可以捕获的事件有:
- create:任务创建并设置所有属性后触发。
- assignment:任务分配给一些人时触发。 当流程到达 userTask, assignment 事件 会在 create 事件 之前发生。 这样的顺序似乎不自然,但是原因很简单:当获得 create 时间时, 我们想获得任务的所有属性,包括执行人。
- complete:当任务完成,并尚未从运行数据中删除时触发。
- delete:只在任务删除之前发生。 注意在通过 completeTask 正常完成时,也会执行。
- 实现的接口是
org.activiti.engine.delegate.TaskListener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| package io.renren.modules.activiti.Score;
import org.activiti.engine.delegate.DelegateTask; import org.activiti.engine.delegate.TaskListener;
public class ScoreExecutionListener implements TaskListener {
@Override public void notify(DelegateTask delegateTask) { String eventName = delegateTask.getEventName(); System.out.println(eventName); if ("create".endsWith(eventName)) { System.out.println("create========="); }else if ("assignment".endsWith(eventName)) { System.out.println("assignment========"); }else if ("complete".endsWith(eventName)) { System.out.println("complete==========="); }else if ("delete".endsWith(eventName)) { System.out.println("delete============="); } } }
String getId(); String getName(); void setName(String name); String getDescription(); void setDescription(String description);
int getPriority(); void setPriority(int priority); String getProcessInstanceId(); String getExecutionId(); String getProcessDefinitionId(); void addCandidateUser(String userId); void addCandidateUsers(Collection<String> candidateUsers); void addCandidateGroup(String groupId);
|