SpringBoot+flowable快速實(shí)現(xiàn)工作流,超級方便
前言
工作流框架大家一定不陌生,各種OA系統(tǒng)里我們常常用到。
對于JAVA領(lǐng)域來說一說起工作流框架第一浮現(xiàn)我在腦海中的便是大名鼎鼎的Activiti了。很久以前學(xué)習(xí)Activiti框架時(shí)我也曾記錄過一篇文章。盡管當(dāng)時(shí)只是學(xué)習(xí)了一下在之后的相關(guān)工作和項(xiàng)目中并沒有用到,通過學(xué)習(xí)后了解了下, 僅對于知識廣度進(jìn)行了擴(kuò)寬。
最近在一個(gè)開源項(xiàng)目里見到有使用另一個(gè)工做流框架:flowable 。簡單用了下感覺還是挺方便的,于是乎決定還是要來使用下并在此做下記錄,便于后面用到時(shí)可以“拿來主義”,哈哈!
什么是flowable?
對于flowable是什么以及關(guān)于此框架的具體信息可以參看此項(xiàng)目的官方文檔:
https://www.flowable.org/docs/userguide/index.html
官網(wǎng)對于此項(xiàng)目如何使用有非常詳細(xì)的描述,只是目前還沒有對應(yīng)的中文文檔。
Flowable is a light-weight business process engine written in Java.
這是官網(wǎng)文檔對此框架的完美解釋:Flowable是一個(gè)用java語言寫的輕量級工作流引擎。
在簡單了解flowable后與activiti框架相比的第一感覺就是開發(fā)方便快速,易與springBoot等各種框架快速整合。如果項(xiàng)目中需要快速實(shí)現(xiàn)一些工作流的相關(guān)功能那么用此框架是一個(gè)不錯(cuò)的選擇。
使用版本
用測試方便,這里都使用springBoot和flowable最新的穩(wěn)定版本
springBoot版本:2.0.1.RELEASE
flowable版本:6.3.0
Flowable與springBoot項(xiàng)目整合
添加依賴
將flowable的依賴加入到POM中即可,flowable使用需要一個(gè)數(shù)據(jù)庫,這里為了方便我選擇mysql
<dependencies>
? ?<dependency>
? ? ? ?<groupId>org.springframework.boot</groupId>
? ? ? ?<artifactId>spring-boot-starter-web</artifactId>
? ?</dependency>
? ?<!--flowable工作流依賴-->
? ?<dependency>
? ? ? ?<groupId>org.flowable</groupId>
? ? ? ?<artifactId>flowable-spring-boot-starter</artifactId>
? ? ? ?<version>6.3.0</version>
? ?</dependency>
? ?<!--mysql依賴-->
? ?<dependency>
? ? ? ?<groupId>mysql</groupId>
? ? ? ?<artifactId>mysql-connector-java</artifactId>
? ? ? ?<version>5.1.45</version>
? ?</dependency>
</dependencies>
flowable配置
測試方便flowable配置為默認(rèn)的即可。為了測試時(shí)方便看日志信息,我這里將flowable的定時(shí)job功能暫時(shí)關(guān)閉,其他的都用默認(rèn)的
當(dāng)然記得要添加一個(gè)數(shù)據(jù)源,我這里添加的mysql,并且記得建好對應(yīng)的mysql庫,如果沒有建就自己建一個(gè)吧
like this:
CREATE DATABASE flowable-spring-boot DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
spring:
?datasource:
? ?url: jdbc:mysql://127.0.0.1:3306/flowable-spring-boot?characterEncoding=UTF-8
? ?username: root
? ?password: root
flowable:
#關(guān)閉定時(shí)任務(wù)JOB
?async-executor-activate: false
這樣操作后,flowable與springBoot的整個(gè)就完成了!個(gè)人非常方便!
然后就可以運(yùn)行了,初次運(yùn)行時(shí)flowable會(huì)將自動(dòng)執(zhí)行flowable中的初始化腳本完成工作流所需要的數(shù)據(jù)表的建立,如果指定的數(shù)據(jù)庫中還未創(chuàng)建過flowable的相關(guān)數(shù)據(jù)表的話。
定義流程文件
上面已經(jīng)完成了flowable與springboot的整合了,接下來就可以使用此框架進(jìn)行流程需要開發(fā)了!
同樣在flowable官方文檔中對于流程文件它有這樣的建議:
The Flowable engine expects processes to be defined in the BPMN 2.0 format, which is an XML standard that is widely accepted in the industry.
flowable建議采用業(yè)界標(biāo)準(zhǔn)BPMN2.0的XML來描述需要定義的工作流。
那么BPMN這個(gè)流程文件應(yīng)該怎么寫呢?
Typically, such a process definition is modeled with a visual modeling tool, such as the Flowable Designer (Eclipse) or the Flowable Modeler (web application).
上官方文檔中有看到這樣的描述后即便我不會(huì)寫也不怕了。通常都是通過專門的流程建模工具來畫出來的,可以用Eclipse里的流程插件來畫。同時(shí)Flowable也提供了對應(yīng)的web管理臺可以對流程文件進(jìn)行創(chuàng)建。詳見:
https://www.flowable.org/docs/userguide/index.html#flowableUIApps
為了方便測試,這里采用一個(gè)開源項(xiàng)目中的流程文件,其描述如下:
<?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: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="Expense" name="ExpenseProcess" isExecutable="true">
? ? ? ?<documentation>報(bào)銷流程</documentation>
? ? ? ?<startEvent id="start" name="開始"></startEvent>
? ? ? ?<userTask id="fillTask" name="出差報(bào)銷" flowable:assignee="${taskUser}">
? ? ? ? ? ?<extensionElements>
? ? ? ? ? ? ? ?<modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler">
? ? ? ? ? ? ? ? ? ?<![CDATA[false]]></modeler:initiator-can-complete>
? ? ? ? ? ?</extensionElements>
? ? ? ?</userTask>
? ? ? ?<exclusiveGateway id="judgeTask"></exclusiveGateway>
? ? ? ?<userTask id="directorTak" name="經(jīng)理審批">
? ? ? ? ? ?<extensionElements>
? ? ? ? ? ? ? ?<flowable:taskListener event="create"
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? class="com.haiyang.flowable.listener.ManagerTaskHandler"></flowable:taskListener>
? ? ? ? ? ?</extensionElements>
? ? ? ?</userTask>
? ? ? ?<userTask id="bossTask" name="老板審批">
? ? ? ? ? ?<extensionElements>
? ? ? ? ? ? ? ?<flowable:taskListener event="create"
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? class="com.haiyang.flowable.listener.BossTaskHandler"></flowable:taskListener>
? ? ? ? ? ?</extensionElements>
? ? ? ?</userTask>
? ? ? ?<endEvent id="end" name="結(jié)束"></endEvent>
? ? ? ?<sequenceFlow id="directorNotPassFlow" name="駁回" sourceRef="directorTak" targetRef="fillTask">
? ? ? ? ? ?<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=='駁回'}]]></conditionExpression>
? ? ? ?</sequenceFlow>
? ? ? ?<sequenceFlow id="bossNotPassFlow" name="駁回" sourceRef="bossTask" targetRef="fillTask">
? ? ? ? ? ?<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=='駁回'}]]></conditionExpression>
? ? ? ?</sequenceFlow>
? ? ? ?<sequenceFlow id="flow1" sourceRef="start" targetRef="fillTask"></sequenceFlow>
? ? ? ?<sequenceFlow id="flow2" sourceRef="fillTask" targetRef="judgeTask"></sequenceFlow>
? ? ? ?<sequenceFlow id="judgeMore" name="大于500元" sourceRef="judgeTask" targetRef="bossTask">
? ? ? ? ? ?<conditionExpression xsi:type="tFormalExpression"><![CDATA[${money > 500}]]></conditionExpression>
? ? ? ?</sequenceFlow>
? ? ? ?<sequenceFlow id="bossPassFlow" name="通過" sourceRef="bossTask" targetRef="end">
? ? ? ? ? ?<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=='通過'}]]></conditionExpression>
? ? ? ?</sequenceFlow>
? ? ? ?<sequenceFlow id="directorPassFlow" name="通過" sourceRef="directorTak" targetRef="end">
? ? ? ? ? ?<conditionExpression xsi:type="tFormalExpression"><![CDATA[${outcome=='通過'}]]></conditionExpression>
? ? ? ?</sequenceFlow>
? ? ? ?<sequenceFlow id="judgeLess" name="小于500元" sourceRef="judgeTask" targetRef="directorTak">
? ? ? ? ? ?<conditionExpression xsi:type="tFormalExpression"><![CDATA[${money <= 500}]]></conditionExpression>
? ? ? ?</sequenceFlow>
? ?</process>
? ?<bpmndi:BPMNDiagram id="BPMNDiagram_Expense">
? ? ? ?<bpmndi:BPMNPlane bpmnElement="Expense" id="BPMNPlane_Expense">
? ? ? ? ? ?<bpmndi:BPMNShape bpmnElement="start" id="BPMNShape_start">
? ? ? ? ? ? ? ?<omgdc:Bounds height="30.0" width="30.0" x="285.0" y="135.0"></omgdc:Bounds>
? ? ? ? ? ?</bpmndi:BPMNShape>
? ? ? ? ? ?<bpmndi:BPMNShape bpmnElement="fillTask" id="BPMNShape_fillTask">
? ? ? ? ? ? ? ?<omgdc:Bounds height="80.0" width="100.0" x="405.0" y="110.0"></omgdc:Bounds>
? ? ? ? ? ?</bpmndi:BPMNShape>
? ? ? ? ? ?<bpmndi:BPMNShape bpmnElement="judgeTask" id="BPMNShape_judgeTask">
? ? ? ? ? ? ? ?<omgdc:Bounds height="40.0" width="40.0" x="585.0" y="130.0"></omgdc:Bounds>
? ? ? ? ? ?</bpmndi:BPMNShape>
? ? ? ? ? ?<bpmndi:BPMNShape bpmnElement="directorTak" id="BPMNShape_directorTak">
? ? ? ? ? ? ? ?<omgdc:Bounds height="80.0" width="100.0" x="735.0" y="110.0"></omgdc:Bounds>
? ? ? ? ? ?</bpmndi:BPMNShape>
? ? ? ? ? ?<bpmndi:BPMNShape bpmnElement="bossTask" id="BPMNShape_bossTask">
? ? ? ? ? ? ? ?<omgdc:Bounds height="80.0" width="100.0" x="555.0" y="255.0"></omgdc:Bounds>
? ? ? ? ? ?</bpmndi:BPMNShape>
? ? ? ? ? ?<bpmndi:BPMNShape bpmnElement="end" id="BPMNShape_end">
? ? ? ? ? ? ? ?<omgdc:Bounds height="28.0" width="28.0" x="771.0" y="281.0"></omgdc:Bounds>
? ? ? ? ? ?</bpmndi:BPMNShape>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
? ? ? ? ? ? ? ?<omgdi:waypoint x="315.0" y="150.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="405.0" y="150.0"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
? ? ? ? ? ? ? ?<omgdi:waypoint x="505.0" y="150.16611295681062"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="585.4333333333333" y="150.43333333333334"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="judgeLess" id="BPMNEdge_judgeLess">
? ? ? ? ? ? ? ?<omgdi:waypoint x="624.5530726256983" y="150.44692737430168"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="735.0" y="150.1392757660167"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="directorNotPassFlow" id="BPMNEdge_directorNotPassFlow">
? ? ? ? ? ? ? ?<omgdi:waypoint x="785.0" y="110.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="785.0" y="37.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="455.0" y="37.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="455.0" y="110.0"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="bossPassFlow" id="BPMNEdge_bossPassFlow">
? ? ? ? ? ? ? ?<omgdi:waypoint x="655.0" y="295.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="771.0" y="295.0"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="judgeMore" id="BPMNEdge_judgeMore">
? ? ? ? ? ? ? ?<omgdi:waypoint x="605.4340277777778" y="169.56597222222223"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="605.1384083044983" y="255.0"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="directorPassFlow" id="BPMNEdge_directorPassFlow">
? ? ? ? ? ? ? ?<omgdi:waypoint x="785.0" y="190.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="785.0" y="281.0"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ? ? ?<bpmndi:BPMNEdge bpmnElement="bossNotPassFlow" id="BPMNEdge_bossNotPassFlow">
? ? ? ? ? ? ? ?<omgdi:waypoint x="555.0" y="295.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="455.0" y="295.0"></omgdi:waypoint>
? ? ? ? ? ? ? ?<omgdi:waypoint x="455.0" y="190.0"></omgdi:waypoint>
? ? ? ? ? ?</bpmndi:BPMNEdge>
? ? ? ?</bpmndi:BPMNPlane>
? ?</bpmndi:BPMNDiagram>
</definitions>
其中的兩個(gè)代理類為:
import org.flowable.engine.delegate.TaskListener;
import org.flowable.task.service.delegate.DelegateTask;
public class ManagerTaskHandler implements TaskListener {
? ?@Override
? ?public void notify(DelegateTask delegateTask) {
? ? ? ?delegateTask.setAssignee("經(jīng)理");
? ?}
}
public class BossTaskHandler implements TaskListener {
? ?@Override
? ?public void notify(DelegateTask delegateTask) {
? ? ? ?delegateTask.setAssignee("老板");
? ?}
}
為了方便,也可以去掉這兩個(gè)JAVA類,將其對應(yīng)的task改寫為如下的形式:
<userTask id="holidayApprovedTask" name="Holiday approved" flowable:assignee="${employee}"/>
盡管上面的BPMN文件很長,但放心,畢竟那是通過相關(guān)的工具生成出來的,對于核心的邏輯部分也很少(主要在process 標(biāo)簽內(nèi)) ,如需要詳細(xì)了解的可自行學(xué)習(xí)下BPMN的標(biāo)簽即可!當(dāng)然,在flowable的使用文檔中也有相關(guān)的描述,詳見:
https://www.flowable.org/docs/userguide/index.html#configuration
如上定義好一個(gè)流程文件后,將其命令為ExpenseProcess.bpmn20.xml并將其放于項(xiàng)目中的resource目錄下的processes(如此目錄不存在自行創(chuàng)建)目錄下就可以了。
like this:

這樣當(dāng)此框架啟動(dòng)的時(shí)候它會(huì)默認(rèn)加載resource目錄下的processes時(shí)就可以將此流程配置加載到數(shù)據(jù)庫進(jìn)行持久化了
測試controller
為了方便這里通過一個(gè)controller來完成此DEMO的快速編寫
@Controller
@RequestMapping(value = "expense")
public class ExpenseController {
? ?@Autowired
? ?private RuntimeService runtimeService;
? ?@Autowired
? ?private TaskService taskService;
? ?@Autowired
? ?private RepositoryService repositoryService;
? ?@Autowired
? ?private ProcessEngine processEngine;
/***************此處為業(yè)務(wù)代碼******************/
}
寫一個(gè)controller,并注入由flowable框架啟動(dòng)時(shí)自動(dòng)注冊的幾個(gè)bean,下面的功能將會(huì)用到!
開始流程
/**
* 添加報(bào)銷
*
* @param userId ? ?用戶Id
* @param money ? ? 報(bào)銷金額
* @param descption 描述
*/
@RequestMapping(value = "add")
@ResponseBody
public String addExpense(String userId, Integer money, String descption) {
? ?//啟動(dòng)流程
? ?HashMap<String, Object> map = new HashMap<>();
? ?map.put("taskUser", userId);
? ?map.put("money", money);
? ?ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("Expense", map);
? ?return "提交成功.流程Id為:" + processInstance.getId();
}
上面的代碼通過接收用戶的一個(gè)請求傳入用戶的ID和金額以及描述信息來開啟一個(gè)報(bào)銷流程,并返回給用戶這個(gè)流程的Id
查詢流程列表,待辦列表
/**
* 獲取審批管理列表
*/
@RequestMapping(value = "/list")
@ResponseBody
public Object list(String userId) {
? ?List<Task> tasks = taskService.createTaskQuery().taskAssignee(userId).orderByTaskCreateTime().desc().list();
? ?for (Task task : tasks) {
? ? ? ?System.out.println(task.toString());
? ?}
? ?return tasks.toArray().toString();
}
通過上面的代碼獲取出此用戶需要處理的流程。
批準(zhǔn),同意
/**
* 批準(zhǔn)
*
* @param taskId 任務(wù)ID
*/
@RequestMapping(value = "apply")
@ResponseBody
public String apply(String taskId) {
? ?Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
? ?if (task == null) {
? ? ? ?throw new RuntimeException("流程不存在");
? ?}
? ?//通過審核
? ?HashMap<String, Object> map = new HashMap<>();
? ?map.put("outcome", "通過");
? ?taskService.complete(taskId, map);
? ?return "processed ok!";
}
通過前端傳入的任務(wù)ID來對此流程進(jìn)行同意處理
拒絕,不同意
/**
* 拒絕
*/
@ResponseBody
@RequestMapping(value = "reject")
public String reject(String taskId) {
? ?HashMap<String, Object> map = new HashMap<>();
? ?map.put("outcome", "駁回");
? ?taskService.complete(taskId, map);
? ?return "reject";
}
生成當(dāng)前流程圖表
? ?/**
? ? * 生成流程圖
? ? *
? ? * @param processId 任務(wù)ID
? ? */
? ?@RequestMapping(value = "processDiagram")
? ?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();
? ? ? ?//使用流程實(shí)例ID,查詢正在執(zhí)行的執(zhí)行對象表,返回流程實(shí)例對象
? ? ? ?String InstanceId = task.getProcessInstanceId();
? ? ? ?List<Execution> executions = runtimeService
? ? ? ? ? ? ? ?.createExecutionQuery()
? ? ? ? ? ? ? ?.processInstanceId(InstanceId)
? ? ? ? ? ? ? ?.list();
? ? ? ?//得到正在執(zhí)行的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();
? ? ? ? ? ?}
? ? ? ?}
? ?}
通過傳入流程ID生成當(dāng)前流程的流程圖給前端,如果流程中使用到中文且生成的圖片是亂碼的,則需要進(jìn)配置下字體:
/**
* @author haiyangp
* desc: flowable配置----為放置生成的流程圖中中文亂碼
*/
@Configuration
public class FlowableConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
? ?@Override
? ?public void configure(SpringProcessEngineConfiguration engineConfiguration) {
? ? ? ?engineConfiguration.setActivityFontName("宋體");
? ? ? ?engineConfiguration.setLabelFontName("宋體");
? ? ? ?engineConfiguration.setAnnotationFontName("宋體");
? ?}
}
整體演示
上面的代碼寫好后就可以演示下整體流程了
1.先啟動(dòng)好此項(xiàng)目,然后創(chuàng)建一個(gè)流程:
訪問:http://localhost:8080/expense/add?userId=123&money=123321
返回:提交成功.流程Id為:2501
2. 查詢待辦列表:
訪問:http://localhost:8080/expense/list?userId=123
輸出:Task[id=2507, name=出差報(bào)銷]
3. 同意:
訪問:http://localhost:8080/expense/apply?taskId=2507
返回:processed ok!
4. 生成流程圖:
訪問:http://localhost:8080/expense/processDiagram?processId=2501
返回如下圖片:

整體流程截圖如下:

總結(jié)
通過springBoot與flowable的整合體驗(yàn)到了工作流的開發(fā)原來如此簡單方便。
給此框架點(diǎn)贊,向巨人們致敬!
本文源碼地址:https://github.com/puhaiyang/flowable-springboot