一文带你入门Flowable流程引擎

5,173 阅读25分钟

一、Flowable简介

Flowable官网:www.flowable.com/

Flowable Documentation:documentation.flowable.com/latest/mode…

Flowable User Guide:www.flowable.com/open-source…

1.1 What is Flowable Work?

官网简介:

Flowable Work is a flexible technology for delivering dynamic and adaptive process and case management solutions.

With Flowable Work you can model your business application, through processes, decision rules, case structures and forms. Visual editing tools make it easy for the business modeler – expert or citizen developer – to create rich and sophisticated business models. These models can then be executed dynamically by Flowable Work to deliver an adaptable business application. This enables a solution to be continuously improved, being as agile as the circumstances require. Flowable Work is built on the trusted and highly scalable open source Flowable Engine, which provides the engines for driving models defined with the open standards BPMN, CMMN and DMN.

图1 业务流转示意图(图片来源:www.flowable.com)

  1. Flowable 是一个使用 Java 编写的轻量级业务流程引擎,使用 Apache V2 license 协议开源。

  2. 基于 Activiti v6 beta4 发布的第一个 Flowable release 版本为 6.0。

  3. Flowable 项目中多种引擎模块,并已有应用及UI示例。

    a. 引擎分类

    1. BPMN(Business Process Model and Notation):业务流程引擎
    2. CMMN(Case Management Model and Notation):案例模型引擎
    3. DMN(Decision Model and Notation):决策引擎
    4. 表单引擎(Form Engine)

    b. 应用类型

    1. Flowable Task:任务应用程序,访问您的完成任务列表, 并处理从任何流程应用程序分配给您的任何任务。此外, 启动新的进程和任务。
    2. Flowable Modeler:建模器应用程序,访问中心位置以对BPMN流程,DMN决策表,表单定义进行建模并创建应用程序定义。
    3. Flowable Admin:管理员应用程序, 访问Flowable Admin应用程序。
    4. Flowable IDM:身份管理应用程序,访问中心位置以定义用户,组和特权。
  4. Flowable架构示意图

图2 Flowable架构示意图(图片来源:www.shareniu.com)

1.2 Flowable BPMN业务流程引擎

  1. 事件(event):用于为流程生命周期中发生的事情建模

    • 捕获(catching):当流程执行到达这个事件时,会等待直到触发器动作
    • 抛出(throwing):当流程执行到达这个事件时,会触发一个触发器
    • 具体事件包括定时器事件、启动事件、结束事件、消息事件、信号事件、边界事件等
  2. 顺序流(sequence flow):是流程中两个元素间的连接器

    • 在流程执行过程中,一个元素被访问后,会沿着其所有出口顺序流继续执行
    • BPMN2.0默认是并行执行
    • 顺序流上定义条件(conditional sequence flow)时为条件顺序流。条件计算为 true 时,选择该出口顺序流;选择多条顺序流,会生成多个执行(流以并行方式继续),不适用于网关。
    • 网关默认都可以使用默认顺序流。
  3. 网关(gateway):控制执行的流向。网关可以消费(consuming)与生成(generating)标志

    • 排他网关(exclusive gateway):也叫异或网关 (XOR gateway),或者基于数据的排他网关 (exclusive data-based gateway),用于对流程中的决策建模。

      • 选择第一个条件计算为 true 的顺序流

      • 只会选择一条顺序流

      • 多条为true,则选择XML中最先定义的顺序流

    • 并行网关:不计算条件,如果连接到并行网关的顺序流上定义了条件,会直接忽略该条件

      • 可以分支为多条
      • 可以合并为单条
      • 分支与合并同时出现多条时,先合并入口顺序流再分裂成多条并行执行路径
    • 包容网关:排他网关与并行网关的组合

      • 计算条件
      • 可以多条出口
    • 基于事件的网关(event-based gateway):提供了根据事件做选择的方式。

      • 网关的每一条出口顺序流都需要连接至一个捕获中间事件。
      • 一个基于事件的网关,必须有两条或更多的出口顺序流。
  4. 任务:常用为用户任务、业务规则任务、邮件任务、任务监听器、执行监听器等。

  5. 子流程与调用活动

    • 子流程完全在父流程中定义(所以也称作嵌入式子流程)
    • 调用活动(call activity)有别于一般的子流程,调用活动引用一个流程定义外部的流程,而子流程嵌入在原有流程定义内。调用活动的主要使用场景是,在多个不同流程定义中调用一个可复用的流程定义。

Flowable以事务的方式执行流程,如果 Flowable 被触发(启动流程,完成任务,为执行发送信号),Flowable 将沿流程执行,直到到达每个执行路径的等待状态。

更具体地说,它以深度优先方式搜索流程图,并在每个执行分支都到达等待状态时返回。等待状态是「之后」再执行的任务,也就是说着 Flowable 将当前执行持久化,并等待再次触发。触发可以来自外部来源如用户任务或消息接受任务,也可以来自 Flowable 自身如定时器事件。

  1. Flowable Web应用:
  • Flowable Modeler: 让具有建模权限的用户可以创建流程模型、表单、选择表与应用定义。
  • Flowable Task: 运行时任务应用。提供了启动流程实例、编辑任务表单、完成任务,以及查询流程实例与任务的功能。
  • Flowable Admin: 管理应用。让具有管理员权限的用户可以查询 BPMN、DMN、Form 及 Content 引擎,并提供了许多选项用于修改流程实例、任务、作业等。管理应用通过 REST API 连接至引擎,并与 Flowable Task 应用及 Flowable REST 应用一同部署。
  • 所有其他的应用都需要 Flowable IDM 提供认证。所有其他的应用都需要 Flowable IDM 提供认证。

1.3 Flowable DMN决策引擎

作为以 BPMN 为核心的工作流引擎,Flowable 原本与规则引擎的关联并不强,但实际业务流程中,有时需要由多个决策来决定流程走向,而每个决策都要根据自身的规则来决定,每个决策之间也可能存在关联。此时就需要规则引擎来提供决策支撑。

  1. DMN 定义由决策(decision)和其他东西组成,决策由表达式描述。

  2. 目前在 Flowable DMN 中仅支持决策表(decision table)类型的表达式。

  3. 决策表分为输入表达式与输出表达式两个主要区域。在输入表达式中,可以定义变量,用于规则输入项(input entries)的表达式。可以通过选择 Add Input(添加输入),定义多个输入表达式。在输出表达式中,可以定义选择表执行结果要创建的变量(变量的值将用于输出项表达式)。可以通过选择 Add Output(添加输出),定义多个输出表达式。

  4. 在决策表编辑界面,可以选择命中策略,共有两大类(单命中、多命中)七种命中策略可选:

    1. 单命中、第一命中(single hit & FIRST):多个规则允许交叉,执行从上到下的第一条命中项。
    2. 单命中、唯一命中(single hit & UNIQUE):多个规则不允许交叉,执行从上到下的第一条唯一命中项。
    3. 单命中、任一命中(single hit & ANY):规则允许交叉,但是所有输出的优先级相同,随机执行一条命中项。
    4. 单命中、优先级(single hit & PRIORITY):多个命中规则的优先级不同,执行优先级最高的那条。
    5. 多命中、输出优先级排序(multiple hit & OUTPUT ORDER):按照输出优先级递减的顺序返回所有命中。
    6. 多命中、规则顺序排序(multiple hit & RULE ORDER):按照规则顺序返回所有命中。
    7. 多命中、聚合(multiple hit & COLLECT):按照随机顺序返回所有命中。

以上,Flowable简单介绍到此结束,本文暂未介绍CMMN案例模型引擎和Form表单引擎,如需详细介绍,请参阅参考文章:xie.infoq.cn/article/ece…

二、Flowable流程设计器及其元素介绍

2.1 下载Flowable

GitHub地址如下,选择flowable-6.7.2.zip下载并解压 github.com/flowable/fl…

2.2 启动Flowable

  1. 这里选择使用Tomcat的方式启动,打开下载解压后的文件夹中K:\Files\flowable-6.7.2\wars

  1. 将上述wars文件夹下的war包全部复制到Tomcat的webapps文件夹下

  2. 打开命令行,进入Tomcat的bin目录下,输入.\startup.bat启动Tomcat

    • Tomcat在启动时会自动加载webapps文件夹下的war包,加载成功后在webapps文件夹中会多出flowable-restflowable-ui两个文件夹
  3. 打开浏览器,输入http://localhost:8080/flowable-ui启动Flowable

  • Flowable 请求认证方式:basic authentication
  • 首次启动时,需要输入账号密码
    • 账号:admin
    • 密码:test

2.3 流程表单设计器

  1. 进入UI界面,选择建模器应用程序进入流程表单设计器

  1. 创建一个业务流程模型,这里以请假流程为例

  1. 流程设计

  1. 流程说明:简单的流程可以直接串行,并配置对应各个节点的人员即可,人员在IDM(身份管理应用程序)中被管理,在流程设计中,人员配置可以使用EL表达式或者字符串,后续将对模型导出的XML文件进行说明!

    1. 开始事件:指示流程从哪里开始,可设置属性如图8所示。

    1. 用户任务:配置用户操作及人员,多实例为会签

    1. 结束事件:指示流程路径的结束为止

2.4 流程组件说明

组件内容过多,具体请查看官网:documentation.flowable.com/latest/user…

三、Flowable流程图

