Flowable工作流教程

Flowable工作流教程

前言

Flowable是什么

Flowable 是一个使用 Java 编写的轻量级业务流程引擎,使用 Apache V2 license 协议开源。2016 年 10 月,Activiti 工作流引擎的主要开发者离开 Alfresco 公司并在 Activiti 分支基础上开启了 Flowable 开源项目。基于 Activiti v6 beta4 发布的第一个 Flowable release 版本为 6.0。以 JAR 形式发布使得 Flowable 可以轻易加入任何 Java 环境:Java SE、Tomcat、Jetty 或 Spring 之类的 servlet 容器;JBoss 或 WebSphere 之类的 Java EE 服务器等等。 另外,也可以使用 Flowable REST API 进行 HTTP 调用。

Flowable 项目中包括 BPMN(Business Process Model and Notation)引擎、CMMN(Case Management Model and Notation)引擎、DMN(Decision Model and Notation)引擎、表单引擎(Form Engine)等模块。也有许多 Flowable 应用(Flowable Modeler、Flowable Admin、Flowable IDM 与 Flowable Task),并提供了直接可用的 UI 示例。模块之间协作关系可以参考下图:

构建 OA、CRM、TMS、财务管理等系统时,若基于 Flowable 生态做定制化开发可以大大减少开发成本,避免写复杂而难以维护的条件代码。Flowable 的关键为其核心引擎,核心引擎是一组服务的集合,并提供管理与执行业务流程的 API。Flowable 生态系统中的业务流程引擎(BPMN)可以与决策引擎(DMN)、案例模型引擎(CMMN)、表单引擎联动,开发者可以根据业务需求选用其中一个或多个模块,通过模块之间相互协作构建业务系统、以实现强大的功能。Flowable 团队在开源项目之外也承接商业项目,提供 Flowable Work、Flowable Engage 等商业产品与服务,www.flowable.com 网站上提供了该团队为银行和保险业实施过的成功案例,展示了 Flowable 对复杂场景的业务支撑能力。下文简要介绍 Flowable 中的几个主要引擎模块

构建命令行程序

创建流程引擎

在这个初步教程中,将构建一个简单的例子,以展示如何创建一个Flowable流程引擎,介绍一些核心概念,并展示如何使用API。

我们将构建的例子是一个简单的请假(holiday request)流程:

  • 雇员(employee)申请几天的假期
  • 经理(manager)批准或驳回申请
  • 我们会模拟将申请注册到某个外部系统,并给雇员发送结果邮件

创建Maven项目

项目创建后添加maven依赖

<dependencies>
        <!--Flowable流程引擎。使我们可以创建一个ProcessEngine流程引擎对象,并访问Flowable API -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-engine</artifactId>
            <version>6.3.0</version>
        </dependency>
        <!--一个内存数据库。因为Flowable引擎在运行流程实例时,需要使用数据库来存储执行与历史数据 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!-- 日志框架。Flowable使用SLF4J作为内部日志框架。本例中,我们使用log4j作为SLF4J的实现-->
        <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>
    </dependencies>

创建一个新的Java类,并添加标准的Java main方法,用来生产流程所需的库表结构:

public class HolidayRequest {
    public static void main(String[] args) {
        //创建了一个独立(standalone)配置对象。这里的’独立’指的是引擎是完全独立创建及使用的(而不是在Spring环境中使用,这时需要使用SpringProcessEngineConfiguration类代替)
        ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration();
        //传递了一个内存Mysql数据库实例的JDBC连接参数
        configuration.setJdbcDriver("com.mysql.cj.jdbc.Driver");
        configuration.setJdbcUsername("root");
        configuration.setJdbcPassword("123456");
        configuration.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/flowable_learn?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false");
        //设置了true,确保在JDBC参数连接的数据库中,数据库表结构不存在时,会创建相应的表结构。 另外,Flowable也提供了一组SQL文件,可用于手动创建所有表的数据库表结构
        configuration.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        //创建ProcessEngine实例
        ProcessEngine processEngine = configuration.buildProcessEngine();
    }
}

首先要做的是初始化ProcessEngine流程引擎实例。创建一个ProcessEngineConfiguration实例 ,并配置数据库JDBC连接。然后由ProcessEngineConfiguration创建ProcessEngine实例。

执行main方法后,数据库会产生一批数据表

Flowable使用SLF4J作为内部日志框架。在这个例子中,我们使用log4j作为SLF4J的实现。Log4j需要一个配置文件。在src/main/resources文件夹下添加log4j.properties文件,并写入下列内容

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

进行具体的流程研发

部署流程定义

