前言
我们在项目中需要使用到Flowable,鉴于之前对流程引擎并不是很了解,这里准备写一篇作为入门文章。
实践
这次例子是基于springboot + Flowable ,springboot 的版本是2.3.4.RELEASE,Flowable 的版本为6.3.0
1、依赖
<!--flowable工作流依赖-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.3.0</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 加载jdbc连接数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 加载mybatis jar包 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>compile</scope>
</dependency>
2、依赖(主要添加数据库和flowable相关依赖)
server:
port: 8082
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://localhost:3306/test_mybatis?serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
read-only: false
#客户端等待连接池连接的最大毫秒数
connection-timeout: 60000
#允许连接在连接池中空闲的最长时间(以毫秒为单位)
idle-timeout: 60000
#连接将被测试活动的最大时间量
validation-timeout: 3000
#池中连接关闭后的最长生命周期
max-lifetime: 60000
#最大池大小
maximum-pool-size: 60
#连接池中维护的最小空闲连接数
minimum-idle: 10
#从池返回的连接的默认自动提交行为。默认值为true
auto-commit: true
#如果您的驱动程序支持JDBC4,我们强烈建议您不要设置此属性
connection-test-query: SELECT 1
#自定义连接池名称
pool-name: myHikarCp
flowable:
database-schema-update: false
#关闭定时任务JOB
async-executor-activate: false
3、如何定义xml
flowable建议采用业界标准BPMN2.0的XML来描述需要定义的工作流。所以我们需要在项目中创建一个流程定义,下面这个是我创建的一个xml定义,
很多文章上面并没有如何定义BPMN2.0的XML的操作(主要需要 tomcat 和下载flower)
(1) 下载tomcat
[tomcat 下载官网](https://tomcat.apache.org/download-80.cgi)
(2) 下载flowable的zip文件
https://github.com/flowable/flowable-engine/releases/download/flowable-6.4.0/flowable-6.4.0.zip 下载flowable的zip文件
下载下来解压之后,我们把flowable-6.4.0/wars 下的所有war包复制到tomcat/webapps下面:
拷入
(3)登录编辑流程图
启动成功之后,可以登陆创建流程:http://localhost:8080/flowable-modeler :
默认账号密码:admin 密码 test
BPMN2.0 图有两个需要注意的地方,确定Flowable 中的审批,驳回放置内容(这里例子用的${taskUser} ,也可以用其他的,决定放置到那个地方)
这里判断之后走那个流程需要(${finishFlag==“NO”} 或者 ${finishFlag==“YES”} )选择走那个方向,这里的这两个都是自定义的
3、例子使用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="adviceApply" name="投诉建议" isExecutable="true">
<documentation>投诉建议流程</documentation>
<startEvent id="startEvent1" name="开始" flowable:formFieldValidation="true"></startEvent>
<userTask id="customerService" name="客服代表" flowable:candidateGroups="${customerServiceId}" flowable:formFieldValidation="true"></userTask>
<sequenceFlow id="sid-C443333F-D5FF-41E4-9F84-AA4B33BC57AB" sourceRef="startEvent1" targetRef="customerService"></sequenceFlow>
<userTask id="department" name="部门领导" flowable:assignee="${taskUser}" flowable:formFieldValidation="true">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<userTask id="support" name="专业主管" flowable:assignee="${taskUser}" flowable:formFieldValidation="true">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<endEvent id="sid-D7B9B979-C32C-4959-A4F7-129C0691D8E5" name="结束"></endEvent>
<exclusiveGateway id="sid-A93C80FA-86B7-4200-B486-2A8B4719748D"></exclusiveGateway>
<exclusiveGateway id="sid-70D7E21E-345D-4CC8-BCDF-55F0DC887303"></exclusiveGateway>
<exclusiveGateway id="sid-4E7CF6E7-796A-48D1-BACA-FE3C1D95F21B"></exclusiveGateway>
<userTask id="feedback" name="客服代表" flowable:candidateGroups="${customerServiceId}" flowable:formFieldValidation="true"></userTask>
<sequenceFlow id="f10" sourceRef="feedback" targetRef="sid-D7B9B979-C32C-4959-A4F7-129C0691D8E5"></sequenceFlow>
<sequenceFlow id="f1" sourceRef="customerService" targetRef="sid-A93C80FA-86B7-4200-B486-2A8B4719748D"></sequenceFlow>
<sequenceFlow id="f2" sourceRef="sid-A93C80FA-86B7-4200-B486-2A8B4719748D" targetRef="department">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${finishFlag=="NO"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="f4" sourceRef="department" targetRef="sid-70D7E21E-345D-4CC8-BCDF-55F0DC887303"></sequenceFlow>
<sequenceFlow id="f7" sourceRef="support" targetRef="sid-4E7CF6E7-796A-48D1-BACA-FE3C1D95F21B"></sequenceFlow>
<sequenceFlow id="f5" sourceRef="sid-70D7E21E-345D-4CC8-BCDF-55F0DC887303" targetRef="support">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${finishFlag=="NO"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="f8" sourceRef="sid-4E7CF6E7-796A-48D1-BACA-FE3C1D95F21B" targetRef="feedback">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${finishFlag=="NO"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="f9" sourceRef="sid-4E7CF6E7-796A-48D1-BACA-FE3C1D95F21B" targetRef="sid-D7B9B979-C32C-4959-A4F7-129C0691D8E5">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${finishFlag=="YES"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="f6" sourceRef="sid-70D7E21E-345D-4CC8-BCDF-55F0DC887303" targetRef="sid-D7B9B979-C32C-4959-A4F7-129C0691D8E5">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${finishFlag=="YES"}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="f3" name="通过" sourceRef="sid-A93C80FA-86B7-4200-B486-2A8B4719748D" targetRef="sid-D7B9B979-C32C-4959-A4F7-129C0691D8E5">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${finishFlag=="YES"}]]></conditionExpression>
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_adviceApply">
<bpmndi:BPMNPlane bpmnElement="adviceApply" id="BPMNPlane_adviceApply">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="30.0" width="30.0" x="40.0" y="163.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="customerService" id="BPMNShape_customerService">
<omgdc:Bounds height="80.0" width="100.0" x="115.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="department" id="BPMNShape_department">
<omgdc:Bounds height="80.0" width="100.0" x="350.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="support" id="BPMNShape_support">
<omgdc:Bounds height="80.0" width="100.0" x="585.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-D7B9B979-C32C-4959-A4F7-129C0691D8E5" id="BPMNShape_sid-D7B9B979-C32C-4959-A4F7-129C0691D8E5">
<omgdc:Bounds height="28.0" width="28.0" x="866.0" y="300.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-A93C80FA-86B7-4200-B486-2A8B4719748D" id="BPMNShape_sid-A93C80FA-86B7-4200-B486-2A8B4719748D">
<omgdc:Bounds height="40.0" width="40.0" x="270.9999791604486" y="158.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-70D7E21E-345D-4CC8-BCDF-55F0DC887303" id="BPMNShape_sid-70D7E21E-345D-4CC8-BCDF-55F0DC887303">
<omgdc:Bounds height="40.0" width="40.0" x="500.0" y="158.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-4E7CF6E7-796A-48D1-BACA-FE3C1D95F21B" id="BPMNShape_sid-4E7CF6E7-796A-48D1-BACA-FE3C1D95F21B">
<omgdc:Bounds height="40.0" width="40.0" x="740.0" y="158.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="feedback" id="BPMNShape_feedback">
<omgdc:Bounds height="80.0" width="100.0" x="830.0" y="138.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="f6" id="BPMNEdge_f6">
<omgdi:waypoint x="520.5" y="197.44301253687323"></omgdi:waypoint>
<omgdi:waypoint x="520.5" y="314.0"></omgdi:waypoint>
<omgdi:waypoint x="866.0" y="314.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f7" id="BPMNEdge_f7">
<omgdi:waypoint x="684.9499999999999" y="178.0"></omgdi:waypoint>
<omgdi:waypoint x="740.0" y="178.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f8" id="BPMNEdge_f8">
<omgdi:waypoint x="779.5215994962216" y="178.42016806722688"></omgdi:waypoint>
<omgdi:waypoint x="829.9999999999989" y="178.20899581589958"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f9" id="BPMNEdge_f9">
<omgdi:waypoint x="760.5" y="197.44301253687317"></omgdi:waypoint>
<omgdi:waypoint x="760.5" y="314.0"></omgdi:waypoint>
<omgdi:waypoint x="866.0" y="314.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f10" id="BPMNEdge_f10">
<omgdi:waypoint x="880.0" y="217.95000000000002"></omgdi:waypoint>
<omgdi:waypoint x="880.0" y="300.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-C443333F-D5FF-41E4-9F84-AA4B33BC57AB" id="BPMNEdge_sid-C443333F-D5FF-41E4-9F84-AA4B33BC57AB">
<omgdi:waypoint x="69.94999848995758" y="178.0"></omgdi:waypoint>
<omgdi:waypoint x="114.9999999999917" y="178.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f1" id="BPMNEdge_f1">
<omgdi:waypoint x="214.94999999999868" y="178.1974308625642"></omgdi:waypoint>
<omgdi:waypoint x="271.4206140679542" y="178.42063490750846"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f2" id="BPMNEdge_f2">
<omgdi:waypoint x="310.528864436638" y="178.41203705401028"></omgdi:waypoint>
<omgdi:waypoint x="349.99999999999466" y="178.23018428758581"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f3" id="BPMNEdge_f3">
<omgdi:waypoint x="291.0" y="197.94783897384158"></omgdi:waypoint>
<omgdi:waypoint x="291.0" y="314.1342468261719"></omgdi:waypoint>
<omgdi:waypoint x="866.0" y="314.1342468261719"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f4" id="BPMNEdge_f4">
<omgdi:waypoint x="449.9499999999953" y="178.20726141078836"></omgdi:waypoint>
<omgdi:waypoint x="500.4166666666667" y="178.41666666666669"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="f5" id="BPMNEdge_f5">
<omgdi:waypoint x="539.5247370727428" y="178.41666666666663"></omgdi:waypoint>
<omgdi:waypoint x="584.9999999999953" y="178.21812227074233"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
4、添加关于FlowableConfig 的配置文件,防止乱码
@Configuration
public class FlowableConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
@Override
public void configure(SpringProcessEngineConfiguration engineConfiguration) {
engineConfiguration.setActivityFontName("宋体");
engineConfiguration.setLabelFontName("宋体");
engineConfiguration.setAnnotationFontName("宋体");
}
}
5、启动的时候需要动态部署流程实例
这里交给CommandLineRunner 方式来实现
@SpringBootApplication
public class MybatisPlusApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class, args);
}
@Resource
private RepositoryService repositoryService;
@Override
public void run(String... args) {
InputStream inputStream = this.getClass().getResourceAsStream("/process/投诉建议.bpmn20.xml");
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
deploymentBuilder.addInputStream("投诉建议.bpmn20.xml", inputStream);
deploymentBuilder.deploy();
}
}
6、controller测试类
package com.yin.demo.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.*;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.identitylink.api.history.HistoricIdentityLink;
import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.task.api.Task;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/flower")
@Api(tags = {"流程引擎-API"})
public class FlowerController {
@Resource
private RuntimeService runtimeService;
@Resource
private TaskService taskService;
@Resource
private RepositoryService repositoryService;
@Resource
private ProcessEngine processEngine;
/**
* 启动流程()
* @param roleId 角色id(我这里配置候选用户,这里就是用roleId)
*/
@PostMapping(value = "add")
@ApiOperation("创建流程引擎")
public String addExpense(String roleId) {
//启动流程
HashMap<String, Object> map = new HashMap<>(4);
//name="客服代表" flowable:candidateGroups="${customerServiceId}"
map.put("customerServiceId", roleId);
//<process id="adviceApply" name="投诉建议" isExecutable="true">
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("adviceApply", map);
return "提交成功.流程Id为:" + processInstance.getId();
}
/**
* 流转
*
* @param processInstanceId 流程id
*/
@PutMapping(value = "apply")
@ApiOperation("流传流程")
public String apply(String processInstanceId,Long userId,boolean isPass) {
// Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
//查询当前办理人的任务ID
Task task = taskService.createTaskQuery()
//使用流程实例ID
.processInstanceId(processInstanceId)
//任务办理人
.singleResult();
if (task == null) {
throw new RuntimeException("流程不存在");
}
//通过审核
HashMap<String, Object> map = new HashMap<>();
map.put("taskUser", userId);
String pass = "NO";
if(isPass){
pass = "YES";
}
map.put("finishFlag", pass);
taskService.complete(task.getId(), map);
return "processed ok!";
}
/**
* 生成流程图
*
* @param processId 任务ID
*/
@GetMapping(value = "processDiagram")
@ApiOperation("生成流程图")
public void genProcessDiagram(HttpServletResponse httpServletResponse, String processId) throws Exception {
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();
//流程走完的不显示图
if (pi == null) {
return;
}
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, engconf.getActivityFontName(), engconf.getLabelFontName(), engconf.getAnnotationFontName(), engconf.getClassLoader(), 1.0);
OutputStream out = null;
byte[] buf = new byte[1024];
int legth = 0;
try {
out = httpServletResponse.getOutputStream();
while ((legth = in.read(buf)) != -1) {
out.write(buf, 0, legth);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
演示
创建一个流程
查看当前流程
进行流程流转
再次查看当前流程
附录
这里面的演示都使用很多很多service,这里简单讲解一下如何做出信息