3.1 Flowable-6.7.2版本流程图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" exporter="Flowable Open Source Modeler" exporterVersion="6.7.2">
  <process id="qingjia" name="请假流程" isExecutable="true">
    <documentation>请假流程模型</documentation>
    <startEvent id="startEvent1" name="开始" flowable:formFieldValidation="true"></startEvent>
    <userTask id="sid-1CB72ABE-DAC8-4F13-8F6E-C826696A3A18" name="申请" flowable:formFieldValidation="true"></userTask>
    <sequenceFlow id="sid-E0BD1161-9318-4E31-A737-26E09B1D6FEF" sourceRef="startEvent1" targetRef="sid-1CB72ABE-DAC8-4F13-8F6E-C826696A3A18"></sequenceFlow>
    <userTask id="sid-726F3B8B-A566-48C8-AE9B-A02A2BA65858" name="组长审批" flowable:formFieldValidation="true"></userTask>
    <sequenceFlow id="sid-EF179399-3D9C-42D6-9DB9-5836A547DFF4" sourceRef="sid-1CB72ABE-DAC8-4F13-8F6E-C826696A3A18" targetRef="sid-726F3B8B-A566-48C8-AE9B-A02A2BA65858"></sequenceFlow>
    <userTask id="sid-52DAF4F5-C954-400F-BED4-B3D651FB00F3" name="经理审批" flowable:formFieldValidation="true"></userTask>
    <sequenceFlow id="sid-09BE1D1C-E264-4546-8593-AA9809D48D6A" sourceRef="sid-726F3B8B-A566-48C8-AE9B-A02A2BA65858" targetRef="sid-52DAF4F5-C954-400F-BED4-B3D651FB00F3"></sequenceFlow>
    <endEvent id="sid-FEF12675-D998-4B95-99AE-52F18D87C01B" name="结束"></endEvent>
    <sequenceFlow id="sid-CBC16578-F5F9-4B6D-A0B9-84548B4CF047" sourceRef="sid-52DAF4F5-C954-400F-BED4-B3D651FB00F3" targetRef="sid-FEF12675-D998-4B95-99AE-52F18D87C01B"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_qingjia">
    <bpmndi:BPMNPlane bpmnElement="qingjia" id="BPMNPlane_qingjia">
      <bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
        <omgdc:Bounds height="30.0" width="30.0" x="90.0" y="163.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-1CB72ABE-DAC8-4F13-8F6E-C826696A3A18" id="BPMNShape_sid-1CB72ABE-DAC8-4F13-8F6E-C826696A3A18">
        <omgdc:Bounds height="80.0" width="100.0" x="195.0" y="138.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-726F3B8B-A566-48C8-AE9B-A02A2BA65858" id="BPMNShape_sid-726F3B8B-A566-48C8-AE9B-A02A2BA65858">
        <omgdc:Bounds height="80.0" width="100.0" x="375.0" y="138.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-52DAF4F5-C954-400F-BED4-B3D651FB00F3" id="BPMNShape_sid-52DAF4F5-C954-400F-BED4-B3D651FB00F3">
        <omgdc:Bounds height="80.0" width="100.0" x="525.0" y="138.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-FEF12675-D998-4B95-99AE-52F18D87C01B" id="BPMNShape_sid-FEF12675-D998-4B95-99AE-52F18D87C01B">
        <omgdc:Bounds height="28.0" width="28.0" x="690.0" y="164.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="sid-CBC16578-F5F9-4B6D-A0B9-84548B4CF047" id="BPMNEdge_sid-CBC16578-F5F9-4B6D-A0B9-84548B4CF047" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="14.0" flowable:targetDockerY="14.0">
        <omgdi:waypoint x="624.9499999999999" y="178.0"></omgdi:waypoint>
        <omgdi:waypoint x="690.0" y="178.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-E0BD1161-9318-4E31-A737-26E09B1D6FEF" id="BPMNEdge_sid-E0BD1161-9318-4E31-A737-26E09B1D6FEF" flowable:sourceDockerX="15.0" flowable:sourceDockerY="15.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
        <omgdi:waypoint x="119.94999906759469" y="178.0"></omgdi:waypoint>
        <omgdi:waypoint x="194.99999999996822" y="178.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-EF179399-3D9C-42D6-9DB9-5836A547DFF4" id="BPMNEdge_sid-EF179399-3D9C-42D6-9DB9-5836A547DFF4" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
        <omgdi:waypoint x="294.9499999999431" y="178.0"></omgdi:waypoint>
        <omgdi:waypoint x="374.99999999997226" y="178.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-09BE1D1C-E264-4546-8593-AA9809D48D6A" id="BPMNEdge_sid-09BE1D1C-E264-4546-8593-AA9809D48D6A" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
        <omgdi:waypoint x="474.9499999999581" y="178.0"></omgdi:waypoint>
        <omgdi:waypoint x="524.9999999999363" y="178.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

3.2 Flowable-6.3版本流程图及其说明

6.7版本也可参考,基本一致

其实就是把流程图的各种线条逻辑,用Flowable自己的XML标签描绘出来了。

<process> : 表示一个完整的工作流

<documentation> : 对工作流的描述

<startEvent> : 工作流中起点位置(开始)

<endEvent> : 工作流中结束位置(结束)

<userTask> : 代表一个任务审核节点(组长、经理等角色)

<exclusiveGateway> : 逻辑判断节点,相当于流程图中的菱形框

<sequenceFlow> :链接各个节点的线条,sourceRef 属性表示线的起始节点,targetRef 属性表示线指向的节点。

四、Flowable API 简单案例

4.1 简单案例

API 文档:www.flowable.com/open-source…

4.2 FlowableAPI其余情况

  1. 查询API中不能满足查询要求,可自行构建SQL查询
List<Task> tasks = taskService.createNativeTaskQuery()
  .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) +
      " T WHERE T.NAME_ = #{taskName}")
  .parameter("taskName", "gonzoTask")
  .list();

long count = taskService.createNativeTaskQuery()
  .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T1, " +
      managementService.getTableName(VariableInstanceEntity.class) + " V1 WHERE V1.TASK_ID_ = T1.ID_")
  .count();
  1. 流程变量 流程实例可以持有任意数量的变量,每个变量存储为ACT_RU_VARIABLE数据库表的一行。
    也可以设置局部变量,局部变量生存期为任务持续的时间。(流程变量方法名末尾带上Local)

五、Flowable数据库表说明

表分类表名表说明
一般数据(2)ACT_GE_BYTEARRAY通用的流程定义和流程资源
ACT_GE_PROPERTY系统相关属性
流程历史记录(8)ACT_HI_ACTINST历史的流程实例
ACT_HI_ATTACHMENT历史的流程附件
ACT_HI_COMMENT历史的说明性信息
ACT_HI_DETAIL历史的流程运行中的细节信息
ACT_HI_IDENTITYLINK历史的流程运行过程中用户关系
ACT_HI_PROCINST历史的流程实例
ACT_HI_TASKINST历史的任务实例
ACT_HI_VARINST历史的流程运行中的变量信息
用户用户组表(9)ACT_ID_BYTEARRAY二进制数据表
ACT_ID_GROUP用户组信息表
ACT_ID_INFO用户信息详情表
ACT_ID_MEMBERSHIP人与组关系表
ACT_ID_PRIV权限表
ACT_ID_PRIV_MAPPING用户或组权限关系表
ACT_ID_PROPERTY属性表
ACT_ID_TOKEN系统登录日志表
ACT_ID_USER用户表
流程定义表(3)ACT_RE_DEPLOYMENT部署单元信息
ACT_RE_MODEL模型信息
ACT_RE_PROCDEF已部署的流程定义
运行实例表(10)ACT_RU_DEADLETTER_JOB正在运行的任务表
ACT_RU_EVENT_SUBSCR运行时事件
ACT_RU_EXECUTION运行时流程执行实例
ACT_RU_HISTORY_JOB历史作业表
ACT_RU_IDENTITYLINK运行时用户关系信息
ACT_RU_JOB运行时作业表
ACT_RU_SUSPENDED_JOB暂停作业表
ACT_RU_TASK运行时任务表
ACT_RU_TIMER_JOB定时作业表
ACT_RU_VARIABLE运行时变量表
其他表(2)ACT_EVT_LOG事件日志表
ACT_PROCDEF_INFO流程定义信息

六、Flowable整合SpringBoot

6.1 自动部署

  1. 默认放于项目resources目录下的文件会被自动部署
  • processes:目录下任何BPMN 2.0流程定义都会被自动部署。
  • cases:目录下的任何CMMN 1.1事例都会被自动部署。
  • forms:目录下的任何Form定义都会被自动部署
  1. 也可以在配置文件中显式增加自动部署
flowable:
  check-process-definitions: true #启用自动部署
  process-definition-location-prefix: classpath:/processes/ #流程定义文件位置

6.2 Flowable异常

尽管我们想避免过大的异常层次结构,但在特定情况下仍然会抛出下述异常子类。所有流程执行与API调用中发生的错误,如果不符合下面列出的异常,会统一抛出FlowableExceptions

  • FlowableWrongDbException: 当Flowable引擎检测到数据库表结构版本与引擎版本不匹配时抛出。
  • FlowableOptimisticLockingException: 当对同一数据实体的并发访问导致数据存储发生乐观锁异常时抛出。
  • FlowableClassLoadingException: 当需要载入的类(如JavaDelegate, TaskListener, …)无法找到,或载入发生错误时抛出。
  • FlowableObjectNotFoundException: 当请求或要操作的对象不存在时抛出。
  • FlowableIllegalArgumentException: 当调用Flowable API时使用了不合法的参数时抛出。可能是引擎配置中的不合法值,或者是API调用传递的不合法参数,也可能是流程定义中的不合法值。
  • FlowableTaskAlreadyClaimedException: 当对已被认领的任务调用taskService.claim(…)时抛出。

6.3 整合SpringBoot

  1. 导入maven依赖
<dependency>
    <groupId>org.flowable</groupId>
    <artifactId>flowable-spring-boot-starter</artifactId>
    <version>6.7.2</version>
</dependency>
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.21</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.21</version>
</dependency>
  1. 添加日志配置
log4j.rootLogger=DEBUG, CA

log4j.appender.CA=org.apache.log4j.ConsoleAppender
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
log4j.appender.CA.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n
  1. 配置bootstrap
  • 第一次运行时需要将database-schema-update改为true
  • 第一次运行时会自动生成数据库表,默认使用H2,也可以使用MySQL数据库,需在POM文件中添加数据库驱动,并在bootstrap中添加配置
flowable:
  common:
    app:
      idm-url: http://localhost:8989/flowable-idm
      idm-admin:
        user: admin
        password: test
  async-executor-activate: false #关闭定时任务JOB
  database-schema-update: false #关闭flowable数据库自动更新
  check-process-definitions: true #启用自动部署
  process-definition-location-prefix: classpath:/processes/ #流程定义文件位置
  1. 创建一个简单的流程模型文件holiday.bpmn20.xml
