通用拒绝
从这章开始,就正式进入activiti的实战开发,使用activiti实现各种审批动作,包括一些中国式流程操作,比如回退,征询等,这些操作activiti的标准功能是没有的,但因为activiti不算复杂,也比较灵活,因此可以通过一些技巧或者变通的方法实现,这章就讨论通用拒绝
的实现。为什么叫通用拒绝,因为在activiti里,正常的拒绝都是通过连接线加条件判断实现,你可以定义一个变量如outcome,拒绝的时候给这个变量赋值REJECT
,在连接线上设置条件表达式从而实现拒绝操作。如图:
项目经理拒绝到发起人的表达式为${outcome=='REJECT'}
,在流程里设置好变量就能实现拒绝操作:
taskService.setVariable(taskId, "outcome", "approve");
taskService.complete(taskId);
这种拒绝实现方式优点是简单,标准支持,灵活性强,能够从任意节点拒绝回任意节点,但缺点也是明显的
- 一般流程每个节点都有可能拒绝,那就意味着每个节点都需要设置判断条件,如果都要拒绝回发起人,那么都要跟发起人节点进行连接,如果节点多的话会大大增加流程图的复杂度,让流程图变成一张“蜘蛛网”。
因此我们需要一个通用拒绝的功能,需求是,在任意节点拒绝后自动回到发起人节点,发起人重新提交后流程重新开始。
那么面临的两个问题是
- 流程图中没有发起人节点,怎么造出这个发起人节点
- 流程已经在流转中了,如何重新流转
我们依次解决以上两个问题
发起人节点处理
activiti提供动态修改流程模型的api,但修改流程模型后全局生效,所有的流程都会受影响,因此就算我们能通过代码“造出”发起人几点,也是不可取的。其实仔细想想,我们是需要一个发起人节点,还是需要一个审批人是发起人的节点,显然,我们的需求是后面那个,明白了这个道理后,问题就变得简单了,如何让当前节点的审批人变成发起人,方案可以是这样的:
- 删除当前节点所有的待办,只保留一个待办
- 将保留下来的那个待办审批人设置为发起人
通过以上两个步骤我们可以实现拒绝后将待办转移到发起人那里,当然为了在流程里能够获取到发起人,你应该在流程发起的时候将发起人信息存储到变量中。
那么问题又来了,我们这是将当前节点伪造成了发起人节点,但假的毕竟是假的,等发起人一审批,就露馅了,因为流程会继续往下走,那么为了达到以假乱真的地步,我们要继续完成以下两件事
- 审批接口需要知道当前节点的审批是否是“伪造”的发起人节点
- 如果审批接口知道了当前节点的审批是发起人发起的,那么就需要将流程重新拨回到第一个节点
第一个需求可以通过设置一个变量进行标识,第二个需求是我们的下一个议题。
拒绝实现代码参考:
public TaskResponse reject(TaskResponse task, String user) {
//删除所有当前task,保留一个,并且将该task的审批人设为发起人
//设置reject标志
Task t = taskService.createTaskQuery()
.taskId(task.getTaskId())
.singleResult();
String instanceId = t.getProcessInstanceId();
List tasks = taskService.createTaskQuery()
.processInstanceId(instanceId)
.list();
Task luckyTask = tasks.get(0);
managementService.executeCommand(new ExecutionVariableDeleteCmd(t.getExecutionId()));
for (int i = 1; i < tasks.size(); ++i) {
managementService.executeCommand(new TaskDeleteCmd(tasks.get(i).getId()));
managementService.executeCommand(new ExecutionVariableDeleteCmd(tasks.get(i).getExecutionId()));
}
//将发起人设置为当前审批人
taskService.setAssignee(luckyTask.getId(), (String) taskService.getVariable(luckyTask.getId(), "submitter"));
//设置变量标识当前状态是已拒绝状态
taskService.setVariable(luckyTask.getId(), "status", "reject");
return this.taskResponse(t, instanceId);
}
审批的代码参考如下:
String status = (String) taskService.getVariable(taskId, "status");
if ("reject".equals(status)) {
//发起人重新发起
this.rollbackFirstask(task, user);
} else {
//正常审批
taskService.complete(taskId, taskParams);
}
流程重新发起
剩下最后一个问题,就算上面的代码中rollbackFirstask
如何实现,该方法将流程拨回到第一个节点重新开始,在讨论实现之前,我们需要了解下命令模式,也是设计模式中的一种,其实并不陌生,我们可以在日常的开发中就用到了,但并不知道原来这个还有一个专门的名字。简单说就像Linux下的shell脚本,调用一个个命令一样,将每个独立的操作封装成一个命令(Command),由命令调用者(Command Executor)进行调用,每个命令只负责自己的业务逻辑,不与其他命令交互,上下文信息(Command Context)由命令调用者提供。activiti就是采用命令模式对流程资源进行操作,比如删除一个任务,会有一个DeleteTaskCmd的命令类。activiti命令声明如下:
public interface Command {
T execute(CommandContext commandContext);
}
命令调用者执行命令的execute方法,命令可以通过commandContext获取上下文,commandContext里包含了对所有资源的管理类。了解了命令模式后,我们就可以开始执行我们的回滚方案了,具体方案步骤:
-
- 清除现场,清除所有中间过程的变量
-
- 找到开始节点,调用api将流程拨回到开始节点
实现代码如下:
/**
* 流程回退到第一个节点
*
* @param context
* @param request
* @param user
* @return
*/
public TaskResponse rollbackFirstask(String taskId, String user) {
//移除标记REJECT的status
taskService.removeVariable(taskId, "status");
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
//删除任务
managementService.executeCommand(new TaskDeleteCmd(request.getTaskId()));
//删除变量
managementService.executeCommand(new ExecutionVariableDeleteCmd(task.getExecutionId()));
//将流程回滚到第一个节点
managementService.executeCommand(new FlowToFirstCmd(task));
return this.taskResponse(task.getProcessInstanceId());
}
几个命令的实现如下:
TaskDeleteCmd
import org.activiti.engine.impl.cmd.NeedsActiveTaskCmd;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.*;
import java.util.List;
/**
* @Copyright: Shanghai Definesys Company.All rights reserved.
* @Description:
* @author: jianfeng.zheng
* @since: 2019/9/24 6:09 PM
* @history: 1.2019/9/24 created by jianfeng.zheng
*/
public class TaskDeleteCmd extends NeedsActiveTaskCmd {
public TaskDeleteCmd(String taskId) {
super(taskId);
}
@Override
public String execute(CommandContext commandContext, TaskEntity currentTask) {
TaskEntityManagerImpl taskEntityManager = (TaskEntityManagerImpl) commandContext.getTaskEntityManager();
ExecutionEntity executionEntity = currentTask.getExecution();
taskEntityManager.deleteTask(currentTask, "reject", false, false);
return executionEntity.getId();
}
}
ExecutionVariableDeleteCmd
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.VariableInstanceEntity;
import org.activiti.engine.impl.persistence.entity.VariableInstanceEntityManager;
import java.util.List;
/**
* @Copyright: Shanghai Definesys Company.All rights reserved.
* @Description:
* @author: jianfeng.zheng
* @since: 2019/9/24 6:10 PM
* @history: 1.2019/9/24 created by jianfeng.zheng
*/
public class ExecutionVariableDeleteCmd implements Command {
private String executionId;
public ExecutionVariableDeleteCmd(String executionId) {
this.executionId = executionId;
}
@Override
public String execute(CommandContext commandContext) {
VariableInstanceEntityManager vm = commandContext.getVariableInstanceEntityManager();
List vs = vm.findVariableInstancesByExecutionId(this.executionId);
for (VariableInstanceEntity v : vs) {
vm.delete(v);
}
return executionId;
}
}
FlowToFirstCmd
import com.definesys.mpaas.common.exception.MpaasBusinessException;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.task.Task;
import java.util.List;
/**
* @Copyright: Shanghai Definesys Company.All rights reserved.
* @Description:
* @author: jianfeng.zheng
* @since: 2019/9/25 12:36 AM
* @history: 1.2019/9/25 created by jianfeng.zheng
*/
public class FlowToFirstCmd implements Command {
private Task task;
public FlowToFirstCmd(Task task) {
this.task = task;
}
@Override
public String execute(CommandContext context) {
FlowElement startNode = this.getFirstNode(this.task, context);
ExecutionEntity executionEntity = context.getExecutionEntityManager().findById(task.getExecutionId());
executionEntity.setCurrentFlowElement(startNode);
context.getAgenda().planTakeOutgoingSequenceFlowsOperation(executionEntity, true);
return executionEntity.getId();
}
private FlowElement getFirstNode(Task task, CommandContext context) {
HistoryService historyService = context.getProcessEngineConfiguration().getHistoryService();
HistoricActivityInstance startNode = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.activityType("startEvent")
.singleResult();
if (startNode == null) {
throw new MpaasBusinessException("未找到开始节点");
}
RepositoryService repositoryService = context.getProcessEngineConfiguration().getRepositoryService();
org.activiti.bpmn.model.Process process = repositoryService.getBpmnModel(task.getProcessDefinitionId()).getMainProcess();
FlowElement node = process.getFlowElement(startNode.getActivityId());
return node;
}
}
总结
其实,稍微改造下FlowToFirstCmd命令,就能将流程路由到任意节点,一开始我们也想靠这个实现任意节点路由的功能,但仔细一想里面的坑非常多,遇到子流程,并行审批等复杂的流程时,会产生很多矛盾点,想想也就放弃了。