我们要构建的流程是一个非常简单的请假流程。Flowable引擎需要流程定义为BPMN 2.0格式,这是一个业界广泛接受的XML标准。 在Flowable术语中,我们将其称为一个流程定义(process definition)。一个流程定义可以启动多个流程实例(process instance)。流程定义可以看做是重复执行流程的蓝图。 在这个例子中,流程定义定义了请假的各个步骤,而一个流程实例对应某个雇员提出的一个请假申请

我们要使用的流程定义为:

这个流程应该已经十分自我解释了。但为了明确起见,说明一下几个要点:

  • 我们假定启动流程需要提供一些信息,例如雇员名字、请假时长以及说明。当然,这些可以单独建模为流程中的第一步。 但是如果将它们作为流程的“输入信息”,就能保证只有在实际请求时才会建立一个流程实例。否则(将提交作为流程的第一步),用户可能在提交之前改变主意并取消,但流程实例已经创建了。 在某些场景中,就可能影响重要的指标(例如启动了多少申请,但还未完成),取决于业务目标。
  • ·左侧的圆圈叫做启动事件(start event)。这是一个流程实例的起点·
  • 第一个矩形是一个用户任务(user task)。这是流程中人类用户操作的步骤。在这个例子中,经理需要批准或驳回申请
  • 取决于经理的决定,排他网关(exclusive gateway) (带叉的菱形)会将流程实例路由至批准或驳回路径。
  • 如果批准,则需要将申请注册至某个外部系统,并跟着另一个用户任务,将经理的决定通知给申请人。当然也可以改为发送邮件。
  • 如果驳回,则为雇员发送一封邮件通知他。

一般来说,这样的流程定义使用可视化建模工具建立,如Flowable Designer(Eclipse)或Flowable Web Modeler(Web应用)。但在这里我们直接撰写XML,以熟悉BPMN 2.0及其概念。

将下面的XML保存在src/main/resources文件夹下名为holiday-request.bpmn20.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: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"
  xmlns:flowable="http://flowable.org/bpmn"
  typeLanguage="http://www.w3.org/2001/XMLSchema"
  expressionLanguage="http://www.w3.org/1999/XPath"
  targetNamespace="http://www.flowable.org/processdef">

  <process id="holidayRequest" name="Holiday Request" isExecutable="true">

    <startEvent id="startEvent"/>
    <sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>

    <userTask id="approveTask" name="Approve or reject request"/>
    <sequenceFlow sourceRef="approveTask" targetRef="decision"/>

    <exclusiveGateway id="decision"/>
    <sequenceFlow sourceRef="decision" targetRef="externalSystemCall">
      <conditionExpression xsi:type="tFormalExpression">
        <![CDATA[
          ${approved}
        ]]>
      </conditionExpression>
    </sequenceFlow>
    <sequenceFlow  sourceRef="decision" targetRef="sendRejectionMail">
      <conditionExpression xsi:type="tFormalExpression">
        <![CDATA[
          ${!approved}
        ]]>
      </conditionExpression>
    </sequenceFlow>

    <serviceTask id="externalSystemCall" name="Enter holidays in external system"
        flowable:class="org.flowable.CallExternalSystemDelegate"/>
    <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>

    <userTask id="holidayApprovedTask" name="Holiday approved"/>
    <sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>

    <serviceTask id="sendRejectionMail" name="Send out rejection email"
        flowable:class="org.flowable.SendRejectionMail"/>
    <sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>

    <endEvent id="approveEnd"/>

    <endEvent id="rejectEnd"/>

  </process>

</definitions>

每一个步骤(在BPMN 2.0术语中称作活动(activity)都有一个id属性,为其提供一个在XML文件中唯一的标识符。所有的活动都可以设置一个名字,以提高流程图的可读性。

活动之间通过顺序流(sequence flow)连接,在流程图中是一个有向箭头。在执行流程实例时,执行(execution)会从启动事件沿着顺序流流向下一个活动。

离开排他网关(带有X的菱形)的顺序流很特别:都以表达式(expression)的形式定义了条件(condition) 。当流程实例的执行到达这个网关时,会计算条件,并使用第一个计算为true的顺序流。这就是排他的含义:只选择一个。

这里用作条件的表达式为"$ {approved}",这是${approved == true}的简写。变量’approved’被称作流程变量。

现在我们已经有了流程BPMN 2.0 XML文件,下来需要将它部署(deploy)到引擎中(还是在我们main方法中添加)。

 RepositoryService repositoryService = processEngine.getRepositoryService();
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("holiday-request.bpmn20.xml")
                .name("员工请假流程")
                .deploy();

我们现在可以通过API查询验证流程定义已经部署在引擎中。通过RepositoryService创建的ProcessDefinitionQuery对象实现。

 ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deployment.getId())
                .singleResult();
        System.out.println("定义流程的名称 : " + processDefinition.getName());