<!-- Flowable模型文件,未包含流程图BPMNDiagram -->
<?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" 
             exporter="Flowable Open Source Modeler" exporterVersion="6.7.2">

  <process id="holiday" name="请假流程" isExecutable="true">
    <documentation>请假流程</documentation>
    <!-- 开始事件 -->
    <startEvent id="startEvent" name="开始" flowable:formFieldValidation="true" 
                flowable:initiator="INITIATOR">
      <documentation>开始</documentation>
    </startEvent>
    
    <!-- 用户任务 - 申请 -->
    <userTask id="apply" name="提交申请" flowable:assignee="${INITIATOR}" 
              flowable:formFieldValidation="true">
      <documentation>提交申请</documentation>
      <extensionElements>
        <modeler:activiti-idm-initiator xmlns:modeler="http://flowable.org/modeler"><![CDATA[true]]></modeler:activiti-idm-initiator>
        <flowable:taskListener event="complete" expression="${ApplyTaskListener}"/>
      </extensionElements>
    </userTask>
    <sequenceFlow id="sid-42E49640-19BA-4617-84A3-3D98650F2B51" 
                  sourceRef="start" targetRef="apply"></sequenceFlow>
    
    <!-- 用户任务 - 审批 -->
    <userTask id="approval" name="审批" flowable:assignee="${userId}" 
              flowable:formFieldValidation="true">
      <documentation>审批</documentation>
    </userTask>
    <sequenceFlow id="sid-58446799-2B9E-4455-AFF6-98B0D45B3565" 
                  sourceRef="apply" targetRef="approval"></sequenceFlow>

    <!-- 排他网关 -->
    <exclusiveGateway id="decision"/>
    <sequenceFlow sourceRef="decision" targetRef="externalSystemCallServer">
        <!--顺序流条件:以表达式(expression)的形式定义了条件(condition) -->
        <conditionExpression xsi:type="tFormalExpression">
            <!--条件表达式:是${approved == true}的简写-->
            <![CDATA[
              ${approved}
            ]]>
        </conditionExpression>
    </sequenceFlow>
    <sequenceFlow sourceRef="decision" targetRef="sendRejectionMailServer">
        <conditionExpression xsi:type="tFormalExpression">
            <![CDATA[
              ${!approved}
            ]]>
        </conditionExpression>
    </sequenceFlow>
    <sequenceFlow sourceRef="approval" targetRef="decision"/>
    
    <!-- 服务任务,自动活动,可以进行业务操作 -->
    <serviceTask id="externalSystemCallServer" name="批准请假,录入业务系统" 
                 flowable:expression="${callExternalSystemDelegateListener}"/>
    <sequenceFlow sourceRef="externalSystemCallServer" targetRef="endEvent"/>

    <!-- 邮件服务,自动活动,发送邮件 -->
    <serviceTask id="sendRejectionMailServer" flowable:type="mail" name="发送驳回邮件"
                 flowable:delegateExpression="${sendMessageExecutionCallListener}">
        <extensionElements>
            <flowable:field name="from" expression="${mailObject.sender}"/>
            <flowable:field name="to" expression="${mailObject.recipient}"/>
            <flowable:field name="subject" expression="${mailObject.title}"/>
            <flowable:field name="html" expression="${mailObject.message}"/>
        </extensionElements>
    </serviceTask>
    <sequenceFlow sourceRef="sendRejectionMailServer" targetRef="endEvent"/>
    
    <!-- 结束事件 -->
    <endEvent id="endEvent" name="结束">
      <documentation>结束</documentation>
    </endEvent>
    <sequenceFlow id="sid-88BD681B-C588-4CE4-AA4A-C07A5E4BDC8D" sourceRef="approval" targetRef="end"></sequenceFlow>
  </process>
</definitions>

七、Flowable整体架构及常用内容

Flowable整体是通过processEngine来操作。

7.1 各个服务类的方法及其作用

7.1.1 RepositoryService

  • 提供对流程定义和部署存储库的访问的服务

  • 用于流程定义、流程部署、流程文件查询,验证BpmnModel、流程转BpmnModel,BpmnModel转流程文件(.bpm)等

// 根据流程引擎上执行流程定义的规则验证给定流程定义(验证bmpnModel)
List<ValidationError> validateProcess(BpmnModel bpmnModel);

/**
 * 加载流程/部署流程
 * @param sourceName - bpmn20.xml文件,如 holiday-request.bpmn20.xml
 */
repositoryService.createDeployment().addClasspathResource(String sourceName).deploy();

// 查询流程定义 - 仅查询具有给定名字的流程定义
ProcessDefinitionQuery createProcessDefinitionQuery().processDefinitionName(String processDefinitionName).singleResult();
// 查询流程定义 - 仅查询具有给定部署ID的部署中的流程定义
ProcessDefinitionQuery createProcessDefinitionQuery().deploymentId(String deploymentId).singleResult();

/** 
 * 通过bpmnmodel定义流程并部署流程
 *     resourceName bpmn20.xml文件名
 *     bpmnModel 实例
 * 源码实现方法:
 *     BpmnModel bpmnModel = createOneTaskTestProcess(); // 这里用代码的方式创建了一个model,设置了标签属性及其值,和bpmn20.xml类似
 *     Deployment deployment = repositoryService.createDeployment().addBpmnModel("oneTasktest.bpmn20.xml", bpmnModel).deploy();
 */ 
Deployment createDeployment().addBpmnModel(String resourceName, BpmnModel bpmnModel).deploy();

/**
 * 返回与具有提供的流程定义 id 的流程定义对应的BpmnModel
 */
BpmnModel getBpmnModel(String processDefinitionId);

/** 
 * 通过字节流访问部署资源。
 * 通过deploymentId获取流程定义生成的bpmn文件或者其他资源文件
 * @param deploymentId - 部署的 ID,不能为空。
 * @param resourceName – 资源的名称,不能为空。
 * @throw FlowableObjectNotFoundException – 当给定部署中不存在资源或给定部署 ID 不存在部署时
 */
InputStream getResourceAsStream(String deploymentId, String resourceName);

/**
 * 删除给定的部署。
 * @param deploymentId - 部署的 ID,不能为空。
 * @throw RuntimeException – 如果仍有运行时或历史流程实例或作业
 */
void deleteDeployment(String deploymentId);

7.1.2 RuntimeService

  • 流程实例管理

  • 管理流程实例的启动、推进、删除,获取流程实例当前状态等。

/**
 * 启动流程:
 * 在具有给定 id 的流程定义的确切指定版本中启动一个新流程实例。可以提供业务键来将流程实例与具有明确业务含义的特定标识符相关联。例如,在订单流程中,业务键可以是订单 ID。
 * @param processDefinitionId – 流程定义的 id,不能为空。
 * @param businessKey – 标识流程实例的密钥,可用于稍后通过查询 API 检索流程实例。
 * @throw FlowableObjectNotFoundException – 当没有使用给定键部署流程定义时
 */
ProcessInstance startProcessInstanceById(String processDefinitionId, String businessKey);

7.1.3 HistoryService:流程历史查询

  • 历史是捕获流程执行过程中发生的事件并永久保存的组件,历史数据在流程实例完成后也将保留在数据库中
  • 一共有6个历史实体:
    • HistoricProcessInstance - 包含有关当前和过去的流程实例的信息
    • HistoricVariableInstance - 包含过程变量或任务变量的最新值。
    • HistoricActivityInstance - 包含有关活动(进程中的节点)的单个执行的信息。
    • HistoricTaskInstance - 包含有关当前和过去(已完成和已删除)任务实例的信息。
    • HistoricIdentityLink - 包含关于任务和流程实例上当前和过去身份链接的信息。
    • HistoricDetail - 包含与历史流程实例,活动实例或任务实例相关的各种信息
7.1.3.1 HistoricProcessInstanceQuery createHistoricProcessInstanceQuery();
// 获取流程定义ID是'XXX'、已经结束、花费时间最长(持续时间最长)的10个
historyService.createHistoricProcessInstanceQuery()
  .finished()
  .processDefinitionId("XXX")
  .orderByProcessInstanceDuration().desc()
  .listPage(0, 10);
7.1.3.2 HistoricVariableInstanceQuery createHistoricVariableInstanceQuery();
// 在ID为'xxx'、已经结束的流程实例中查询所有HistoricVariableInstances, 并按变量名排序
historyService.createHistoricVariableInstanceQuery()
  .processInstanceId("XXX")
  .orderByVariableName.desc()
  .list();
7.1.3.3 HistoricActivityInstanceQuery createHistoricActivityInstanceQuery();
// 获取流程定义ID是'processInstance.getId()'、已经结束、时间倒序的活动
historyService.createHistoricActivityInstanceQuery() .activityType("serviceTask")
    .processDefinitionId("XXX")
    .finished()
    .orderByHistoricActivityInstanceEndTime().desc()
    .listPage(0, 1);
7.1.3.4 HistoricDetailQuery createHistoricDetailQuery();
// 获取所有id为123的流程实例中产量的可变更新信息。这个查询只会返回 HistoricVariableUpdates
//  注意一些变量名可能包含多个 HistoricVariableUpdate 实体, 每次流程运行时会更新变量
historyService.createHistoricDetailQuery()
  .variableUpdates()
  .processInstanceId("123")
  .orderByVariableName().asc()
  .list();

// 获取所有流程实例ID为123的流程中,提交任务或者启动流程时的表单属性.这个查询只会返回 HistoricFormProperties
historyService.createHistoricDetailQuery()
  .formProperties()
  .processInstanceId("123")
  .orderByVariableName().asc()
  .list();

// 获取所有在执行ID为123的任务时的变量更新。 返回全部在任务中设置的变量(任务局部变量) HistoricVariableUpdates
historyService.createHistoricDetailQuery()
  .variableUpdates()
  .taskId("123")
  .orderByVariableName().asc()
  .list();

任务局部变量可以用 TaskService 设置或者使用 DelegateTask, 在TaskListener里设置

taskService.setVariableLocal("123", "myVariable", "Variable value");
// 或者:
public void notify(DelegateTask delegateTask) {
  delegateTask.setVariableLocal("myVariable", "Variable value");
}
7.1.3.5 HistoricTaskInstanceQuery createHistoricTaskInstanceQuery();
// 获取所有任务中10个花费时间最长(持续时间最长)并已经结束的 HistoricTaskInstances
historyService.createHistoricTaskInstanceQuery()
  .finished()
  .orderByHistoricTaskInstanceDuration().desc()
  .listPage(0, 10);

// 获取删除原因包含"无效",最后分配给用户"kermit"的 HistoricTaskInstances
historyService.createHistoricTaskInstanceQuery()
  .finished()
  .taskDeleteReasonLike("%invalid%")
  .taskAssignee("kermit")
  .listPage(0, 10);
7.1.3.6 流程历史配置
  1. 代码方式配置
ProcessEngine processEngine = ProcessEngineConfiguration
  .createProcessEngineConfigurationFromResourceDefault()
  .setHistory(HistoryLevel.AUDIT.getKey())
  .buildProcessEngine();
  1. flowable.cfg.xml中或spring-context配置
<bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
  <property name="history" value="audit" />
  ...
</bean>
  1. 历史级别参数

    • none: 忽略所有历史存档。这是流程执行时性能最好的状态,但没有任何历史信息可用。
    • activity: 保存所有流程实例信息和活动实例信息。 在流程实例结束时, 最后一个流程实例中的最新的变量值将赋值给历史变量。 不会保存过程中的详细信息。
    • audit: 这个是默认值. 它保存所有流程实例信息, 活动信息, 保证所有的变量和提交的表单属性保持同步 这样所有用户交互信息都是可追溯的,可以用来审计。
    • full: 这个是最高级别的历史信息存档,同样也是最慢的。 这个级别存储发生在审核以及所有其它细节的信息, 主要是更新流程变量。

7.1.4 TaskService:任务服务

  • 用于指定节点任务的主要Service。比如ReceiveTask, UserTask, MailTask,HttpTask等等。不同的Task具备了不同的处理方式。
// 通过TaskService查询manager组的任务
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
/**
 * 获取所有变量并在任务范围内搜索,如果可用,还包括执行范围。
 * 如果您有很多变量并且只需要一些变量,请考虑使用getVariables(String, Collection)以获得更好的性能
 */
Map<String, Object> getVariables(String taskId);
/**
 * 当任务成功执行时调用,所需的任务参数由最终用户给出。
 * @param taskId – 要完成的任务的 id,不能为空。
 * @param variables 变量——任务参数。可以为 null 或为空。
 * @throw FlowableObjectNotFoundException – 当不存在具有给定 ID 的任务时。
 */
 void complete(String taskId, Map<String, Object> variables);

