背景
某项目流程使用activiti开发,现需要开发一个流程预测的功能,流程预测,也称流程预跑,是指用户在发起流程或者执行审批动作时希望看到流程后续流转的节点,方便用户跟踪流程。Activiti本身不提供流程预测的功能,实际上流程在运行时每一个变量的变化,比如审批结果,表单数据等,都会影响流程的走向,而这些变化是无法进行预见的,所以流程预测的前提条件就是,我们需要假设一些变量的值,比较典型的就是审批结果,我们需要假设审批结果都是通过的,基于这个前提,我们才能完成一个可以提供参考价值的预测数据。
实现
如果要看懂本文,你必须对activiti开发有基本的了解,对一些基本的概念熟悉,建议先阅读之前发表的Activiti教程
流程模型
这边准备了一个简单的报销流程模型示例
流程很简单,提交报销单,项目负责人先审批,如果金额小于500,项目经理审批,如果金额大于500,项目总监审批,最后财务审核,财务如果拒绝直接退回项目经理。其中判断金额的表达式为${amount >500}
,判断财务审核结果的表达式为${outcome=='REJECT'}
,并且财务审核这个节点的审批人是一个变量${finApprover}
实现思路
方案就是获取到流程模型,代码根据流程模型进行计算流程流转路径,实际审批人,这里需要解决两个问题
- 流程变量的计算
- 流程模型和节点流向信息的获取
- 参数的获取
activiti都有提供相应的api进行获取,为了模拟计算表达式需要在流程中设置变量,这些一般都是在审批代码中动态设置,比如outcome变量,一般都是根据用户的审批结果进行动态设置,这里为了方便模拟,所有参数都是通过流程发起接口传入,在流程发起时预先设置好的
流程发起
下面是流程发起的接口代码
@RequestMapping(value = "start", method = RequestMethod.GET)
public String start(@RequestParam(value = "processId") String processId, @RequestParam Map params) {
ProcessInstance instance = runtimeService.startProcessInstanceByKey(process, params);
return instance.getId();
}
接口接受一个流程id和参数,并且发起时会把参数设置到流程实例中,流程发起需要借助runtimeService
流程预测
这边创建了一个bean用于存储每个预测节点信息
ApproveNode.java
public class ApproveNode {
private String nodeName;
private String approvers;
public ApproveNode(String nodeName, String approvers) {
this.nodeName = nodeName;
this.approvers = approvers;
}
//getter and setter
//....
}
预测的主函数如下
@Service
public class PreviewProcessService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private RepositoryService repositoryService;
public List getPreviewNodes(String taskId) {
/**
* 获取待办任务信息
*/
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
//获取流程模型
BpmnModel model = repositoryService.getBpmnModel(task.getProcessDefinitionId());
//获取当前节点
FlowElement flowElement = model.getFlowElement(task.getTaskDefinitionKey());
//获取流程变量
Map params = runtimeService.getVariables(task.getExecutionId());
//保存访问过的节点,避免死循环
Set visitedElements = new HashSet<>();
//递归获取所有预测节点
List approveNodes = visiteElement(flowElement, params, visitedElements);
return approveNodes;
}
//....
}
- 这里是根据待办id(taskId)获取到流程模型,再获取流程运行时变量进行计算,如果是流程发起时的预跑,直接根据流程id获取模型,流程变量从前端表单传入即可
- visitedElements为了避免死循环,因为程序需要根据流程连线获取信息进行计算,如果流程本身存在循环,比如上面的例子,财务审批退回给项目经理,项目经理提交后又可以回到财务审核,如果处理不当,就容易出现死循环
- 这里使用递归进行节点计算,核心逻辑在
visiteElement
中
完整代码
import com.definesys.totorial.activiti.dto.ApproveNode;
import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import org.activiti.bpmn.model.*;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.task.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import java.util.*;
/**
* @Description:
* @author: jianfeng.zheng
* @since: 2021/1/28 12:40 上午
* @history: 1.2021/1/28 created by jianfeng.zheng
*/
@Service
public class PreviewProcessService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private RepositoryService repositoryService;
public List getPreviewNodes(String taskId) {
/**
* 获取代办任务信息
*/
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
//获取流程模型
BpmnModel model = repositoryService.getBpmnModel(task.getProcessDefinitionId());
//获取当前节点
FlowElement flowElement = model.getFlowElement(task.getTaskDefinitionKey());
//获取流程变量
Map params = runtimeService.getVariables(task.getExecutionId());
//保存访问过的节点,避免死循环
Set visitedElements = new HashSet<>();
//递归获取所有预测节点
List approveNodes = visiteElement(flowElement, params, visitedElements);
return approveNodes;
}
/**
* 递归获取预测节点列表
*
* @param flowElement
* @param params
* @param visitedElements
* @return
*/
private List visiteElement(FlowElement flowElement, Map params, Set visitedElements) {
String id = flowElement.getId();
//如果之前访问过的节点就不再访问
if (visitedElements.contains(id)) {
return Collections.EMPTY_LIST;
}
visitedElements.add(id);
List nodes = new ArrayList<>();
//UserTask是实际的审批节点,如果是UserTask就可以加入到预测的节点中
if (flowElement instanceof UserTask) {
UserTask item = (UserTask) flowElement;
nodes.add(new ApproveNode(item.getName(), this.executeExpression(item.getAssignee(), params, String.class)));
}
//获取所有的出口,也就是流程模型中的连线
List sequenceFlows = this.getElementSequenceFlow(flowElement);
if (sequenceFlows == null || sequenceFlows.isEmpty()) {
return nodes;
}
FlowElement nextElement = null;
if (sequenceFlows.size() == 1 && sequenceFlows.get(0).getConditionExpression() == null) {
/**
* 如果只有一条连线并且没有设置流转条件,直接获取连线目标节点作为下一节点
*/
nextElement = sequenceFlows.get(0).getTargetFlowElement();
} else {
for (SequenceFlow seq : sequenceFlows) {
if (seq.getConditionExpression() == null) {
/**
* 如果没有条件符合,那么就取获取到的第一条条件为空的节点
*/
if (nextElement == null) {
nextElement = seq.getTargetFlowElement();
}
} else {
/**
* 计算条件
*/
boolean value = this.verificationExpression(seq.getConditionExpression(), params);
if (value) {
nextElement = seq.getTargetFlowElement();
break;
}
}
}
}
nodes.addAll(this.visiteElement(nextElement, params, visitedElements));
return nodes;
}
/**
* 获取流程连线
*
* @param flowElement
* @return
*/
private List getElementSequenceFlow(FlowElement flowElement) {
if (flowElement instanceof FlowNode) {
return ((FlowNode) flowElement).getOutgoingFlows();
}
return Collections.EMPTY_LIST;
}
/**
* 执行表达式计算
* @param expression
* @param variableMap
* @param returnType
* @param
* @return
*/
private T executeExpression(String expression, Map variableMap, Class returnType) {
if (expression == null) {
return null;
}
ExpressionFactory factory = new ExpressionFactoryImpl();
SimpleContext context = new SimpleContext();
for (String k : variableMap.keySet()) {
context.setVariable(k, factory.createValueExpression(variableMap.get(k), Object.class));
}
ValueExpression valueExpression = factory.createValueExpression(context, expression, returnType);
return (T) valueExpression.getValue(context);
}
/**
* 验证表达式结果 true/false
* @param expression
* @param variableMap
* @return
*/
private Boolean verificationExpression(String expression, Map variableMap) {
Boolean value = this.executeExpression(expression, variableMap, Boolean.class);
return value == null ? false : value;
}
}
测试
流程发起
curl 'http://localhost:8888/activiti/start?processId=simple&amount=1000&outcome=APPROVE&finApprover=helen'
- amount:金额设置1000
- outcome:APPROVE
- finApprover:财务审核
获取待办信息
{
"id": "20019",
"name": "项目负责人"
}
获取预测信息
$ curl http://localhost:8888/activiti/preview\?taskId\=20019
[
{
"nodeName": "项目负责人",
"approvers": "001"
},
{
"nodeName": "项目总监",
"approvers": "003"
},
{
"nodeName": "财务审核",
"approvers": "helen"
}
]
因为金额超过500,所以是走项目总监审批的逻辑,并且财务审核也是根据变量获取,将金额改为100后结果
[
{
"nodeName": "项目负责人",
"approvers": "001"
},
{
"nodeName": "项目经理",
"approvers": "002"
},
{
"nodeName": "财务审核",
"approvers": "helen"
}
]