工作流引擎目前开源上可以选的就只有activiti
、flowable
、camunda
三种,当然除了activiti
它们都有商用版本,flowable
在6之后搞商用版了。所以暂时选用了比较稳定的开源版6.5.0。最近也是利用空闲时间研究了一下flowable的具体使用流程,总体来说还是比较简单的。
参考文档:
官网:https://tkjohn.github.io/flowable-userguide/#_introduction
Flowable介绍
Flowable是一个使用Java编写的轻量级业务流程引擎。Flowable可以十分灵活地加入你的应用/服务/构架。可以将JAR形式发布的Flowable库加入应用或服务,来嵌入引擎。 以JAR形式发布使Flowable可以轻易加入任何Java环境:Java SE;Tomcat、Jetty或Spring之类的servlet容器;JBoss或WebSphere之类的Java EE服务器,等等。 另外,也可以使用Flowable REST API进行HTTP调用。也有许多Flowable应用(Flowable Modeler, Flowable Admin, Flowable IDM 与 Flowable Task),提供了直接可用的UI示例,可以使用流程与任务。
项目集成
在pom文件引入:
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.5.0</version>
</dependency>
配置文件加入:
#flowable流程引擎测试
flowable:
async-executor-activate: false
database-schema-update: true #开启数据库结构自动同步,启动项目后会自动往数据库添加表大概七八十张
这个方式是独立部署,逻辑是一个流程设计器->设计流程->导出流程文件xxx.bpmn2.0.xml文件->工程项目加载流程文件->调用流程相关接口(开始流程、提交、结束流程、查询流程流转、历史查询等等)->实现自己的业务逻辑。然后通过接口实现流程导入、授权、部署和更新等操作实现新流程加入。 最后要基于数据库使用配置mysql数据库连接池这些就不做说明了。
流程设计器
为了得到我们的流程部署文件(xxx.bpmn2.0.xml),我们可以安装流程设计工具或者使用一些第三方插件来做这个事情,不过我们可以利用官方提供的docker镜像来直接运行Flowable UI来实现流程设计。镜像地址:flowable/all-in-one:6.5.0,至于持久化映射就自行查阅吧。启动之访问http://localhost:28080/flowable-modeler即可,默认的用户名是:admin,密码:test,进去了之后就是编辑页面了编辑完成之后导出为xml文件就可以放在项目里面使用了,算是比较简单。
docker run -p 28080:8080 flowable/all-in-one:6.5.0
Flowable ui提供了几个web应用,用于演示及介绍Flowable项目提供的功能:
-
Flowable IDM: 身份管理应用。为所有Flowable UI应用提供单点登录认证功能,并且为拥有IDM管理员权限的用户提供了管理用户、组与权限的功能。
-
Flowable Modeler: 让具有建模权限的用户可以创建流程模型、表单、选择表与应用定义。
-
Flowable Task: 运行时任务应用。提供了启动流程实例、编辑任务表单、完成任务,以及查询流程实例与任务的功能。
-
Flowable Admin: 管理应用。让具有管理员权限的用户可以查询BPMN、DMN、Form及Content引擎,并提供了许多选项用于修改流程实例、任务、作业等。管理应用通过REST API连接至引擎,并与Flowable Task应用及Flowable REST应用一同部署。
demo.bpmn2.0.xml:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef">
<process id="demo" name="demo" isExecutable="true">
<documentation>这个流程是一个测试流程</documentation>
<startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent>
<userTask id="SubmitVacation" name="提交请假" flowable:candidateUsers="staff" flowable:candidateGroups="staff" flowable:formFieldValidation="true">
<documentation>人员提出请假申请</documentation>
</userTask>
<sequenceFlow id="sid-6654930C-965A-44A6-A9E4-5D4AD7195E79" sourceRef="startEvent1" targetRef="SubmitVacation"></sequenceFlow>
<exclusiveGateway id="sid-9F0C1BD9-DCE8-4542-A299-77F47BC985CE"></exclusiveGateway>
<userTask id="DepartmentManagerReview" name="部门经理审核" flowable:candidateUsers="dept_manger" flowable:candidateGroups="dept_manger" flowable:formFieldValidation="true">
<documentation>由部门经理审核请假申请</documentation>
</userTask>
<userTask id="ProjectManagerReview" name="项目经理审核" flowable:candidateUsers="project_manger" flowable:candidateGroups="project_manger" flowable:formFieldValidation="true">
<documentation>由项目经理审核请假申请</documentation>
</userTask>
<sequenceFlow id="sid-E37BFB13-BA35-4CFE-92DA-4DB34091C04C" sourceRef="SubmitVacation" targetRef="ProjectManagerReview"></sequenceFlow>
<exclusiveGateway id="sid-970F1E93-EDBC-4D4A-876B-E7E06377638C"></exclusiveGateway>
<exclusiveGateway id="sid-2A3E3784-673A-49E4-B3FB-E9F09486397A"></exclusiveGateway>
<endEvent id="sid-DCA2F1E5-AC5D-4BBC-90E8-5B274F56A5D9"></endEvent>
<endEvent id="sid-C83E28D4-423F-459B-A789-1D6211E4757E"></endEvent>
<sequenceFlow id="sid-0FB8260C-1F08-4AF6-BF97-21B56189A556" sourceRef="ProjectManagerReview" targetRef="sid-970F1E93-EDBC-4D4A-876B-E7E06377638C"></sequenceFlow>
<sequenceFlow id="sid-61AAC13D-9B4A-41C8-850F-ACCF423A5CA8" name="审核通过" sourceRef="sid-970F1E93-EDBC-4D4A-876B-E7E06377638C" targetRef="sid-9F0C1BD9-DCE8-4542-A299-77F47BC985CE">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${project_pass==true}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-1E10E485-8FA3-4A79-9C45-5985CE6871F9" name="审核不通过" sourceRef="sid-970F1E93-EDBC-4D4A-876B-E7E06377638C" targetRef="SubmitVacation">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${project_pass==false}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-6A32EEA5-051B-482F-BBC3-1A9D98DC0EF8" name="请假大于或等于三天" sourceRef="sid-9F0C1BD9-DCE8-4542-A299-77F47BC985CE" targetRef="DepartmentManagerReview">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${days >= 3}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-A9646AF4-6F60-4E47-8A86-CCC363880AA8" name="请假小于三天" sourceRef="sid-9F0C1BD9-DCE8-4542-A299-77F47BC985CE" targetRef="sid-C83E28D4-423F-459B-A789-1D6211E4757E">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${days < 3}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-97207CE9-A758-40B4-9D9B-8896F6CD5692" sourceRef="DepartmentManagerReview" targetRef="sid-2A3E3784-673A-49E4-B3FB-E9F09486397A"></sequenceFlow>
<sequenceFlow id="sid-AF408E83-505D-4642-9155-5D33A782D28F" name="审核通过" sourceRef="sid-2A3E3784-673A-49E4-B3FB-E9F09486397A" targetRef="sid-DCA2F1E5-AC5D-4BBC-90E8-5B274F56A5D9">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${dept_pass==true}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-1FD9C6EB-152E-486F-9F2F-B72D7CD8C9B6" name="审核不通过" sourceRef="sid-2A3E3784-673A-49E4-B3FB-E9F09486397A" targetRef="SubmitVacation">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${dept_pass==false}]]></conditionExpression>
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_demo">
<bpmndi:BPMNPlane bpmnElement="demo" id="BPMNPlane_demo">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="30.0" width="30.0" x="120.0" y="220.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="SubmitVacation" id="BPMNShape_SubmitVacation">
<omgdc:Bounds height="80.0" width="100.0" x="240.0" y="195.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-9F0C1BD9-DCE8-4542-A299-77F47BC985CE" id="BPMNShape_sid-9F0C1BD9-DCE8-4542-A299-77F47BC985CE">
<omgdc:Bounds height="40.0" width="40.0" x="655.0" y="215.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="DepartmentManagerReview" id="BPMNShape_DepartmentManagerReview">
<omgdc:Bounds height="80.0" width="100.0" x="835.0" y="195.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="ProjectManagerReview" id="BPMNShape_ProjectManagerReview">
<omgdc:Bounds height="80.0" width="100.0" x="445.0" y="195.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-970F1E93-EDBC-4D4A-876B-E7E06377638C" id="BPMNShape_sid-970F1E93-EDBC-4D4A-876B-E7E06377638C">
<omgdc:Bounds height="40.0" width="40.0" x="475.0" y="350.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-2A3E3784-673A-49E4-B3FB-E9F09486397A" id="BPMNShape_sid-2A3E3784-673A-49E4-B3FB-E9F09486397A">
<omgdc:Bounds height="40.0" width="40.0" x="865.0" y="455.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-DCA2F1E5-AC5D-4BBC-90E8-5B274F56A5D9" id="BPMNShape_sid-DCA2F1E5-AC5D-4BBC-90E8-5B274F56A5D9">
<omgdc:Bounds height="28.0" width="28.0" x="985.0" y="461.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-C83E28D4-423F-459B-A789-1D6211E4757E" id="BPMNShape_sid-C83E28D4-423F-459B-A789-1D6211E4757E">
<omgdc:Bounds height="28.0" width="28.0" x="661.0" y="65.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-1E10E485-8FA3-4A79-9C45-5985CE6871F9" id="BPMNEdge_sid-1E10E485-8FA3-4A79-9C45-5985CE6871F9">
<omgdi:waypoint x="475.0" y="370.0"></omgdi:waypoint>
<omgdi:waypoint x="290.0" y="370.0"></omgdi:waypoint>
<omgdi:waypoint x="290.0" y="274.95000000000005"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-A9646AF4-6F60-4E47-8A86-CCC363880AA8" id="BPMNEdge_sid-A9646AF4-6F60-4E47-8A86-CCC363880AA8">
<omgdi:waypoint x="675.0" y="215.0"></omgdi:waypoint>
<omgdi:waypoint x="675.0" y="92.94992723251232"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-1FD9C6EB-152E-486F-9F2F-B72D7CD8C9B6" id="BPMNEdge_sid-1FD9C6EB-152E-486F-9F2F-B72D7CD8C9B6">
<omgdi:waypoint x="865.0" y="475.0"></omgdi:waypoint>
<omgdi:waypoint x="290.0" y="475.0"></omgdi:waypoint>
<omgdi:waypoint x="290.0" y="274.95000000000005"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-6654930C-965A-44A6-A9E4-5D4AD7195E79" id="BPMNEdge_sid-6654930C-965A-44A6-A9E4-5D4AD7195E79">
<omgdi:waypoint x="149.9499992392744" y="235.0"></omgdi:waypoint>
<omgdi:waypoint x="240.0" y="235.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-6A32EEA5-051B-482F-BBC3-1A9D98DC0EF8" id="BPMNEdge_sid-6A32EEA5-051B-482F-BBC3-1A9D98DC0EF8">
<omgdi:waypoint x="694.9452522606873" y="235.0"></omgdi:waypoint>
<omgdi:waypoint x="835.0" y="235.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-E37BFB13-BA35-4CFE-92DA-4DB34091C04C" id="BPMNEdge_sid-E37BFB13-BA35-4CFE-92DA-4DB34091C04C">
<omgdi:waypoint x="339.94999999988573" y="235.0"></omgdi:waypoint>
<omgdi:waypoint x="444.99999999995566" y="235.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-61AAC13D-9B4A-41C8-850F-ACCF423A5CA8" id="BPMNEdge_sid-61AAC13D-9B4A-41C8-850F-ACCF423A5CA8">
<omgdi:waypoint x="514.9389289678135" y="370.0"></omgdi:waypoint>
<omgdi:waypoint x="585.0" y="370.0"></omgdi:waypoint>
<omgdi:waypoint x="585.0" y="235.0"></omgdi:waypoint>
<omgdi:waypoint x="655.0" y="235.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-0FB8260C-1F08-4AF6-BF97-21B56189A556" id="BPMNEdge_sid-0FB8260C-1F08-4AF6-BF97-21B56189A556">
<omgdi:waypoint x="495.0" y="274.95000000000005"></omgdi:waypoint>
<omgdi:waypoint x="495.0" y="350.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-97207CE9-A758-40B4-9D9B-8896F6CD5692" id="BPMNEdge_sid-97207CE9-A758-40B4-9D9B-8896F6CD5692">
<omgdi:waypoint x="885.0" y="274.95000000000005"></omgdi:waypoint>
<omgdi:waypoint x="885.0" y="455.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-AF408E83-505D-4642-9155-5D33A782D28F" id="BPMNEdge_sid-AF408E83-505D-4642-9155-5D33A782D28F">
<omgdi:waypoint x="904.941257668518" y="475.0"></omgdi:waypoint>
<omgdi:waypoint x="985.0" y="475.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
具体使用:
我这里简单创建了一个服务和写了一些前端文件来测试:
后端代码:
package com.example.springboottest.FlowableTest.service;
import com.example.springboottest.FlowableTest.Entity.ProcessInstanceObject;
import com.example.springboottest.FlowableTest.Entity.TaskObject;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
/**
* @Author: liangpeng
* @name: FlowableService 流程引擎服务
* @Date: 2023/5/30 11:49
*/
public interface FlowableService {
/**
* 启动流程
* @param processKey 流程定义key(流程图ID)
* @param businessKey 业务key(可以为空)
* @param map 参数键值对
* @return 流程实例ID
*/
ProcessInstance start(String processKey, String businessKey, Map<String, Object> map);
/**
* 停止流程
* @param processInstanceId 流程实例ID
* @param reason 终止理由
*/
void stop(String processInstanceId, String reason);
/**
* 完成指定任务(推动流程运行)
* @param taskId 任务ID
* @param map 变量键值对
* @throws RuntimeException 任务不存在
*/
void complete(String taskId, Map<String, Object> map);
/**
* 获取指定用户的待办任务列表(创建时间倒序)
* @param userId 用户ID
* @return 任务列表
*/
List<TaskObject> getTasksByUserId(String userId);
/**
* 获取指定用户组的待办任务列表
* @param group 用户组
* @return 任务列表
*/
List<TaskObject> getTasksByGroup(String group);
/**
* 获取指定实例的任务列表
* @param processInstanceId 流程实例id
* @return 任务列表
*/
List<TaskObject> getTasksByProcessInstanceId(String processInstanceId);
/**
* 获取指定任务列表中的特定任务
* @param list 任务列表
* @param businessKey 业务key
* @return 任务
*/
Task getOneByBusinessKey(List<Task> list, String businessKey);
/**
* 创建流程并完成第一个任务
* @param processKey 流程定义key(流程图ID)
* @param businessKey 业务key
* @param map 变量键值对
*/
Map<String,Object> startAndComplete(String processKey, String businessKey, Map<String, Object> map);
/**
* 退回到指定任务节点
* @param currentTaskId 当前任务ID
* @param targetTaskKey 目标任务节点key
* @throws Exception 当前任务节点不存在
*/
void backToStep(String currentTaskId, String targetTaskKey) throws Exception;
/**
* 返回尚未结束的流程实例
* @param processKey 流程的名称(传入空返回所有)
* @return 流程列表
*/
List<ProcessInstanceObject> getProcessListByKey(String processKey);
/**
* 返回所有的流程实例
*
* @param processKey 流程的名称(传入空返回所有)
* @return 流程列表
*/
List<ProcessInstanceObject> getHistoryProcessList(String processKey);
/**
* 获取尚未结束的流程图
* @param outputStream 流程图的输出流(传入空返回所有)
* @param processId 流程的ID
* @throws IOException 流无法读取或写入
*/
Boolean getProcessChart(OutputStream outputStream, String processId) throws IOException;
}
package com.example.springboottest.FlowableTest.service.impl;
import com.example.springboottest.FlowableTest.Entity.ProcessInstanceObject;
import com.example.springboottest.FlowableTest.Entity.TaskObject;
import com.example.springboottest.FlowableTest.service.FlowableService;
import liquibase.pro.packaged.B;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.task.api.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author: liangpeng
* @name: FlowableServiceImpl
* @Date: 2023/5/30 11:49
* @description: 流程引擎实现
*/
@Service
public class FlowableServiceImpl implements FlowableService {
private static final Logger logger = LoggerFactory.getLogger("flowable-service-log");
@Resource
RuntimeService runtimeService;
@Resource
TaskService taskService;
@Resource
HistoryService historyService;
@Resource
ProcessEngine processEngine;
@Resource
RepositoryService repositoryService;
@Override
public ProcessInstance start(String processKey, String businessKey, Map<String, Object> map) {
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processKey, businessKey, map);
return processInstance;
}
@Override
public void stop(String processInstanceId, String reason) {
runtimeService.deleteProcessInstance(processInstanceId, reason);
}
@Override
public void complete(String taskId, Map<String, Object> map) {
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null) {
logger.error(taskId + ":指定的任务不存在");
throw new RuntimeException("任务不存在");
}
taskService.complete(taskId, map);
}
@Override
public List<TaskObject> getTasksByUserId(String userId) {
List<Task> tasks = taskService.createTaskQuery().taskAssignee(userId).orderByTaskCreateTime().desc().list();
return taskToTaskObject(tasks);
}
@Override
public List<TaskObject> getTasksByGroup(String group) {
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup(group).orderByTaskCreateTime().desc().list();
return taskToTaskObject(tasks);
}
@Override
public List<TaskObject> getTasksByProcessInstanceId(String processInstanceId) {
List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstanceId).orderByTaskCreateTime().desc().list();
return taskToTaskObject(tasks);
}
private List<TaskObject> taskToTaskObject(List<Task> tasks) {
List<TaskObject> taskObjects = new ArrayList<>();
for (Task task : tasks) {
taskObjects.add(TaskObject.fromTask(task));
}
return taskObjects;
}
@Override
public Task getOneByBusinessKey(List<Task> list, String businessKey) {
Task task = null;
for (Task t : list) {
// 通过任务对象获取流程实例
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(t.getProcessInstanceId()).singleResult();
if (businessKey.equals(pi.getBusinessKey())) {
task = t;
}
}
return task;
}
@Override
public Map<String,Object> startAndComplete(String processKey, String businessKey, Map<String, Object> map) {
Map<String,Object> out=new HashMap<>();
ProcessInstance processInstance = start(processKey, businessKey, map);//启动流程,返回流程id
if(ObjectUtils.isEmpty(processInstance)) return out;
out.put("processInstance",ProcessInstanceObject.fromProcessInstance(processInstance));
Task task = processEngine.getTaskService().createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
taskService.complete(task.getId(), map);
out.put("task",TaskObject.fromTask(task));
return out;
}
@Override
public void backToStep(String currentTaskId, String targetTaskKey) throws Exception {
Task currentTask = taskService.createTaskQuery().taskId(currentTaskId).singleResult();
if (currentTask == null) {
logger.error(currentTaskId + ":指定的任务不存在");
throw new Exception("当前任务节点不存在");
}
List<String> currentTaskKeys = new ArrayList<>();
currentTaskKeys.add(currentTask.getTaskDefinitionKey());
runtimeService.createChangeActivityStateBuilder().processInstanceId(currentTask.getProcessInstanceId()).moveActivityIdsToSingleActivityId(currentTaskKeys, targetTaskKey);
}
@Override
public List<ProcessInstanceObject> getProcessListByKey(String processKey) {
if (processKey == null) {
return processToProcessObject(runtimeService.createProcessInstanceQuery().orderByStartTime().desc().list());
}
return processToProcessObject(runtimeService.createProcessInstanceQuery().processDefinitionKey(processKey).orderByStartTime().desc().list());
}
@Override
public List<ProcessInstanceObject> getHistoryProcessList(String processKey) {
if (processKey == null) {
return historyProcessToProcessObject(historyService.createHistoricProcessInstanceQuery().orderByProcessInstanceStartTime().desc().list());
}
return historyProcessToProcessObject(historyService.createHistoricProcessInstanceQuery().orderByProcessInstanceStartTime().desc().processDefinitionKey(processKey).list());
}
private List<ProcessInstanceObject> processToProcessObject(List<ProcessInstance> instances) {
List<ProcessInstanceObject> instanceObjects = new ArrayList<>();
for (ProcessInstance instance : instances) {
instanceObjects.add(ProcessInstanceObject.fromProcessInstance(instance));
}
return instanceObjects;
}
private List<ProcessInstanceObject> historyProcessToProcessObject(List<HistoricProcessInstance> instances) {
List<ProcessInstanceObject> instanceObjects = new ArrayList<>();
for (HistoricProcessInstance instance : instances) {
instanceObjects.add(ProcessInstanceObject.fromHistoryProcessInstance(instance));
}
return instanceObjects;
}
@Override
public Boolean getProcessChart(OutputStream out, String processId) throws IOException {
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();
//流程走完的不显示图
if (pi == null) {
return false;
}
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
//使用流程实例ID,查询正在执行的执行对象表,返回流程实例对象
String InstanceId = task.getProcessInstanceId();
List<Execution> executions = runtimeService
.createExecutionQuery()
.processInstanceId(InstanceId)
.list();
//得到正在执行的Activity的Id
List<String> activityIds = new ArrayList<>();
List<String> flows = new ArrayList<>();
for (Execution exe : executions) {
List<String> ids = runtimeService.getActiveActivityIds(exe.getId());
activityIds.addAll(ids);
}
//获取流程图
BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());
ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();
ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();
InputStream in = diagramGenerator.generateDiagram(bpmnModel, "png",
activityIds, flows, "宋体", "宋体", "宋体", null, 1.0, true);
byte[] buf = new byte[1024];
int legth = 0;
try {
while ((legth = in.read(buf)) != -1) {
out.write(buf, 0, legth);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
return true;
}
}
package com.example.springboottest.FlowableTest.Controller;
import com.example.springboottest.FlowableTest.Entity.ProcessInstanceObject;
import com.example.springboottest.FlowableTest.service.FlowableService;
import com.example.springboottest.common.vo.result.R;
import liquibase.util.StringUtil;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value = "${application.admin-path}/flowable")
public class FlowableController {
final private String PROCESS_KEY = "demo";
@Resource
FlowableService flowableService;
//TODO:动态部署一个流程(xml文件)
@GetMapping("/demo/stopProcessById")
//@ApiOperation("停止流程实例")
public R stopProcessById(String processInstanceId) {
if(StringUtil.isEmpty(processInstanceId))
throw new RuntimeException("参数不全请指定流程实例id(processInstanceId)字段");
flowableService.stop(processInstanceId, "测试手动停止!");
return R.buildOk("停止流程实例["+processInstanceId+"]成功!");
}
@GetMapping("/demo/startAndComplete")
//@ApiOperation("开始请假流程,并提交")
public R startAndComplete(Integer days,String processKey) {
if(ObjectUtils.isEmpty(days)||StringUtil.isEmpty(processKey))
throw new RuntimeException("参数不全请指定请假天数(days)和流程key(processKey)字段");
Map<String, Object> map = new HashMap<>();
map.put("days", days);
Map<String, Object> objectMap = flowableService.startAndComplete(processKey, "请假", map);
return R.buildOkData(objectMap);
}
@GetMapping("/demo/reComplete")
//@ApiOperation("重新提交申请流程")
public R reComplete(Integer days,String taskId){
if(ObjectUtils.isEmpty(days)||StringUtil.isEmpty(taskId))
throw new RuntimeException("参数不全请指定请假天数(days)和任务id(taskId)字段");
Map<String, Object> map = new HashMap<>();
map.put("project_pass", false);
flowableService.complete(taskId, map);
return R.buildOk("重新提交申请流程成功,项目经理审核!");
}
@GetMapping("/demo/project/pass")
//@ApiOperation("项目经理审核通过")
public R projectPass(String taskId) throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("project_pass", true);
flowableService.complete(taskId, map);
return R.buildOk("项目经理审核通过成功,自动判定请假天数决定是否部门经理审核!");
}
@GetMapping("/demo/project/not_pass")
//@ApiOperation("项目经理审核驳回")
public R projectNotPass(String taskId) {
Map<String, Object> map = new HashMap<>();
map.put("project_pass", false);
flowableService.complete(taskId, map);
return R.buildOk("项目经理审核驳回成功,请修改后重新提交申请!");
}
@GetMapping("/demo/dept/pass")
//@ApiOperation("部门经理审核通过")
public R deptPass(String taskId) {
Map<String, Object> map = new HashMap<>();
map.put("dept_pass", true);
flowableService.complete(taskId, map);
return R.buildOk("部门经理审核通过,流程结束!");
}
@GetMapping("/demo/dept/not_pass")
//@ApiOperation("部门经理审核驳回")
public R deptNotPass(String taskId) {
Map<String, Object> map = new HashMap<>();
map.put("dept_pass", false);
flowableService.complete(taskId, map);
return R.buildOk("部门经理审核驳回成功,请修改后重新提交申请!");
}
@GetMapping("/demo/process/list")
//@ApiOperation("通过流程key获取未完成的流程实例列表")
public R<ProcessInstanceObject> getProcessList(String processKey) {
if(StringUtil.isEmpty(processKey))
throw new RuntimeException("参数不全请指定流程key(processKey)字段");
return R.buildOkData(flowableService.getProcessListByKey(processKey));
}
@GetMapping("/demo/getTasksByGroup")
//@ApiOperation("获取任务列表-指定用户组,倒叙")
public R getTaskList(String groupId) {
return R.buildOkData(flowableService.getTasksByGroup(groupId));
}
@GetMapping("/demo/getTasksByProcessInstanceId")
//@ApiOperation("获取任务列表-指定流程实例id,倒叙")
public R getTasksByProcessInstanceId(String processInstanceId) {
return R.buildOkData(flowableService.getTasksByProcessInstanceId(processInstanceId));
}
@GetMapping("/demo/process/history/list")
//@ApiOperation("获取已经完成的流程列表")
public Object getHistoryProcessList() throws Exception {
return flowableService.getHistoryProcessList(PROCESS_KEY);
}
@RequestMapping("/demo/process/chart/{processId}")
// @ApiOperation("获取某个流程实例的进度状态")
public void getProcessChart(HttpServletResponse httpServletResponse, @PathVariable("processId") String processId) throws IOException {
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
httpServletResponse.setContentType("image/png");
Boolean processChart = flowableService.getProcessChart(outputStream, processId);
//return R.buildOkData(processChart);
}
}
package com.example.springboottest.FlowableTest.Entity;
import lombok.Data;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.beans.BeanUtils;
import java.util.Date;
import java.util.Map;
/**
*
* 流程实体表--转换
*/
@Data
public class ProcessInstanceObject {
String processDefinitionId;
String processDefinitionName;
String processDefinitionKey;
Integer processDefinitionVersion;
String deploymentId;
String businessKey;
boolean isSuspended;
Map<String, Object> processVariables;
String tenantId;
String name;
String description;
String localizedName;
String localizedDescription;
Date startTime;
Date endTime;
Long durationInMillis;
String endActivityId;
String startUserId;
String callbackId;
String callbackType;
String id;
boolean suspended;
boolean ended;
String activityId;
String processInstanceId;
String parentId;
String superExecutionId;
String rootProcessInstanceId;
String referenceId;
String referenceType;
String propagatedStageInstanceId;
public static ProcessInstanceObject fromProcessInstance(ProcessInstance instance){
ProcessInstanceObject object = new ProcessInstanceObject();
BeanUtils.copyProperties(instance,object);
return object;
}
public static ProcessInstanceObject fromHistoryProcessInstance(HistoricProcessInstance instance){
ProcessInstanceObject object = new ProcessInstanceObject();
BeanUtils.copyProperties(instance,object);
return object;
}
}
package com.example.springboottest.FlowableTest.Entity;
import lombok.Data;
import org.flowable.task.api.Task;
import org.springframework.beans.BeanUtils;
import java.util.Date;
import java.util.Map;
/**
*
* 任务实体表--转换
*/
@Data
public class TaskObject {
String id;
String name;
String description;
int priority;
String owner;
String assignee;
String processInstanceId;
String executionId;
String taskDefinitionId;
String processDefinitionId;
String scopeId;
String subScopeId;
String scopeType;
String scopeDefinitionId;
String propagatedStageInstanceId;
Date createTime;
String taskDefinitionKey;
Date dueDate;
String category;
String parentTaskId;
String tenantId;
String formKey;
Map<String, Object> taskLocalVariables;
Map<String, Object> processVariables;
Date claimTime;
public static TaskObject fromTask(Task task){
TaskObject object = new TaskObject();
BeanUtils.copyProperties(task,object);
return object;
}
}
package com.example.springboottest.common.vo.result;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.springframework.http.HttpStatus;
import org.springframework.util.ObjectUtils;
import java.io.Serializable;
/**
* 响应信息主体
*
* @param <T>
* @author somewhere
*/
@ToString
@Accessors(chain = true)
@AllArgsConstructor
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Getter
@Setter
private int code = CommonConstants.SUCCESS;
@Getter
@Setter
private HttpStatus httpStatus;
@Getter
@Setter
private T data;
private String[] messages = {};
public R() {
super();
}
public R(T data) {
super();
this.data = data;
}
public R(String... msg) {
super();
this.messages = msg;
}
public R(T data, String... msg) {
super();
this.data = data;
this.messages = msg;
}
public R(T data, int code, String... msg) {
super();
this.data = data;
this.code = code;
this.messages = msg;
}
public R(Throwable e) {
super();
setMessage(e.getMessage());
this.code = CommonConstants.FAIL;
}
public static R buildOk(String... messages) {
return new R(messages);
}
public static <T> R buildOkData(T data, String... messages) {
return new R(data, messages);
}
public static <T> R buildFailData(T data, String... messages) {
return new R(data, CommonConstants.FAIL, messages);
}
public static <T> R buildFail(String... messages) {
return new R(null, CommonConstants.FAIL, messages);
}
public static <T> R build(T data, int code, String... messages) {
return new R(data, code, messages);
}
public static <T> R build(int code, String... messages) {
return new R(null, code, messages);
}
public String getMessage() {
return readMessages();
}
public void setMessage(String message) {
addMessage(message);
}
public String readMessages() {
StringBuilder sb = new StringBuilder();
for (String message : messages) {
sb.append(message);
}
return sb.toString();
}
public void addMessage(String message) {
this.messages = ObjectUtils.addObjectToArray(messages, message);
}
}
前端页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>flowable测试</title>
<script src="../js/jquery-3.0.0.js"> </script>
<script src="../js/wfs.js"></script>
<!-- <link href="js/jquery/jquery-ui.css" rel="stylesheet" type="text/css" /> -->
</head>
<body>
<h2>flowable测试</h2>
<div style="width:1200px; height:600px;float:right">
<button id="viewProcess">查看进度,并实时查看进度和更新任务id</button>
info:<textarea id="viewProcessMessage" style="width:500px;height:20px;"></textarea>
<img id="processImg" src="./default.jpg" style="width:100%; height:100%;float:left" alt="图片显示"/>
</div>
<button id="login">登录授权</button>
<br>
-----------------------------------------基本信息区域-----------------------------------------
<br>
流程key:<textarea id="processKeyTxt" style="width:100px;height:20px;"></textarea>
<button id="getProcessProcessKey">查询并更新实例id</button>
<br>
当前流程实例id:<textarea id="processIdTxt" style="width:300px;height:20px;"></textarea>
<button id="getTasksByProcessInstanceId">查询并更新任务id</button>
<button id="stopProcess">停止该流程</button>
<br>
当前任务id:<textarea id="taskIdTxt" style="width:300px;height:20px;"></textarea>
<br>
<br>
-----------------------------------------操作区域-----------------------------------------
<script>
var mypath="http://127.0.0.1:8888/a";
var tocken="";
document.getElementById("processKeyTxt").value='demo';
document.getElementById("processIdTxt").value="4555232f-fed5-11ed-bd05-0250f2000002";
document.getElementById("taskIdTxt").value="86248e60-ff81-11ed-9e75-0250f2000002";
</script>
<script>
/**
* 登录授权 获取tocken 并缓存tocken
*/
$("#login").click(function () {
$.ajax({
type: 'get',
url: mypath + "/jwt/login",
dataType: 'json',
contentType: "application/json;charset=UTF-8",
success: function (res) {
console.log(res)
if(res.code&&res.code==200&&res.data&&res.data.state)
{
tocken=res.data.token;
window.localStorage.setItem("tocken",tocken);
alert("授权成功!")
}
else
{
alert("授权失败,错误信息:"+JSON.stringify(res))
}
}
});
})
</script>
<script>
var runNum=1;
//更新任务id
function refaushTaskId () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/getTasksByProcessInstanceId?processInstanceId="
+document.getElementById("processIdTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
if(res.code&&res.code==200&&res.data.length>0)
{
var idsstr="任务ids: \n";
for (let i = 0; i < res.data.length; i++) {
idsstr+=res.data[i].id+"\n"
}
//alert("查询成功,"+idsstr);
console.log(idsstr)
document.getElementById("taskIdTxt").value=res.data[0].id;
}
else
{
console.log("查询失败,错误信息:"+(res.message||"该流程没有待办的任务"));
}
},
error:function (res){
//alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
}
//更新流程实例id
function refaushProcessInstanceIdByKey(){
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/process/list?processKey=" +document.getElementById("processKeyTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
//console.log(res)
if(res.code&&res.code==200&&res.data.length>0)
{
var idsstr="流程实例ids: \n";
for (let i = 0; i < res.data.length; i++) {
idsstr+=res.data[i].id+"\n"
}
//alert("查询成功,"+idsstr);
console.log(idsstr)
document.getElementById("processIdTxt").value=res.data[0].id;
refaushTaskId();
}
else
{
alert("查询失败:"+(res.message||"该流程没有正在运行的实例,请先点击新建请假流程!"))
}
},
error:function (res){
//alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
}
//查询更更新流程进度
function refaushProcessImg () {
runNum+=1;
var xmlhttp;
xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET",mypath + "/flowable/demo/process/chart/"+ document.getElementById("processIdTxt").value,true);
xmlhttp.responseType = "blob";
xmlhttp.setRequestHeader("access_token", tocken||window.localStorage.getItem("tocken"));
xmlhttp.onload = function(){
console.log("xmlhttp RES",this);
if (this.status == 200) {
console.log("asdasdasdasdsad")
var blob = this.response;
if(blob.size&&blob.size>10)
{
var img_processImg = document.getElementById("processImg");
// img_processImg.onload = function(e) {
// window.URL.revokeObjectURL(img_processImg.src);
// };
img_processImg.src = window.URL.createObjectURL(blob);
document.getElementById("viewProcessMessage").value="已经刷新次数:"+runNum;
}
else
{
document.getElementById("viewProcessMessage").value="当前流程不存在或者已结束!,已经刷新次数:"+runNum;
document.getElementById("processImg").src="./default.jpg";
}
}
}
xmlhttp.send();
}
//按钮点击-根据流程key查询流程实例
$("#getProcessProcessKey").click(function () {
refaushProcessInstanceIdByKey();
})
//按钮点击-查询并更新任务id
$("#getTasksByProcessInstanceId").click(function () {
refaushTaskId();
})
//按钮点击-查看流程执行情况
$("#viewProcess").click(function () {
refaushProcessImg ();
//定时器刷新
setInterval(function (){
refaushProcessImg();
refaushTaskId();
},2000)
})
//按钮点击-停止流程
$("#stopProcess").click(function () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/stopProcessById?processInstanceId="
+document.getElementById("processIdTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
alert(res.message||JSON.stringify(res));
refaushProcessImg();
},
error:function (res){
//alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
})
</script>
<br>
<br>
请假人操作:
<div style="width:300px; height:50px;">
请输入请假天数:<textarea id="qjdays" style="width:50px;height:20px;"></textarea>
<br>
<button id="startAndComplete">新建请假流程</button>
<button id="reComplete">重新提交</button>
</div>
<br>
项目经理操作:
<div style="width:300px; height:50px;">
<button id="project_pass">同意</button>
<button id="project_not_pass">不同意</button>
</div>
<br>
部门经理操作:
<div style="width:300px; height:50px;">
<button id="dept_pass">同意</button>
<button id="dept_not_pass">不同意</button>
</div>
<br>
<script>
document.getElementById("qjdays").value=3;
/**
* 新建开始请假流程
*/
$("#startAndComplete").click(function () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/startAndComplete?days="
+ document.getElementById("qjdays").value
+"&processKey="+document.getElementById("processKeyTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
if(res.code&&res.code==200)
{
alert("提交请假申请成功!")
document.getElementById("processIdTxt").value=res.data.processInstance.id;
document.getElementById("taskIdTxt").value=res.data.task.id;
}
else
{
alert("提交请假申请失败,错误信息:"+(res.message||JSON.stringify(res)))
}
},
error:function (res){
alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
})
//驳回后重新提交
$("#reComplete").click(function () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/reComplete?days="
+ document.getElementById("qjdays").value
+"&taskId="+ document.getElementById("taskIdTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
if(res.code&&res.code==200)
{
alert("重新提交请假申请成功!")
//document.getElementById("processIdTxt").value=res.data.processInstance.id;
//document.getElementById("taskIdTxt").value=res.data.task.id;
}
else
{
alert("提交请假申请失败,错误信息:"+(res.message||JSON.stringify(res)))
}
},
error:function (res){
alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
})
</script>
<script>
//项目经理按钮区域
/**
* 通过
*/
$("#project_pass").click(function () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/project/pass?taskId="+ document.getElementById("taskIdTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
alert(res.message||JSON.stringify(res))
if(res.code&&res.code==200&&res.data)
{
}
else
{
}
},
error:function (res){
alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
})
$("#project_not_pass").click(function () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/project/not_pass?taskId="+ document.getElementById("taskIdTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
alert(res.message||JSON.stringify(res))
if(res.code&&res.code==200&&res.data)
{
}
else
{
}
},
error:function (res){
alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
})
</script>
<script>
//部门经理按钮区域
/**
* 通过
*/
$("#dept_pass").click(function () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/dept/pass?taskId="+ document.getElementById("taskIdTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
alert(res.message||JSON.stringify(res))
if(res.code&&res.code==200&&res.data)
{
}
else
{
}
},
error:function (res){
alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
})
$("#dept_not_pass").click(function () {
$.ajax({
type: 'get',
url: mypath + "/flowable/demo/dept/not_pass?taskId="+ document.getElementById("taskIdTxt").value,
dataType: 'json',
contentType: "application/json;charset=UTF-8",
headers: {
"access_token": tocken||window.localStorage.getItem("tocken")
},
success: function (res) {
console.log(res)
alert(res.message||JSON.stringify(res))
if(res.code&&res.code==200&&res.data)
{
}
else
{
}
},
error:function (res){
alert("请求异常,错误信息:"+(JSON.stringify(res)))
}
});
})
</script>
</body>
</html>
TODO:
目前只是建的测试了一下,具体的还有很多想做的没做完的事:
- 流程发布接口和更新接口。(目前只需要放在
resources/processes
里面,只要不冲突和重复就不会有问题,项目启动时会自动部署) - 在框架项目albedo里面集成流程设计器,实现用户可以自己编辑和定义流程。参考:Spring Boot 整合 Flowable-ui-modeler 6.7.2_springboot 整合flowableui_繁华哟的博客-CSDN博客
- 若依流程引擎框架探索:开源项目Ruoyi-Flowable-plus逆向学习视频教程配套文档_zhaozhiqiang1981的博客-CSDN博客
常见错误 :
- 由于升级本后回退发现版本报错:
Could not update Flowable database schema: unknown version from database: ‘6.7.1.0‘
如下两个表的版本都改为你当前pom文件里面指定的版本:
ACT_GE_PROPERTY 把这个表的所有的
ACT_ID_PROPERTY 把schema.version参数改为和你的