7.2 任务监听 - TaskListener - 针对用户任务

BaseTaskListerner 定义了任务监听器的类型,ALL类型不代表事件

7.2.1 任务监听器触发类型

  • create:在任务创建且所有任务属性设置完成之后才触发。
  • assignment:在任务被分配给某班人之后触发,它是在create事件触发前被触发。
  • complete:在配置了监听器的任务完成时触发,也就是说运行期任务删除之前触发。
  • delete:任务删除前触发

7.2.2 如何使用

案例:bpmn20.xml文件

<!--用户任务:需要人工来进行操作-->
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers">
  <!--
        任务监听器
        params:
            event:事件,可选值create、assignment、complete、delete
            class:类的位置(非必选),也可以交给spring管理,参数:delegateExpression="${listenerName}"
    -->
  <extensionElements>
    <!--<flowable:taskListener event="create" delegateExpression="${taskListener}"/>-->
    <flowable:taskListener event="create" class="listener.MyTaskListener"/>
  </extensionElements>
</userTask>

三种方式:

7.2.2.1 class路径加入到xml文件
<flowable:taskListener event="create" class="listener.MyTaskListener"/>

监听器

public class MyTaskListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        String processInstanceId = delegateTask.getProcessInstanceId();
        System.out.println("监听的用户任务ID是:" + processInstanceId);
    }
}
7.2.2.2 expression表达式

指定事件发生时将被执行的表达式,不能和class属性一起使用。

  1. 可以将DelegateTask对象和事件名称(使用task.eventName)作为参数传递给被调用对象
<flowable:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
  1. 直接使用监听器的方法
<flowable:taskListener event="create" expression="${testTaskListener.notify(task)}"/>
<!-- 或者 -->
<flowable:taskListener event="create" expression="${testTaskListener}"/>
  1. 以上两种方式都需要在流程初始化的时候将对象加入到流程的初始Map集合,并且该监听器需要实现序列化。
// 1.监听器实现序列化
@Component(value = "testTaskListener")
public class TestTaskListener implements TaskListener, Serializable {}

// 2. 启动流程
@Autowired
private TestTaskListener testTaskListener;

Map<String, Object> variables = new HashMap<>(3);
// variables.put("testTaskListener", new TestTaskListener()); // 这里必须加入对象,不然会报错
// 交给spring管理,可以写成如下
variables.put("testTaskListener", testTaskListener);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holiday", variables);
7.2.2.3 delegateExpression参数

允许指定一个解析为实现该TaskListener接口的对象的表达式,类似于服务任务。

<flowable:taskListener event="create" delegateExpression="${testTaskListener}"/>
7.2.2.4 脚本任务监听器

允许为任务侦听器事件执行一段脚本逻辑

<flowable:taskListener event="complete" class="org.flowable.engine.impl.bpmn.listener.ScriptTaskListener" >
    <flowable:field name="script">
        <flowable:string>
            def bar = "BAR"; // local variable
            foo = "FOO"; // pushes variable to execution context
            task.setOwner("kermit"); // test access to task instance
            bar // implicit return value
        </flowable:string>
    </flowable:field>
    <flowable:field name="language" stringValue="groovy" />
    <flowable:field name="resultVariable" stringValue="myVar" />
</flowable:taskListener>

7.3 服务任务

目前可使用方式:序列化+方法调用

7.3.1 XML调用

7.3.1.1 class方式
<serviceTask id="externalSystemCallServer" name="Enter holidays in external system"
             flowable:class="${demo.delegate.callExternalSystemDelegateListener}"/>
7.3.1.2 flowable:expression
  • 注意:serviceTask中不能使用expression,必须加上flowable标签
<serviceTask id="externalSystemCallServer" name="Enter holidays in external system"
             flowable:expression="${callExternalSystemDelegateListener.execute(execution)}"/>
7.3.1.3 flowable:delegateExpression
  • 注意:serviceTask中不能使用delegateExpression,必须加上flowable标签
<serviceTask id="externalSystemCallServer" name="Enter holidays in external system"
                     flowable:delegateExpression="${callExternalSystemDelegateListener}"/>

7.3.2 Java处理:序列化+传参

// 1.监听器实现序列化
public class CallExternalSystemDelegateListener implements JavaDelegate, Serializable {}

// 2.服务类实现
boolean approved = "y".equals(mailObject.getApproved().toLowerCase());
variables = new HashMap<String, Object>();
variables.put("approved", approved);
variables.put("mailObject", mailObject);
variables.put("callExternalSystemDelegateListener", new CallExternalSystemDelegateListener()); // 放入排他网关 -- 局部变量
// 任务完成,并会在离开排他网关的两条路径中,基于'approved'流程变量选择一条
taskService.complete(task.getId(), variables);

7.4 通知处理-邮件

Flowable邮件任务被实现为一个服务任务(可自动执行)

7.4.1 邮件服务器配置

属性需要描述
mailServerHost没有邮件服务器的主机名(如mail.mycorp.com)。默认为localhost
mailServerPort是的,不在默认端口上邮件服务器的SMTP端口。默认值为25
mailServerDefaultFrom没有当用户没有提供电子邮件发件人的默认电子邮件地址。默认情况下,这是flowable@flowable.org
mailServerUsername如果适用于您的服务器某些邮件服务器需要凭证才能发送电子邮件。默认情况下不设置。
mailServerPassword如果适用于您的服务器某些邮件服务器需要凭证才能发送电子邮件。默认情况下不设置。
mailServerUseSSL如果适用于您的服务器一些邮件服务器需要SSL通信。默认情况下设置为false。
mailServerUseTLS如果适用于您的服务器某些邮件服务器(例如gmail)需要TLS通信。默认情况下设置为false。

7.4.2 定义邮件服务

<serviceTask id="sendMail" flowable:type="mail">

电子邮件任务由字段注入配置。这些属性的所有值都可以包含EL表达式,这些表达式在流程执行期间在运行时被解析。以下属性可以设置:

属性必填描述
to电子邮件的收件人。多个收件人在逗号分隔列表中定义
from发件人的电子邮件地址。如果未提供,则使用从地址配置的默认值。
subject电子邮件的主题。
cc电子邮件的cc。多个收件人在逗号分隔列表中定义
bcc电子邮件的密件抄送。多个收件人在逗号分隔列表中定义
charset允许指定电子邮件的字符集,这是许多非英语语言所必需的。
html一段HTML是电子邮件的内容。
text电子邮件的内容,以防需要发送简单的,非丰富的电子邮件。可以与html结合使用,适用于不支持丰富内容的电子邮件客户端。电子邮件客户端然后可以回到这个纯文本的选择。
htmlVar包含作为电子邮件内容的HTML的流程变量的名称。这和html的主要区别在于,这个内容在被邮件任务发送之前会被替换掉。
textVar包含电子邮件的纯文本内容的过程变量的名称。这和文本之间的主要区别在于,这个内容在被邮件任务发送之前会被替换表达式。
ignoreException处理电子邮件时是否失败,而不是抛出FlowableException。默认情况下,它被设置为false。
exceptionVariableName当电子邮件处理由于ignoreException = true而不会引发异常时,将使用具有给定名称的变量来保存失败消息

7.4.3 邮件示例

<serviceTask id="sendMail" flowable:type="mail">
  <extensionElements>
    <flowable:field name="from" stringValue="order-shipping@thecompany.com" />
    <flowable:field name="to" expression="${recipient}" />
    <flowable:field name="subject" expression="Your order ${orderId} has been shipped" />
    <flowable:field name="html">
      <flowable:expression>
        <![CDATA[
          <html>
            <body>
              Hello ${male ? 'Mr.' : 'Mrs.' } ${recipientName},<br/><br/>

              As of ${now}, your order has been <b>processed and shipped</b>.<br/><br/>

              Kind regards,<br/>

              TheCompany.
            </body>
          </html>
        ]]>
      </flowable:expression>
    </flowable:field>
  </extensionElements>
</serviceTask>

7.4.4 邮件配置

# v6.x
flowable.mail.server.host=SMTP服务域v6.x名
flowable.mail.server.port=相应SMTP服务对应开放端口
flowable.mail.server.username=SMTP服务邮箱账号
flowable.mail.server.password=SMTP服务授权码
flowable.mail.server.defaultFrom=默认发送人邮箱

例如:使用QQ邮箱的SMTP服务

flowable:
  mail:
    server:
      host: smtp.qq.com
      port: 587
      default-from: test@qq.com
      username: test@qq.com
      password: 邮箱STMP服务授权码
      use-stl: true

如何开启QQ邮箱SMTP服务?

7.4.5 Flowable集成钉钉具体案例

blog.csdn.net/liuwenjun05…

7.4.6 SpringBoot集成Flowable邮箱操作Demo实现

7.5 业务规则任务

  • 可同步地执行一条或者多条规则
  • Flowable使用Drools Expert的Drools规则引擎执行业务规则。
  1. XML文件配置
<process id="simpleBusinessRuleProcess">
  <startEvent id="theStart" />
  <sequenceFlow sourceRef="theStart" targetRef="businessRuleTask" />
  
  <businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
                    flowable:resultVariable="rulesOutput" />
  
  <sequenceFlow sourceRef="businessRuleTask" targetRef="theEnd" />
  <endEvent id="theEnd" />
</process>
  1. 也可以将业务规则任务配置为只执行部署的.drl文件中的一组规则。要做到这一点,需要指定规则名字的列表,用逗号分隔。

    1. 只执行rule1和rule2
    <businessRuleTask id="businessRuleTask" 
                      flowable:ruleVariablesInput="${order}" 
                      flowable:rules="rule1, rule2" />
    
    1. 不执行rule1和rule2
    <businessRuleTask id="businessRuleTask" 
                      flowable:ruleVariablesInput="${order}" 
                      flowable:rules="rule1, rule2" 
                      exclude="true" />
    
    1. 自定义规则 - 配置后与服务任务完全一样 - 任务执行器
    <businessRuleTask id="businessRuleTask" flowable:class="${MyRuleServiceDelegate}" />
    

7.6 服务EL表达式与Spring容器管理

7.6.1 步骤说明

  1. 使用EL表达式,同时需要交给Spring管理的,需要使用

    • flowable:delegateExpression="${bean名}"或者flowable:expression="${bean名}"(对应任务监听器)
    • delegateExpression="${bean名}"或者expression="${bean名}"(对应服务任务)
  2. 同时在Java类中该模块需要传入局部变量,可使用工具类获取Bean或者直接用@Autowired TestTaskListener testTaskListener

    • variables.put("testTaskListener", SpringContextUtil.getBean("testTaskListener"));
    • variables.put("testTaskListener", testTaskListener);
  3. 如果是服务任务且实现了JavaDelegate接口,则需要再实现Serializable接口;

  4. 交给Spring管理可以直接在该类上方加上注解@Component,或者在SpringConfig中统一构建Bean并初始化实例。

