背景

某我的项目流程应用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    //....}

预测的主函数如下

@Servicepublic class PreviewProcessService {    @Autowired    private RuntimeService runtimeService;    @Autowired    private TaskService taskService;    @Autowired    private RepositoryService repositoryService;    public List<ApproveNode> getPreviewNodes(String taskId) {        /**         * 获取待办工作信息         */        Task task = taskService.createTaskQuery()                .taskId(taskId)                .singleResult();        //获取流程模型        BpmnModel model = repositoryService.getBpmnModel(task.getProcessDefinitionId());        //获取以后节点        FlowElement flowElement = model.getFlowElement(task.getTaskDefinitionKey());        //获取流程变量        Map<String, Object> params = runtimeService.getVariables(task.getExecutionId());        //保留拜访过的节点,防止死循环        Set<String> visitedElements = new HashSet<>();        //递归获取所有预测节点        List<ApproveNode> 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 */@Servicepublic class PreviewProcessService {    @Autowired    private RuntimeService runtimeService;    @Autowired    private TaskService taskService;    @Autowired    private RepositoryService repositoryService;    public List<ApproveNode> getPreviewNodes(String taskId) {        /**         * 获取代办工作信息         */        Task task = taskService.createTaskQuery()                .taskId(taskId)                .singleResult();        //获取流程模型        BpmnModel model = repositoryService.getBpmnModel(task.getProcessDefinitionId());        //获取以后节点        FlowElement flowElement = model.getFlowElement(task.getTaskDefinitionKey());        //获取流程变量        Map<String, Object> params = runtimeService.getVariables(task.getExecutionId());        //保留拜访过的节点,防止死循环        Set<String> visitedElements = new HashSet<>();        //递归获取所有预测节点        List<ApproveNode> approveNodes = visiteElement(flowElement, params, visitedElements);        return approveNodes;    }    /**     * 递归获取预测节点列表     *     * @param flowElement     * @param params     * @param visitedElements     * @return     */    private List<ApproveNode> visiteElement(FlowElement flowElement, Map<String, Object> params, Set<String> visitedElements) {        String id = flowElement.getId();        //如果之前拜访过的节点就不再拜访        if (visitedElements.contains(id)) {            return Collections.EMPTY_LIST;        }        visitedElements.add(id);        List<ApproveNode> 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<SequenceFlow> 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<SequenceFlow> getElementSequenceFlow(FlowElement flowElement) {        if (flowElement instanceof FlowNode) {            return ((FlowNode) flowElement).getOutgoingFlows();        }        return Collections.EMPTY_LIST;    }    /**     * 执行表达式计算     * @param expression     * @param variableMap     * @param returnType     * @param <T>     * @return     */    private <T> T executeExpression(String expression, Map<String, Object> variableMap, Class<T> 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<String, Object> 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"    }]