启动一个流程

现在已经在流程引擎中部署了流程定义,因此可以使用这个流程定义作为“蓝图”启动流程实例。

要启动流程实例,需要提供一些初始化流程变量。一般来说,可以通过呈现给用户的表单,或者在流程由其他系统自动触发时通过REST API,来获取这些变量。在这个例子里,我们简化为使用java.util.Scanner类在命令行输入一些数据:

//4模拟信息
        Scanner scanner = new Scanner(System.in);
        System.out.println("姓名");
        String employee = scanner.nextLine();
        System.out.println("请假时长");
        Integer nrOfHolidays = Integer.valueOf(scanner.nextLine());
        System.out.println("请假原因");
        String description = scanner.nextLine();

我们使用RuntimeService启动一个流程实例。收集的数据作为一个java.util.Map实例传递,其中的键就是之后用于获取变量的标识符。这个流程实例使用key启动。这个key就是BPMN 2.0 XML文件中设置的id属性,在这个例子里是holidayRequest。

<process id="holidayRequest" name="请假流程" isExecutable="true"> //这个id是我们流程的key

将用户录入的信息保存到map中,作为流程变量

Map<String, Object> maps = new HashMap<String, Object>();
        maps.put("employee", employee);
        maps.put("nrOfHolidays", nrOfHolidays);
        maps.put("description", description);

我们使用RuntimeService启动一个流程实例。收集的数据作为一个java.util.Map实例传递,其中的键就是之后用于获取变量的标识符。这个流程实例使用key启动。这个key就是BPMN 2.0 XML文件中设置的id属性,在这个例子里是holidayRequest。

<process id="holidayRequest" name="请假流程" isExecutable="true"> //这个id是我们流程的key

将用户录入的信息保存到map中,作为流程变量

 //5使用引擎部署流程
        ProcessInstance processInstance = processEngine.getRuntimeService().startProcessInstanceByKey("holidayRequest", maps);

在流程实例启动后,会创建一个执行(execution),并将其放在启动事件上。从这里开始,这个执行沿着顺序流移动到经理审批的用户任务,并执行用户任务行为。这个行为将在数据库中创建一个任务,该任务可以之后使用查询找到。用户任务是一个等待状态(wait state),引擎会停止执行,返回API调用处。

启动任务后我们需要完成任务,我们来实现操作查询任务并且完成任务

我们还没有为用户任务配置办理人。我们想将第一个任务指派给"经理(managers)"组,而第二个用户任务指派给请假申请的提交人。因此需要为第一个任务添加candidateGroups属性:

在xml文件中添加flowable:candidateGroups="managers"
如下:
<userTask id="approveTask" name="Approve or reject request" flowable:candidateGroups="managers"/>

并如下所示为第二个任务添加assignee属性。请注意我们没有像上面的’managers’一样使用静态值,而是使用一个流程变量动态指派。这个流程变量是在流程实例启动时传递的:

<userTask id="holidayApprovedTask" name="Holiday approved" flowable:assignee="${employee}"/>

要获得实际的任务列表,需要通过TaskService创建一个TaskQuery。我们配置这个查询只返回’managers’组的任务:

//6创建任务流程对象
        TaskService taskService = processEngine.getTaskService();
        //7根据组名称来查询代办任务
        List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
        System.out.println("你有" + tasks.size() + "个任务");
        for (int i = 0; i < tasks.size(); i++) {
            System.out.println((i + 1) + ")" + tasks.get(i).getName() + ":编号为" + (i + 1));
        }

可以使用任务Id获取特定流程实例的变量,并在屏幕上显示实际的申请:

System.out.println("你想处理哪个任务?");
        Integer taskIndex = Integer.valueOf(scanner.nextLine());
        Task task = tasks.get(taskIndex - 1);
        //通过任务id获取在流程中的信息
        Map<String, Object> variables = taskService.getVariables(task.getId());
        System.out.println(variables.get("employee") + "想请假" + variables.get("nrOfHolidays") + "天,请假理由是:" + variables.get("description") + ",是否同意请假?(y/n)");

经理现在就可以完成任务了。在现实中,这通常意味着由用户提交一个表单。表单中的数据作为流程变量传递。在这里,我们在完成任务时传递带有’approved’变量(这个名字很重要,因为之后会在顺序流的条件中使用!)的map来模拟:

//判断审批是否通过
        boolean approved = scanner.nextLine().toLowerCase().equals("y");
        variables = new HashMap<String, Object>();
        variables.put("approved", approved);
        //保存意见,完成任务(不保存否则任务完成后找不到)
        if (approved) {
            Comment comment = taskService.addComment(task.getId(), processInstance.getProcessInstanceId(), "同意休假");
            comment.setUserId("manager");
            taskService.saveComment(comment);
        } else {
            Comment comment = taskService.addComment(task.getId(), processInstance.getProcessInstanceId(), "不同意休假");
            comment.setUserId("manager");
            taskService.saveComment(comment);
        }
//处理任务
        taskService.complete(task.getId(), variables);

现在任务完成,并会在离开排他网关的两条路径中,基于’approved’流程变量选择一条。

实现自动逻辑(JavaDelegate)

我们还没有实现申请通过后执行的自动逻辑。在BPMN 2.0 XML中,这是一个服务任务(service task):

<serviceTask id="externalSystemCall" name="Enter holidays in external system"
                     flowable:class="com.thrj.CallExternalSystemDelegate"/>

在现实中,这个逻辑可以做任何事情,创建一个新的类,填入org.flowable作为包名,CallExternalSystemDelegate作为类名。让这个类实现org.flowable.engine.delegate.JavaDelegate接口,并实现execute方法:

package com.thrj;

import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;

/**
 * @Description: TODO
 * @Author: wanping
 * @Date: 12/25/22
 **/
public class CallExternalSystemDelegate implements JavaDelegate {
    @Override
    public void execute(DelegateExecution execution) {
        System.out.println("审批用户通过" + execution.getVariable("employee"));
    }
}

当执行到达服务任务时,会初始化并调用BPMN 2.0 XML中所引用的类

查询历史数据

如果希望显示流程实例已经执行的时间,就可以从ProcessEngine获取HistoryService,并创建历史活动(historical activities)的查询。在下面的代码片段中,可以看到我们添加了一些额外的过滤条件:

  • 只选择一个特定流程实例的活动

  • 只选择已完成的活

     //查询历史数据
            HistoryService historyService = processEngine.getHistoryService();
            List<HistoricActivityInstance> histories = historyService.createHistoricActivityInstanceQuery().finished().orderByHistoricActivityInstanceStartTime().desc().list();
            for (HistoricActivityInstance history : histories) {
                System.out.println(history.getActivityId() + " == id, 耗时" + history.getDurationInMillis() + "ms");
            }
    

    结果如下:

定义流程的名称 : 请假流程
姓名
wp
请假时长
24
请假原因
吃饭
你有1个任务
1)Approve or reject request:编号为1
你想处理哪个任务?
1
wp想请假24天,请假理由是:吃饭,是否同意请假?(y/n)
y
审批用户通过wp
你有2个任务,编号为1
1
你好wp先生,您的假期一共有24天,请享用
审批人employee,审批时间Sun Dec 25 22:10:11 CST 2022,审批意见wp休假中
审批人manager,审批时间Sun Dec 25 22:10:07 CST 2022,审批意见同意休假
approveEnd == id, 耗时4ms
_flow_holidayApprovedTask__approveEnd == id, 耗时0ms
_flow_externalSystemCall__holidayApprovedTask == id, 耗时0ms
externalSystemCall == id, 耗时2ms
_flow_decision__externalSystemCall == id, 耗时0ms
decision == id, 耗时2ms
_flow_approveTask__decision == id, 耗时0ms
_flow_startEvent__approveTask == id, 耗时0ms
approveTask == id, 耗时5161ms
startEvent == id, 耗时3ms
holidayApprovedTask == id, 耗时50683ms
_flow_externalSystemCall__holidayApprovedTask == id, 耗时0ms
externalSystemCall == id, 耗时1ms
_flow_decision__externalSystemCall == id, 耗时0ms
decision == id, 耗时1ms
_flow_approveTask__decision == id, 耗时0ms
_flow_startEvent__approveTask == id, 耗时0ms
approveTask == id, 耗时8722ms
startEvent == id, 耗时3ms
_flow_externalSystemCall__holidayApprovedTask == id, 耗时0ms
externalSystemCall == id, 耗时2ms
_flow_decision__externalSystemCall == id, 耗时0ms
decision == id, 耗时2ms
_flow_approveTask__decision == id, 耗时0ms
_flow_startEvent__approveTask == id, 耗时0ms
approveTask == id, 耗时62065ms
startEvent == id, 耗时3ms

参考博文链接:https://blog.csdn.net/Syals/article/details/127318933

posted @ 2022-12-25 22:24  小学程序员  阅读(579)  评论(0编辑  收藏  举报