7.6.2 完整小案例

  1. bpmn20.xml文件配置
<serviceTask id="externalSystemCallServer" name="Enter holidays in external system" 
             flowable:delegateExpression="${callExternalSystemDelegateListener}"/>
  1. 监听器实现
@Component(value = "callExternalSystemDelegateListener")
public class CallExternalSystemDelegateListener implements JavaDelegate, Serializable{
    
    private static final long serialVersionUID = -1L;
    
    @Override
    public void execute(DelegateExecution execution) {
        System.out.println("Calling the external system for employee.");
    }
}
  1. 流程中参数传入
@Autowired
private CallExternalSystemDelegateListener callExternalSystemDelegateListener;

public void test(MailObject mailObject) {
    // 经理完成任务
    boolean approved = "y".equals(mailObject.getApproved().toLowerCase());
    Map<String, Object> variables = new HashMap<String, Object>();
    variables.put("approved", approved);
    variables.put("mailObject", mailObject);
    variables.put("callExternalSystemDelegateListener", callExternalSystemDelegateListener);
    // 任务完成,并会在离开排他网关的两条路径中,基于’approved ’流程变量选择一条
    taskService.complete(task.getId(), variables);
}

八、常见流程功能设计及实现

8.1 流程部署/定义

8.1.1 查询已部署流程

/**
 * 查询已部署流程
 *
 * @param processKey 流程定义ID
 */
@Transactional(rollbackFor = Exception.class)
public void deployProcessDefinition(String processKey) {
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey(processKey).latestVersion().singleResult();
    if (processDefinition == null) {
        throw new Exception("流程部署失败,key=[" + processKey + "]不存在!");
    }
    log.info("Process deploy success! definitionKey: {}, definitionId: {}, definitionName: {} ", processDefinition.getKey(), processDefinition.getId(), processDefinition.getName());
}

8.1.2 挂起流程定义

/**
 * 挂起流程定义
 * 
 * @param state    流程部署状态 1-激活 0-挂起
 * @param deployId 流程部署ID act_ge_bytearray 表中 deployment_id值
 */
public void updateState(Integer state, String deployId) {
    ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery().deploymentId(deployId).singleResult();
    // 激活
    if (state == 1) {
        repositoryService.activateProcessDefinitionById(procDef.getId(), true, null);
    }
    // 挂起
    if (state == 0) {
        repositoryService.suspendProcessDefinitionById(procDef.getId(), true, null);
    }
}

8.1.3 删除流程定义

/**
 * 删除流程定义
 *
 * @param deployId 流程部署ID act_ge_bytearray 表中 deployment_id值
 */
public void delete(String deployId) {
    // true 允许级联删除 ,不设置会导致数据库外键关联异常
    repositoryService.deleteDeployment(deployId, true);
}

8.2 流程实例管理

8.2.1 启动流程

/**
 * 启动流程
 *
 * @param initiator  流程创建人
 * @param processKey 流程定义ID
 * @param variables  流程启动参数
 */
@Transactional(rollbackFor = Exception.class)
public void startProcessInstance(String initiator, String processKey, Map<String, Object> variables) {
    // 设置流程创建人
    Authentication.setAuthenticatedUserId(initiator);
    // 启用自动跳过,可用于跳过某个流程节点
    variables.put("skip", true);
    variables.put("_FLOWABLE_SKIP_EXPRESSION_ENABLED", true);
    // 启动流程
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processKey, variables);
    // 流程实例ID
    String processInstanceId = processInstance.getProcessInstanceId();
    // 日志输出
    log.info("Processes start success! ProcessInstanceId={}", processInstanceId);
}

8.2.2 终止流程

/**
 * 终止流程
 *
 * @param processInstanceId 流程实例ID
 */
@Transactional(rollbackFor = Exception.class)
public void stopProcessInstanceByProcessInstanceId(String processInstanceId) {
    ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
    if (processInstance == null) {
        throw new Exception("流程实例不存在,流程实例ID=[" + processInstanceId + "]");
    }
    // 获取终止节点
    Process mainProcess = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId()).getMainProcess();
    Collection<FlowElement> flowElementList = mainProcess.getFlowElements();
    List<FlowElement> endNodes = flowElementList.stream().filter(element -> element instanceof EndEvent).collect(Collectors.toList());
    String endId = endNodes.get(0).getId();
    // 执行终止
    List<Execution> executions = runtimeService.createExecutionQuery().parentId(processInstanceId).list();
    List<String> executionIds = new ArrayList<>();
    executions.forEach(execution -> executionIds.add(execution.getId()));
    runtimeService.createChangeActivityStateBuilder().moveExecutionsToSingleActivityId(executionIds, endId).changeState();
}

8.2.3 查询我发起的流程

/**
 * 查询我发起的流程
 *
 * @param userId   用户ID		
 * @param pageNum  页码
 * @param pageSize 页大小
 */
public void myProcess(String userId, Integer pageNum, Integer pageSize) {
    HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery()
        .startedBy(userId)
        .orderByProcessInstanceStartTime()
        .desc();
    List<HistoricProcessInstance> historicProcessInstances = historicProcessInstanceQuery.listPage(pageSize * (pageNum - 1), pageSize);
    if (CollectionUtils.isEmpty(historicProcessInstances)) {
        log.info("用户[{}]未发起流程", userId);
        return;
    }
    historicProcessInstances.forEach(historicProcessInstance -> {
        // 流程实例ID
        System.out.println(historicProcessInstance.getId());
    });
}

8.2.4 激活/挂起流程

/**
 * 激活/挂起流程
 *
 * @param state				流程状态 1-激活 0-挂起
 * @param processInstanceId 流程实例ID
 */
public void updateState(Integer state, String processInstanceId) {
    // 激活
    if (state == 1) {
        runtimeService.activateProcessInstanceById(processInstanceId);
    }
    // 挂起
    if (state == 0) {
        runtimeService.suspendProcessInstanceById(processInstanceId);
    }
}

8.2.5 删除流程实例

/**
 * 删除流程实例
 *
 * @param processInstanceId   流程实例ID
 * @param deleteReason 删除原因
 */
@Transactional(rollbackFor = Exception.class)
public void delete(String processInstanceId, String deleteReason) {
    // 查询历史数据
    HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
    if (Objects.isNull(historicProcessInstance)) {
        throw new FlowableObjectNotFoundException("流程实例不存在: " + processInstanceId);
    }
    // 1、流程已经结束
    if (historicProcessInstance.getEndTime() != null) {
        historyService.deleteHistoricProcessInstance(historicProcessInstance.getId());
        return;
    }
    // 2、流程未结束
    // 删除流程实例
    runtimeService.deleteProcessInstance(processInstanceId, deleteReason);
    // 删除历史流程实例
    historyService.deleteHistoricProcessInstance(processInstanceId);
}

8.3 流程任务管理

8.3.1 审批任务

/**
 * 审批任务
 *
 * @param taskId 		 任务ID
 * @param userId 		 审批人ID
 * @param commentContent 审批意见
 */
@Transactional(rollbackFor = Exception.class)
public void complete(String taskId, String userId, String commentContent) {
    // 当前任务
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
    if (task == null) {
        throw new Exception("不存在任务Id为: " + taskId + "的任务");
    }
    if (task.isSuspended()) {
        throw new Exception("任务处于挂起状态");
    }
//        if (StringUtil.isEmpty(task.getAssignee())) {
//            throw new Exception("任务未指定审批人!");
//        }
//		  if (!(task.getAssignee().equals(userId))) {
//         	  throw new Exception("任务审批人与当前用户不符,不能进行审批操作!");
//        }
    // 添加审批意见
    taskService.addComment(taskId, task.getProcessInstanceId(), commentContent);
    // 完成审批
    taskService.complete(taskId);
}

8.3.2 退回任务

/**
 * 退回任务
 *
 * @param taskId 		 任务ID
 * @param commentContent 退回意见
 */
@Transactional(rollbackFor = Exception.class)
public void returnTask(String taskId, String commentContent) {
    // 当前任务
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
    // 任务校验
    if (task == null) {
        throw new Exception("不存在任务Id为: " + taskId + "的任务");
    }
    if (task.isSuspended()) {
        throw new Exception("任务处于挂起状态");
    }
    // 流程定义信息
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId()).singleResult();
    // 流程模型
    BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
    // 当前节点组件
    FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
    // 获取当前活动节点Id
    String activityId = flowElement.getId();
    // 获取流程起点活动Id
    String startEventId = bpmnModel.getMainProcess().getInitialFlowElement().getId();
    // 添加退回意见
    taskService.addComment(taskId, task.getProcessInstanceId(), commentContent);
    // 直接退回第一步
    runtimeService.createChangeActivityStateBuilder().processInstanceId(task.getProcessInstanceId())
                .moveActivityIdTo(activityId, startEventId).changeState();
}

8.3.3 撤回任务

8.3.4 驳回任务

8.3.4 转办任务

/**
 * 转办任务
 *
 * @param taskId  		 任务ID
 * @param commentContent 转办意见
 */
@Transactional(rollbackFor = Exception.class)
public void assign(String taskId, String commentContent) {
    taskService.setAssignee(taskId, commentContent);
}

8.3.5 认领/签收任务

/**
 * 认领/签收任务
 *
 * @param taskId 任务ID
 * @param userId 认领/签收任务用户ID
 */
@Transactional(rollbackFor = Exception.class)
public void claim(String taskId, String userId) {
    taskService.claim(taskId, userId);
}

8.3.6 取消认领任务

/**
 * 取消认领任务
 *
 * @param taskId 任务ID
 */
@Transactional(rollbackFor = Exception.class)
public void unClaim(String taskId) {
    taskService.unclaim(taskId);
}

8.3.7 委派任务

/**
 * 委派任务
 *
 * @param taskId 任务ID
 * @param userId 被委派用户ID
 */
@Transactional(rollbackFor = Exception.class)
public void delegate(String taskId, String userId) {
    taskService.delegateTask(taskId, userId);
}

8.3.8 查询我的待办任务

/**
 * 查询我的待办任务
 *
 * @param userId   用户ID
 * @param pageNum  页码
 * @param pageSize 页大小
 */
public void getPendingList(String userId, Integer pageNum, Integer pageSize) {
    TaskQuery taskQuery = taskService.createTaskQuery();
    List<Task> list = taskQuery
            .taskAssignee(userId)
            .includeProcessVariables()
            .active()
            .orderByTaskCreateTime()
            .desc()
            .listPage(pageSize * (pageNum - 1), pageSize, pageSize);
    if (!CollectionUtils.isEmpty(list)) {
        list.forEach(System.out::println);
    }
}

8.3.9 查询我的已办任务

/**
 * 查询我的已办任务
 * 
 * @param userId   用户ID
 * @param pageNum  页码
 * @param pageSize 页大小
 */
public void myHandledList(String userId,  Integer pageNum, Integer pageSize) {
     HistoricTaskInstanceQuery taskInstanceQuery = historyService.createHistoricTaskInstanceQuery()
         .includeProcessVariables()
         .finished()
         .taskAssignee(userId)
         .orderByHistoricTaskInstanceEndTime()
         .desc();
    List<HistoricTaskInstance> historicTaskInstanceList = taskInstanceQuery.listPage(pageSize * (pageNum - 1), pageSize);
    if (!CollectionUtils.isEmpty(historicTaskInstanceList)) {
        historicTaskInstanceList.forEach(System.out::println);
    }
}

8.3.10 查询流程任务列表

/**
 * 查询流程任务列表
 *
 * @param pageNum  页码
 * @param pageSize 页大小
 */
public void list(Integer pageNum, Integer pageSize) {
    HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
    List<HistoricTaskInstance> list = historicTaskInstanceQuery
        .includeProcessVariables()
        .orderByTaskId()
        .desc()
        .listPage(pageSize * (pageNum - 1), pageSize);
    if (!CollectionUtils.isEmpty(list)) {
        list.forEach(System.out::println);
    }
}

8.4 流程图管理

8.4.1 生成流程图

  1. Controller层
@Slf4j
@RestController
public class FlowTaskController {
    
    @Resource
    private FlowTaskService flowTaskService;
    
    /**
     * 生成流程图
     *
     * @param processInstanceId 流程实例id
     */
    @GetMapping('/flowable/diagram/gen')
    public void genProcessDiagram(String processInstanceId) {
        InputStream inputStream = flowTaskService.diagram(processInstanceId);
        OutputStream os = null;
        BufferedImage image;
        try {
            image = ImageIO.read(inputStream);
            response.setContentType("image/png");
            os = response.getOutputStream();
            if (image != null) {
                ImageIO.write(image, "png", os);
            }
        } catch (Exception e) {
            log.error("create flow chart failed:", e);
        } finally {
            try {
                if (os != null) {
                    os.flush();
                    os.close();
                }
            } catch (IOException e) {
                log.error("close io failed:", e);
            }
        }
}
  1. Service层
public interface FlowTaskService {
   /**
     * 获取流程过程图
     *
     * @param processInstanceId 流程实例Id
     * @return 输入流
     */
    InputStream diagram(String processInstanceId);
}
  1. ServiceImpl服务实现类
@Service
@Slf4j
public class FlowTaskServiceImpl extends FlowServiceFactory implements FlowTaskService {
    /**
     * 获取流程过程图
     *
     * @param processInstanceId 流程实例Id
     * @return 输入流
     */
    @Override
    public InputStream diagram(String processInstanceId) {
        String processDefinitionId;
        // 获取当前的流程实例
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
        // 如果流程已经结束,则得到结束节点
        if (Objects.isNull(processInstance)) {
            HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
            processDefinitionId = pi.getProcessDefinitionId();
        } else {
            // 如果流程没有结束,则取当前活动节点
            // 根据流程实例ID获得当前处于活动状态的ActivityId合集
            ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
            processDefinitionId = pi.getProcessDefinitionId();
        }

        // 获得活动的节点
        List<HistoricActivityInstance> highLightedFlowList = historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();

        List<String> highLightedFlows = new ArrayList<>();
        List<String> highLightedNodes = new ArrayList<>();
        //高亮线
        for (HistoricActivityInstance tempActivity : highLightedFlowList) {
            if ("sequenceFlow".equals(tempActivity.getActivityType())) {
                //高亮线
                highLightedFlows.add(tempActivity.getActivityId());
            } else {
                //高亮节点
                highLightedNodes.add(tempActivity.getActivityId());
            }
        }

        //获取流程图
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
        ProcessEngineConfiguration configuration = processEngine.getProcessEngineConfiguration();

        //获取自定义图片生成器
        ProcessDiagramGenerator diagramGenerator = new CustomProcessDiagramGenerator();
        return diagramGenerator.generateDiagram(bpmnModel, "png", highLightedNodes, highLightedFlows, configuration.getActivityFontName(),
                configuration.getLabelFontName(), configuration.getAnnotationFontName(), configuration.getClassLoader(), 1.0, true);
    }
}
  1. 流程图生成器重写
public class CustomProcessDiagramGenerator extends DefaultProcessDiagramGenerator {
    @SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
    @Override
    protected DefaultProcessDiagramCanvas generateProcessDiagram(BpmnModel bpmnModel, String imageType, List<String> highLightedActivities, List<String> highLightedFlows, String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
        this.prepareBpmnModel(bpmnModel);
        DefaultProcessDiagramCanvas processDiagramCanvas = initProcessDiagramCanvas(bpmnModel, imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);
        Iterator var13 = bpmnModel.getPools().iterator();
        while (var13.hasNext()) {
            Pool process = (Pool) var13.next();
            GraphicInfo subProcesses = bpmnModel.getGraphicInfo(process.getId());
            processDiagramCanvas.drawPoolOrLane(process.getName(), subProcesses, scaleFactor);
        }

        var13 = bpmnModel.getProcesses().iterator();

        Process process1;
        Iterator subProcesses1;
        while (var13.hasNext()) {
            process1 = (Process) var13.next();
            subProcesses1 = process1.getLanes().iterator();

            while (subProcesses1.hasNext()) {
                Lane artifact = (Lane) subProcesses1.next();
                GraphicInfo subProcess = bpmnModel.getGraphicInfo(artifact.getId());
                processDiagramCanvas.drawPoolOrLane(artifact.getName(), subProcess, scaleFactor);
            }
        }

        var13 = bpmnModel.getProcesses().iterator();

        while (var13.hasNext()) {
            process1 = (Process) var13.next();
            subProcesses1 = process1.findFlowElementsOfType(FlowNode.class).iterator();

            while (subProcesses1.hasNext()) {
                FlowNode artifact1 = (FlowNode) subProcesses1.next();
                if (!this.isPartOfCollapsedSubProcess(artifact1, bpmnModel)) {
                    this.drawActivity(processDiagramCanvas, bpmnModel, artifact1, highLightedActivities, highLightedFlows, scaleFactor, Boolean.valueOf(drawSequenceFlowNameWithNoLabelDI));
                }
            }
        }

        var13 = bpmnModel.getProcesses().iterator();

        label75:
        while (true) {
            List subProcesses2;
            do {
                if (!var13.hasNext()) {
                    return processDiagramCanvas;
                }

                process1 = (Process) var13.next();
                subProcesses1 = process1.getArtifacts().iterator();

                while (subProcesses1.hasNext()) {
                    Artifact artifact2 = (Artifact) subProcesses1.next();
                    this.drawArtifact(processDiagramCanvas, bpmnModel, artifact2);
                }

                subProcesses2 = process1.findFlowElementsOfType(SubProcess.class, true);
            } while (subProcesses2 == null);

            Iterator artifact3 = subProcesses2.iterator();

            while (true) {
                GraphicInfo graphicInfo;
                SubProcess subProcess1;
                do {
                    do {
                        if (!artifact3.hasNext()) {
                            continue label75;
                        }

                        subProcess1 = (SubProcess) artifact3.next();
                        graphicInfo = bpmnModel.getGraphicInfo(subProcess1.getId());
                    } while (graphicInfo != null && graphicInfo.getExpanded() != null && !graphicInfo.getExpanded().booleanValue());
                } while (this.isPartOfCollapsedSubProcess(subProcess1, bpmnModel));

                Iterator var19 = subProcess1.getArtifacts().iterator();

                while (var19.hasNext()) {
                    Artifact subProcessArtifact = (Artifact) var19.next();
                    this.drawArtifact(processDiagramCanvas, bpmnModel, subProcessArtifact);
                }
            }
        }
    }

    protected static DefaultProcessDiagramCanvas initProcessDiagramCanvas(BpmnModel bpmnModel, String imageType, String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader) {
        double minX = 1.7976931348623157E308D;
        double maxX = 0.0D;
        double minY = 1.7976931348623157E308D;
        double maxY = 0.0D;

        GraphicInfo nrOfLanes;
        for (Iterator flowNodes = bpmnModel.getPools().iterator(); flowNodes.hasNext(); maxY = nrOfLanes.getY() + nrOfLanes.getHeight()) {
            Pool artifacts = (Pool) flowNodes.next();
            nrOfLanes = bpmnModel.getGraphicInfo(artifacts.getId());
            minX = nrOfLanes.getX();
            maxX = nrOfLanes.getX() + nrOfLanes.getWidth();
            minY = nrOfLanes.getY();
        }

        List var23 = gatherAllFlowNodes(bpmnModel);
        Iterator var24 = var23.iterator();

        label155:
        while (var24.hasNext()) {
            FlowNode var26 = (FlowNode) var24.next();
            GraphicInfo artifact = bpmnModel.getGraphicInfo(var26.getId());
            if (artifact.getX() + artifact.getWidth() > maxX) {
                maxX = artifact.getX() + artifact.getWidth();
            }

            if (artifact.getX() < minX) {
                minX = artifact.getX();
            }

            if (artifact.getY() + artifact.getHeight() > maxY) {
                maxY = artifact.getY() + artifact.getHeight();
            }

            if (artifact.getY() < minY) {
                minY = artifact.getY();
            }

            Iterator process = var26.getOutgoingFlows().iterator();

            while (true) {
                List l;
                do {
                    if (!process.hasNext()) {
                        continue label155;
                    }

                    SequenceFlow graphicInfoList = (SequenceFlow) process.next();
                    l = bpmnModel.getFlowLocationGraphicInfo(graphicInfoList.getId());
                } while (l == null);

                Iterator graphicInfo = l.iterator();

                while (graphicInfo.hasNext()) {
                    GraphicInfo graphicInfo1 = (GraphicInfo) graphicInfo.next();
                    if (graphicInfo1.getX() > maxX) {
                        maxX = graphicInfo1.getX();
                    }

                    if (graphicInfo1.getX() < minX) {
                        minX = graphicInfo1.getX();
                    }

                    if (graphicInfo1.getY() > maxY) {
                        maxY = graphicInfo1.getY();
                    }

                    if (graphicInfo1.getY() < minY) {
                        minY = graphicInfo1.getY();
                    }
                }
            }
        }

        List var25 = gatherAllArtifacts(bpmnModel);
        Iterator var27 = var25.iterator();

        GraphicInfo var37;
        while (var27.hasNext()) {
            Artifact var29 = (Artifact) var27.next();
            GraphicInfo var31 = bpmnModel.getGraphicInfo(var29.getId());
            if (var31 != null) {
                if (var31.getX() + var31.getWidth() > maxX) {
                    maxX = var31.getX() + var31.getWidth();
                }

                if (var31.getX() < minX) {
                    minX = var31.getX();
                }

                if (var31.getY() + var31.getHeight() > maxY) {
                    maxY = var31.getY() + var31.getHeight();
                }

                if (var31.getY() < minY) {
                    minY = var31.getY();
                }
            }

            List var33 = bpmnModel.getFlowLocationGraphicInfo(var29.getId());
            if (var33 != null) {
                Iterator var35 = var33.iterator();

                while (var35.hasNext()) {
                    var37 = (GraphicInfo) var35.next();
                    if (var37.getX() > maxX) {
                        maxX = var37.getX();
                    }

                    if (var37.getX() < minX) {
                        minX = var37.getX();
                    }

                    if (var37.getY() > maxY) {
                        maxY = var37.getY();
                    }

                    if (var37.getY() < minY) {
                        minY = var37.getY();
                    }
                }
            }
        }

        int var28 = 0;
        Iterator var30 = bpmnModel.getProcesses().iterator();

        while (var30.hasNext()) {
            Process var32 = (Process) var30.next();
            Iterator var34 = var32.getLanes().iterator();

            while (var34.hasNext()) {
                Lane var36 = (Lane) var34.next();
                ++var28;
                var37 = bpmnModel.getGraphicInfo(var36.getId());
                if (var37.getX() + var37.getWidth() > maxX) {
                    maxX = var37.getX() + var37.getWidth();
                }

                if (var37.getX() < minX) {
                    minX = var37.getX();
                }

                if (var37.getY() + var37.getHeight() > maxY) {
                    maxY = var37.getY() + var37.getHeight();
                }

                if (var37.getY() < minY) {
                    minY = var37.getY();
                }
            }
        }

        if (var23.isEmpty() && bpmnModel.getPools().isEmpty() && var28 == 0) {
            minX = 0.0D;
            minY = 0.0D;
        }

        return new CustomProcessDiagramCanvas((int) maxX + 10, (int) maxY + 10, (int) minX, (int) minY, imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);
    }


    private static void drawHighLight(DefaultProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {
        processDiagramCanvas.drawHighLight((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight());

    }

    private static void drawHighLightNow(CustomProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {
        processDiagramCanvas.drawHighLightNow((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight());

    }

    private static void drawHighLightEnd(CustomProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {
        processDiagramCanvas.drawHighLightEnd((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight());

    }

    @SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
    @Override
    protected void drawActivity(DefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel,
                                FlowNode flowNode, List<String> highLightedActivities, List<String> highLightedFlows, double scaleFactor, Boolean drawSequenceFlowNameWithNoLabelDI) {

        ActivityDrawInstruction drawInstruction = activityDrawInstructions.get(flowNode.getClass());
        if (drawInstruction != null) {

            drawInstruction.draw(processDiagramCanvas, bpmnModel, flowNode);

            // Gather info on the multi instance marker
            boolean multiInstanceSequential = false;
            boolean multiInstanceParallel = false;
            boolean collapsed = false;
            if (flowNode instanceof Activity) {
                Activity activity = (Activity) flowNode;
                MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = activity.getLoopCharacteristics();
                if (multiInstanceLoopCharacteristics != null) {
                    multiInstanceSequential = multiInstanceLoopCharacteristics.isSequential();
                    multiInstanceParallel = !multiInstanceSequential;
                }
            }

            // Gather info on the collapsed marker
            GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
            if (flowNode instanceof SubProcess) {
                collapsed = graphicInfo.getExpanded() != null && !graphicInfo.getExpanded();
            } else if (flowNode instanceof CallActivity) {
                collapsed = true;
            }
            BigDecimal a = BigDecimal.valueOf(scaleFactor);
            BigDecimal b = BigDecimal.valueOf(1.0);
            if (b.equals(a)) {
                // Actually draw the markers
                processDiagramCanvas.drawActivityMarkers((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight(),
                        multiInstanceSequential, multiInstanceParallel, collapsed);
            }

            // Draw highlighted activities
            if (highLightedActivities.contains(flowNode.getId())) {

                if (highLightedActivities.get(highLightedActivities.size() - 1).equals(flowNode.getId())) {
                    if (bpmnModel.getFlowElement(flowNode.getId()) instanceof EndEvent) {
                        drawHighLightEnd((CustomProcessDiagramCanvas) processDiagramCanvas, bpmnModel.getGraphicInfo(flowNode.getId()));
                    } else {
                        drawHighLightNow((CustomProcessDiagramCanvas) processDiagramCanvas, bpmnModel.getGraphicInfo(flowNode.getId()));
                    }
                } else {
                    drawHighLight(processDiagramCanvas, bpmnModel.getGraphicInfo(flowNode.getId()));
                }


            }

        }

        // Outgoing transitions of activity
        for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
            boolean highLighted = (highLightedFlows.contains(sequenceFlow.getId()));
            String defaultFlow = null;
            if (flowNode instanceof Activity) {
                defaultFlow = ((Activity) flowNode).getDefaultFlow();
            } else if (flowNode instanceof Gateway) {
                defaultFlow = ((Gateway) flowNode).getDefaultFlow();
            }

            boolean isDefault = false;
            if (defaultFlow != null && defaultFlow.equalsIgnoreCase(sequenceFlow.getId())) {
                isDefault = true;
            }
            boolean drawConditionalIndicator = sequenceFlow.getConditionExpression() != null && !(flowNode instanceof Gateway);

            String sourceRef = sequenceFlow.getSourceRef();
            String targetRef = sequenceFlow.getTargetRef();
            FlowElement sourceElement = bpmnModel.getFlowElement(sourceRef);
            FlowElement targetElement = bpmnModel.getFlowElement(targetRef);
            List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());
            if (graphicInfoList != null && graphicInfoList.size() > 0) {
                graphicInfoList = connectionPerfectionizer(processDiagramCanvas, bpmnModel, sourceElement, targetElement, graphicInfoList);
                int[] xPoints = new int[graphicInfoList.size()];
                int[] yPoints = new int[graphicInfoList.size()];

                for (int i = 1; i < graphicInfoList.size(); i++) {
                    GraphicInfo graphicInfo = graphicInfoList.get(i);
                    GraphicInfo previousGraphicInfo = graphicInfoList.get(i - 1);

                    if (i == 1) {
                        xPoints[0] = (int) previousGraphicInfo.getX();
                        yPoints[0] = (int) previousGraphicInfo.getY();
                    }
                    xPoints[i] = (int) graphicInfo.getX();
                    yPoints[i] = (int) graphicInfo.getY();

                }

                processDiagramCanvas.drawSequenceflow(xPoints, yPoints, drawConditionalIndicator, isDefault, highLighted, scaleFactor);


                // Draw sequenceflow label
                GraphicInfo labelGraphicInfo = bpmnModel.getLabelGraphicInfo(sequenceFlow.getId());
                if (labelGraphicInfo != null) {
                    processDiagramCanvas.drawLabel(sequenceFlow.getName(), labelGraphicInfo, false);
                } else {
                    if (drawSequenceFlowNameWithNoLabelDI) {
                        GraphicInfo lineCenter = getLineCenter(graphicInfoList);
                        processDiagramCanvas.drawLabel(sequenceFlow.getName(), lineCenter, false);
                    }

                }
            }
        }

        // Nested elements
        if (flowNode instanceof FlowElementsContainer) {
            for (FlowElement nestedFlowElement : ((FlowElementsContainer) flowNode).getFlowElements()) {
                if (nestedFlowElement instanceof FlowNode && !isPartOfCollapsedSubProcess(nestedFlowElement, bpmnModel)) {
                    drawActivity(processDiagramCanvas, bpmnModel, (FlowNode) nestedFlowElement,
                            highLightedActivities, highLightedFlows, scaleFactor, drawSequenceFlowNameWithNoLabelDI);
                }
            }
        }
    }
}

8.4.2 读取XML文件

/**
 * 读取XML文件
 * 
 * @param deployId 流程部署ID act_ge_bytearray 表中 deployment_id值
 * @throws IOException 如果发生I/O错误
 */
public String readXml(String deployId) throws IOException {
    ProcessDefinition definition = repositoryService.createProcessDefinitionQuery().deploymentId(deployId).singleResult();
    InputStream inputStream = repositoryService.getResourceAsStream(definition.getDeploymentId(), definition.getResourceName());
    String result = IOUtils.toString(inputStream, StandardCharsets.UTF_8.name());
    return result;
}

8.4.3 读取流程图文件

/**
 * 读取流程图文件
 * 
 * @param deployId 流程部署ID act_ge_bytearray 表中 deployment_id值
 */
public InputStream readImage(String deployId) {
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployId).singleResult();
    //获得图片流
    DefaultProcessDiagramGenerator diagramGenerator = new DefaultProcessDiagramGenerator();
    BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
    //输出为图片
    return diagramGenerator.generateDiagram(
            bpmnModel,
            "png",
            Collections.emptyList(),
            Collections.emptyList(),
            "宋体",
            "宋体",
            "宋体",
            null,
            1.0,
            false);
}

8.5 用户/角色管理

8.5.1 获取下一个用户任务

/**
 * 获取下一个用户任务
 * 
 * @param taskId 当前用户任务ID
 */
private UserTask getUserTask(String taskId) {
     // 获取当前任务
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
    // 获取流程定义
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId())
            .singleResult();
    // 获取当前节点信息
    BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
    FlowNode flowNode = (FlowNode) bpmnModel.getFlowElement(task.getTaskDefinitionKey());
    // 当前节点的输出连线
    List<SequenceFlow> outgoingFlows = flowNode.getOutgoingFlows();
    if (CollectionUtils.isEmpty(outgoingFlows)) {
        return null;
    }
    for (SequenceFlow outgoingFlow : outgoingFlows) {
        //类型判断
        FlowElement targetFlowElement = outgoingFlow.getTargetFlowElement();
        //用户任务
        if (targetFlowElement instanceof UserTask) {
            log.info("Find the next userTask success!");
            return (UserTask) targetFlowElement;
        } else if (targetFlowElement instanceof EndEvent) {
            log.info("The next node is EndEvent!");
            return null;
        }
        // TODO:下一个节点为网关,如何进行业务处理
        else if (targetFlowElement instanceof ExclusiveGateway) {
            log.info("下一个任务为排他网关");
            ExclusiveGateway exclusiveGateway = (ExclusiveGateway) outgoingFlow.getTargetFlowElement();
            List<SequenceFlow> gatewayOutgoingFlows = exclusiveGateway.getOutgoingFlows();
            getUserTask(gatewayOutgoingFlows);
        }
    }
    log.info("The next userTask doesn't exist!");
    return null;
}

8.5.2 获取下一个节点角色

  1. flowable:candidateGroups="${角色ID的value值}",即:flowable:candidateGroups="${123456}"
/**
 * 获取下一节点角色ID列表 - 流程图定义的参数值 - 静态值
 * 例如:flowable:candidateGroups="${roleId}" - 获取到的是${roleId}(不会解析El表达式)
 *
 * @param inDTO 输入DTO
 * @return 下一节点角色ID列表 - 流程图定义的参数值
 */
@Override
public String getNextNodeRoleIds(FlowTaskRoleInDTO inDTO) {
    // 获取当前任务
    Task task = taskService.createTaskQuery().taskId(inDTO.getTaskId()).singleResult();
    // 获取流程定义
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId())
            .singleResult();
    // 获取当前节点信息
    BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
    FlowNode flowNode = (FlowNode) bpmnModel.getFlowElement(task.getTaskDefinitionKey());
    // 当前节点的输出连线
    List<SequenceFlow> outgoingFlows = flowNode.getOutgoingFlows();
    // 遍历返回下一个用户节点信息 - 调用8.5.1的接口
    UserTask userTask = getUserTask(outgoingFlows);
    if (userTask == null) {
        return null;
    }
    if (!CollectionUtils.isEmpty(userTask.getCandidateGroups())) {
        log.info("The CandidateGroups: {}", userTask.getCandidateGroups());
        return String.join(",", userTask.getCandidateGroups());
    }
    if (!CollectionUtils.isEmpty(userTask.getCandidateUsers())) {
        log.info("The CandidateUsers: {}", userTask.getCandidateUsers());
        return String.join(",", userTask.getCandidateUsers());
    } else {
        log.info("The Assignee: {}", userTask.getAssignee());
        return userTask.getAssignee();
    }
}
  1. flowable:candidateGroups="${角色ID的Key值}",即:flowable:candidateGroups="${roleId}"
/**
 * 获取下一个节点角色列表 - 这种方式需要在启动流程时,传入全局的角色列表Value值
 * 即传入<k,v> = <roleIdkey, roleIdList>
 * 由于是map,可以传入多个,即最终为:
 * new HashMap<k,v>() {
 *     put("roleIdA", "123,2332,34342,23"); // roleIdA有4个角色ID
 *	   put("roleIdB", "12,333"); // roleIdB有2个角色ID
 * }
 * 一共有6个角色ID
 * 
 * @param taskId 用户任务ID
 * @return 角色列表字符串 用,隔开
 */
private static final String ROLE_IDS_SPLIT = ",";

public String getNextNodeRoleIdList(String taskId) {
    
    // 获取当前任务
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
    // 获取流程定义
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId()).singleResult();
    // 获取当前节点信息
    BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
    FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
    // 遍历返回下一个用户节点信息
    // 调用8.5.1的接口
    UserTask userTask = flowTaskService.getUserTask(flowElement);
    if (userTask == null) {
        return null;
    }
    if (StringUtil.isNotEmpty(userTask.getAssignee())) {
        String assignee = userTask.getAssignee().replace("${", "").replace("}", "");
        throw new Exception("下一个用户任务审核人员已设置为:[" + assignee + "],不能更改设置!");
    }
    // 获取全局执行ID(根节点ExecutionID)
    String executionId = runtimeService.createExecutionQuery().processInstanceId(task.getProcessInstanceId()).onlyProcessInstanceExecutions().singleResult().getId();
    // 获取下一个节点的角色列表
    List<String> roleIdList = new ArrayList<>();
    if (!CollectionUtils.isEmpty(userTask.getCandidateGroups())) {
        String candidateGroupsStr = String.join(ROLE_IDS_SPLIT, userTask.getCandidateGroups()).replace("${", "").replace("}", "");
        List<String> candidateGroups = Arrays.asList(candidateGroupsStr.split(ROLE_IDS_SPLIT));
        log.info("The CandidateGroups: {}", candidateGroups);
        // 汇总每个角色组对应的角色ID列表 ---> candidateGroups="${roleA},${roleB}" ---> roleA对应一个组的角色
        candidateGroups.forEach(candidateId -> {
            List<String> roleId = (List<String>) runtimeService.getVariableLocal(executionId, candidateId);
            roleIdList.addAll(roleId);
        });
    }
    if (!CollectionUtils.isEmpty(userTask.getCandidateUsers())) {
        String candidateUsersStr = String.join(ROLE_IDS_SPLIT, userTask.getCandidateUsers()).replace("${", "").replace("}", "");
        List<String> candidateUsers = Arrays.asList(candidateUsersStr.split(ROLE_IDS_SPLIT));
        log.info("The CandidateUsers: {}", candidateUsers);
        // 汇总每个角色对应的角色ID ---> candidateUsers="${roleA},${roleB}" ---> 只有两个角色
        candidateUsers.forEach(candidateId -> {
            String roleId = runtimeService.getVariableLocal(executionId, candidateId, String.class);
            roleIdList.add(roleId);
        });
    }
    if (CollectionUtils.isEmpty(roleIdList)) {
        throw new Exception("获取下一个用户节点角色列表失败,角色列表为空!");
    }
    return String.join(ROLE_IDS_SPLIT, roleIdList);
}

8.5.3 动态分配审批人 - 监听器实现

基本思路,在上一个节点的时候,将下一节点审批人通过流程参数传递到下一个流程中。

用流程实例ID作为Key,value为下一节点审批人

在进入下一节点时,监听器触发,动态设置审批人

@Component
@Slf4j
public class FlowableBaseEventListenerImpl implements FlowableEventListener {
    @Override
    public void onEvent(FlowableEvent event) {
        FlowableEventType type = event.getType();
        if (type == FlowableEngineEventType.TASK_CREATED) {
            log.info("任务创建完成");
            // 用户任务处理 - 分配审批人
            FlowableEngineEventImpl flowableEngineEvent = (FlowableEngineEventImpl) event;
            String processInstanceId = flowableEngineEvent.getProcessInstanceId();
            // 获取当前任务
            Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).active().singleResult();
            if (task == null) {
                throw new Exception("任务不存在");
            }
            if (task.isSuspended()) {
                throw new Exception("任务处于挂起状态");
            }
            // 流程定义信息
            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId()).singleResult();
            // 当前节点
            FlowElement flowElement = repositoryService.getBpmnModel(processDefinition.getId()).getFlowElement(task.getTaskDefinitionKey());
            if (!(flowElement instanceof UserTask)) {
                log.info("该节点不是任务节点");
                return;
            }
            if (!StringUtil.isEmpty(task.getAssignee())) {
                log.info("该节点已经设置审批人");
                return;
            }
            String taskId = task.getId();
            Object variable = taskService.getVariable(taskId, processInstanceId);
            String userId = (String) variable;
            if (StringUtil.isEmpty(userId)) {
                throw new Exception("指定的审批人员为空!");
            }
            // 任务分配
            try {
                taskService.claim(taskId, userId);
            } catch (FlowableException e) {
                log.info("Task claim failed! taskId: {}, userId: {}", taskId, userId, e);
                throw new FlowableException(e.getMessage());
            }
            // 移除原有变量,防止复用
            taskService.removeVariable(taskId, processInstanceId);
            log.info("任务审批人员设置完成");
        }
    }
}

8.6 流程功能图

8.7 流程变量

8.7.1 新增全局流程变量

// 先取出流程变量Map,再新增或者修改流程变量
String executionId = runtimeService.createExecutionQuery().processInstanceId(task.getProcessInstanceId()).onlyProcessInstanceExecutions().singleResult().getId();
Map<String, Object> variables = runtimeService.getVariables(executionId);
variables.put(FlowableConstant.VAR_SKIP, false);
variables.put(FlowableConstant.VAR_SKIP_EXPRESSION_ENABLED, false);
variables.put(FlowableConstant.VAR_REJECT, true);
runtimeService.setVariables(executionId, variables);

九、后续学习方向

  1. Flowable四种类型网关应用
  2. Flowable多实例会签应用
  3. FLowable复杂流程图例实现

9.2 Flowable多实例会签

9.2.1 多实例会签

  1. 利用脚本任务实现默认赋值——使用脚本语言groovy,对assigneeList进行赋值(注意,脚本任务在会签节点前)
  • 由于flowable:autoStoreVariables="false",因此该变量assigneeList仅存在于脚本变量执行期间。
  • 通过flowable:resultVariable="assigneeList"将脚本任务的返回值赋值给流程变量assigneeList
  • 这里通过生成脚本变量assigneeList,返回assgineeList给流程变量,由于此时流程变量assgineeList并未存在,因此流程会自动创建一个全局流程变量assgineeList,在表act_ru_variable中,能够见到变量assgineeListBYTEARRAY_ID_字段有值。
<scriptTask id="sid-7FD5D420-176F-4935-A2C7-2922AB2F726B" scriptFormat="groovy" flowable:autoStoreVariables="false" flowable:resultVariable="assigneeList">
  <script>
    assigneeList = ["u6","u7","u8","u9","u10"]
  </script>
</scriptTask>
  1. 需要增加groovy的配置
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.15</version>
</dependency>
  1. 会签节点配置
<userTask id="sid-7BF513AD-E262-474F-A9B5-1756C0093609" name="审批人" flowable:assignee="${assignee}" flowable:formFieldValidation="true">
  <extensionElements>
    <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
  </extensionElements>
  <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="assigneeList" flowable:elementVariable="assignee">
    <!-- <extensionElements>标签可以增加TaskListener对会签每个任务监听 -->
    <extensionElements></extensionElements>
    <loopCardinality>5</loopCardinality>
    <completionCondition>${multiInstanceCompleteTaskListener.accessCondition(execution)}</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>
  1. 会签结束条件设置
@Component
public class MultiInstanceCompleteTaskListener implements Serializable {
    public boolean accessCondition(DelegateExecution execution) {
        // 已完成的实例数
        int nrOfCompletedInstances = (int)execution.getVariable("nrOfCompletedInstances");
        // 会签实例总数
        int nrOfInstances = (int) execution.getVariable("nrOfInstances");
        return nrOfCompletedInstances == nrOfInstances - 3;
    }
}

9.2.2 判断用户任务是否为会签

public boolean isMultiInstance(String taskId) {
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
    if (task == null) {
        return false;
    }
    // 流程定义信息
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(task.getProcessDefinitionId()).singleResult();
    // 当前节点
    FlowElement flowElement = repositoryService.getBpmnModel(processDefinition.getId()).getFlowElement(task.getTaskDefinitionKey());
    if (flowElement instanceof UserTask) {
        // 如果该节点的行为为会签行为,说明该节点为会签节点
        return ((UserTask) flowElement).getBehavior() instanceof ParallelMultiInstanceBehavior ||
            ((UserTask) flowElement).getBehavior() instanceof SequentialMultiInstanceBehavior;
    }
    return false;
    }

十、参考资料

  1. 一文看懂开源工作流引擎 Flowable:xie.infoq.cn/article/ece…
  2. Flowable BPMN用户手册(v6.3.0):tkjohn.github.io/flowable-us…
  3. 基于RuoYi-vue + flowable 6.7.2 的工作流管理:gitee.com/tony2y/RuoY…
  4. Flowable5-历史:www.jianshu.com/p/518d61b5c…
  5. Flowable从入门到入土(1)-初始Flowable:juejin.cn/post/684490…
  6. Flowable源码注释:blog.csdn.net/jinyj2014/c…
  7. flowable任务监听器 事件监听器:blog.csdn.net/zzchances/a…
  8. SpringBoot整合Flowable工作流-2(代码整合):blog.csdn.net/JinglongSou…