Flowable(一):一个简单流程分析
在创建Flowable的Engine之前,我们需要了解一件事:Flowable的创建,离不开源文件。所谓Flowable的流程图源文件,就是前端绘制流程后,以XML的格式记录的源文件。
XML文件是Flowable的形式定义文件,根据对xml文件的特征属性的抽取,进而将关键的特征数据存储进入DB,以特征数据作为构建Flowable流程引擎的一个模型,这样就达到了Flowable流程引擎到数据模型存储的目的。
至此,我们只需要知道Flowable本身就可以看做是一个’流程’,这个特殊的流程内部定义了处理XML文件的一系列业务流程。虽然听起来比较绕,但是也不难理解,Flowable本身就是一个处理流程对象的模型,我们只需要意识到:既然是模型一定需要数据的输入。
说了半天就是需要意识到这一点,因为很多同学在第一次接触这个东西时候,对于其提供的内部API不知道如何使用,对如何将流程绘制的数据载体传递到FLowable中不是很熟悉。但你只需要掌握这一点,就够了。
既然是模型,那么模型也是在大量的样本数据中建立的,模型的建立也正是为了处理抽象化得海量样本数据。接下来,我们以Flowable本身看做是一个Model的形式,结合一个入门案例,来快速上手Flowable工作流。
一、快速上手
这里你可以直接下载我创建好的maven项目直接学习,或者按照下方的流程重新创建一个maven项目。
如果选择自主创建,那么创建一个Maven项目,选择Java版本是11.0.X
连续创建两个,以第一个作为parent,后续为children:
这么做,可以让你回顾回顾maven项目的父子结构关系以及方便我们更加专心写代码,只需要将所有的依赖管理都放置在父项目中。
在父项目中导入项目依赖:
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<flowable-engine-version>6.7.2</flowable-engine-version>
<h2database-version>1.3.176</h2database-version>
<slf4j-api-version>1.7.21</slf4j-api-version>
<slf4j-log4j12-version>1.7.21</slf4j-log4j12-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-engine</artifactId>
<version>${flowable-engine-version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2database-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j-log4j12-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
Flowable提供了不同的ProcessEngineConfiguration实现,主要依赖于底层的事务管理策略。
然后在子项目中引入依赖:
<dependencies>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-engine</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
至此,项目结构已经搭建完毕,剩下的就是在此基础上做进一步的研究。
如果你不想自己创建,节省时间,可以直接下载我的源码包:点击下载
二、案例分析
现在有一个需求:构建一个流程模拟向自己的一级主管申请假期调休,如何进行?
第一步:创建一个H2BaseUtils工具类
使用Flowable来实现这个操作,我们需要做的就是,首先去初始化一个流程引擎,尽管你可能到这里还没有了解清楚创建流程的一个基本过程是什么样子,但没有关系,我们先这么做,一步一步去学习就可以了。
首先要做的是初始化ProcessEngine流程引擎实例。这是一个线程安全的对象,因此通常只需要在一个应用中初始化一次。
ProcessEngine由ProcessEngineConfiguration实例创建。该实例可以配置与调整流程引擎的设置。 通常使用一个配置XML文件创建
ProcessEngineConfiguration,但是(像在这里做的一样)也可以编程方式创建它。 ProcessEngineConfiguration所需的最小配置,是数据库JDBC连接:
public class H2BaseUtils {
/**
* 返回一个流程引擎实例
* 这里调用的方法,创建的十一二点一个独立(standalone)配置对象
* 这里的'独立'指的是引擎是完全独立创建及使用的,没有集成Spring
* @return 返回一个流程引擎实例
*/
public static ProcessEngine getSingleProcessEngine(){
return initProcessEngine();
}
/**
* 首先要做的是内部初始化ProcessEngine流程引擎实例。
* 这是一个线程安全的对象,因此通常只需要在一个应用中初始化一次。
* ProcessEngine由ProcessEngineConfiguration实例创建。
* 该实例可以配置与调整流程引擎的设置。
* 通常使用一个配置XML文件创建ProcessEngineConfiguration,但是(像在这里做的一样)也可以编程方式创建它。
* 本demo来自官网: 基于JavaAPI的方式创建ProcessEngine流程引擎
* ProcessEngineConfiguration所需的最小配置,是数据库JDBC连接,因此只需要满足JDBC配置即可。
* 注意事项:这里传递了一个内存H2数据库实例的JDBC连接参数,这样的数据库在JVM重启后会消失。
* @return 流程引擎实例
*/
private static ProcessEngine initProcessEngine(){
ProcessEngineConfiguration processEngineConfig = new StandaloneProcessEngineConfiguration();
processEngineConfig.setJdbcUrl("jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1")
.setJdbcUsername("sa")
.setJdbcPassword("")
.setJdbcDriver("org.h2.Driver")
// 设置数据库约束:当不存储当前数据库/表时候,自动创建:true
.setDatabaseSchemaUpdate(AbstractEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
return processEngineConfig.buildProcessEngine();
}
}
第二步:创建一个HolidayRequest.java文件,构建流程
package org.flowable.demo;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.DeploymentBuilder;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
/**
* -------------------------------------------------------------------------------
* Copyright © 2022 Beijing YuXiang Software Technology Co., Ltd. All rights reserved.
* -------------------------------------------------------------------------------
* Module Name: flowable-parent >>> HolidayRequest
* Product:
* Creator: puye(0303)
* Date Created: 2022/8/16
* Description:DemoTestEngine
* 我们要构建的流程是一个非常简单的请假流程。
* Flowable引擎需要流程定义为BPMN 2.0格式,这是一个业界广泛接受的XML标准。
* 在Flowable术语中,我们将其称为一个流程定义(process definition)。
* 一个流程定义可以启动多个流程实例(process instance)。
* 流程定义可以看做是重复执行流程的蓝图。
* 在这个例子中,流程定义定义了请假的各个步骤,而一个流程实例对应某个雇员提出的一个请假申请。
* -------------------------------------------------------------------------------
* Modification History
* DATE Name Description
* -------------------------------------------------------------------------------
* 2022/8/16 puye(0303) Create
* -------------------------------------------------------------------------------
*
* @author <a href="mailto:suncx@flamelephant.com">puye(0303)</a>
*/
@Slf4j
public class HolidayRequest {
public static void main(String[] args) {
// 获得单个流程引擎实例,流程引擎就好比是一个加载流程定义模板的工具
ProcessEngine processEngine = H2BaseUtils.getSingleProcessEngine();
// 拿到流程引擎实例后,我们需要构建一个{流程定义}对象,如何做?
// 先通过引擎获取一个存储库服务,这个存储库服务对象就是专门管理我们流程定义的源文件的
RepositoryService repositoryService = processEngine.getRepositoryService();
// 要想装载我们的流程源文件,需要找到装载的载体,而存储库服务并不会直接处理我们的源文件,要指派他的下级来做
DeploymentBuilder deployment = repositoryService.createDeployment();
// 基于构建者模式,装载完毕后,将数据进行deploy部署即可
Deployment deploy = deployment.addClasspathResource("holiday-request.bpmn20.xml").deploy();
// 部署后,我们查询一下是否部署成功,源文件经过部署后,演变为一个流程定义
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
// 根据部署的时候返回的部署对象拿到部署的id,根据部署id查询流程定义对象
ProcessDefinition processDefinition = processDefinitionQuery.deploymentId(deploy.getId()).singleResult();
// 拿到流程定义后打印一下看看源文件到流程定义的演变是否成功?
log.info("Found process definition name: " + processDefinition.getName());
log.info("Found process definition version: " + processDefinition.getVersion());
log.info("Found process definition id: " + processDefinition.getId());
log.info("Found process definition category: " + processDefinition.getCategory());
log.info("Found process definition engineVersion: " + processDefinition.getEngineVersion());
log.info("Found process definition description: " + processDefinition.getDescription());
log.info("Found process definition resourceName: " + processDefinition.getResourceName());
//---------------------------业务内容----------------------------------
Scanner scanner= new Scanner(System.in);
log.info("Who are you?");
String employee = scanner.nextLine();
log.info("How many holidays do you want to request?");
Integer nrOfHolidays = Integer.valueOf(scanner.nextLine());
log.info("Why do you need them?");
String description = scanner.nextLine();
//--------------------------保存流程中的业务内容,作为流程实例携带的额外参数----------------------------------------------
RuntimeService runtimeService = processEngine.getRuntimeService();
Map<String, Object> variables = new HashMap<>(3);
variables.put("employee", employee);
variables.put("nrOfHolidays", nrOfHolidays);
variables.put("description", description);
//------------------------启动流程实例--------------------------------------------------------
//在流程实例启动后,会创建一个执行(execution),并将其放在启动事件上。
// 从这里开始,这个执行沿着顺序流移动到经理审批的用户任务,并执行用户任务行为。
// 这个行为将在数据库中创建一个任务,该任务可以之后使用查询找到。
// 用户任务是一个等待状态(wait state),引擎会停止执行,返回API调用处。
// 值得注意的是: 这里的流程定义的Id作为了key传入了运行时服务所调用的方法中,用于启动一个指定流程定义的实例
// 启动某一个流程定义的实例还需要注意的问题就是,需要传递这个流程实例启动时候传递的数据。
// 所以这里将上面流程业务中所获得的流程业务数据通过运行时服务进行提交
ProcessInstance processInstance =
runtimeService.startProcessInstanceByKey("holidayRequest", variables);
log.info("You had made a processDefinition , which named :"+ processInstance.getName());
log.info("You had made a processDefinition , which processDefinitionId :"+ processInstance.getProcessDefinitionId());
log.info("You had made a processDefinition , which processDefinitionKey :"+ processInstance.getProcessDefinitionKey());
processInstance.getProcessVariables().forEach((key, value) -> {
log.info("You had made a processDefinition , which variables key is " + key + ", and value is "+value);
});
//----------------------------用户任务阶段--------------------------------------------------------
//在流程实例启动后,会创建一个执行(execution),并将其放在启动事件上。
// 用户提交的数据跟随者流程定义的实例的创建转化成了
// 从这里开始,这个执行沿着顺序流移动到经理审批的用户任务,并执行用户任务行为。
// 这个行为将在数据库中创建一个任务,该任务可以之后使用查询找到。
// 用户任务是一个等待状态(wait state),引擎会停止执行,返回API调用处。
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
log.info("You have " + tasks.size() + " tasks:");
for (int i=0; i<tasks.size(); i++) {
// 这里的task的信息中,前端UI操作生成的xml中,task的id作为了生成的task的definitionKey : approveTask
log.info("(" + (i+1) + ")" + tasks.get(i).getName() + "this task is belong to the processInstance, which of property named ID is " +
tasks.get(i).getProcessDefinitionId() + " and the key is named "+ tasks.get(i).getTaskDefinitionKey());
}
//----------------------------用户任务:审批阶段-----------------------------------
// 用户审批阶段首先第一步就是列出当前用户所需要处理的所有待办task
// 然后根据用户的选择进行筛选出待处理的任务
log.info("Which task would you like to complete?");
int taskIndex = Integer.parseInt(scanner.nextLine());
Task task = tasks.get(taskIndex - 1);
Map<String, Object> processVariables = taskService.getVariables(task.getId());
log.info(processVariables.get("employee") + " wants " +
processVariables.get("nrOfHolidays") + " of holidays to use for "+processVariables.get("description") +". Do you approve this?");
//------------------------------经理审批阶段----------------------
//经理现在就可以完成任务了。
// 在现实中,这通常意味着由用户提交一个表单。
// 表单中的数据作为流程变量传递。
// 在这里,我们在完成任务时传递带有’approved’变量(这个名字很重要,因为之后会在顺序流的条件中使用!)的map来模拟。
boolean approved = "y".equals(scanner.nextLine().toLowerCase(Locale.ROOT));
variables = new HashMap<>();
variables.put("approved", approved);
// complete只是代表着当前task操作人将这个task处理完成
// 但是具体的处理结果也就是具体的实际业务结果需要根据他的决定也就是variables变量决定
taskService.complete(task.getId(), variables);
// 到此为止,一个完整的task就走完了
//现在任务完成,并会在离开排他网关的两条路径中,基于’approved’流程变量选择一条。
//--------------------------------审批结束:业务自动化-----------------------------------
//到此为止,流程走了一个大半,但是还缺一点:
// 我们还没有实现申请通过后执行的自动逻辑。在BPMN 2.0 XML中,这是一个服务任务(service task):
//--------------------------------流程结束,历史记录-------------------------------------
//到此为止我们已经将流程走完了,由用户发起的流程经过排他网关进行抉择后走向了流程的终点
//选择使用Flowable这样的流程引擎的原因之一,是它可以自动存储所有流程实例的审计数据或历史数据。
// 这些数据可以用于创建报告,深入展现组织运行的情况,瓶颈在哪里,等等。
//例如,如果希望显示流程实例已经执行的时间,就可以从ProcessEngine获取HistoryService,
// 并创建历史活动(historical activities)的查询。在下面的代码片段中,可以看到我们添加了一些额外的过滤条件:
//只选择一个特定流程实例的活动,只选择已完成的活动,结果按照结束时间排序,代表其执行顺序。
HistoryService historyService = processEngine.getHistoryService();
List<HistoricActivityInstance> activities =
historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstance.getId())
.finished()
.orderByHistoricActivityInstanceEndTime().asc()
.list();
for (HistoricActivityInstance activity : activities) {
System.out.println(activity.getActivityId() + " took "
+ activity.getDurationInMillis() + " milliseconds");
}
}
}
第三步:增加一个log配置文件(文件名: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)。流程定义可以看做是重复执行流程的蓝图。 在这个例子中,流程定义定义了请假的各个步骤,而一个流程实例对应某个雇员提出的一个请假申请。
BPMN 2.0存储为XML,并包含可视化的部分:使用标准方式定义了每个步骤类型(人工任务,自动服务调用,等等)如何呈现,以及如何互相连接。这样BPMN 2.0标准使技术人员与业务人员能用双方都能理解的方式交流业务流程。
我们要使用的流程定义为:
这个流程应该已经十分自我解释了。但为了明确起见,说明一下几个要点:
- 我们假定启动流程需要提供一些信息,例如雇员名字、请假时长以及说明。当然,这些可以单独建模为流程中的第一步。 但是如果将它们作为流程的“输入信息”,就能保证只有在实际请求时才会建立一个流程实例。否则(将提交作为流程的第一步),用户可能在提交之前改变主意并取消,但流程实例已经创建了。 在某些场景中,就可能影响重要的指标(例如启动了多少申请,但还未完成),取决于业务目标。
- 左侧的圆圈叫做启动事件(start event)。这是一个流程实例的起点。
- 第一个矩形是一个用户任务(user task)。这是流程中人类用户操作的步骤。在这个例子中,经理需要批准或驳回申请。
- 取决于经理的决定,排他网关(exclusive gateway) (带叉的菱形)会将流程实例路由至批准或驳回路径。
- 如果批准,则需要将申请注册至某个外部系统,并跟着另一个用户任务,将经理的决定通知给申请人。当然也可以改为发送邮件。
- 如果驳回,则为雇员发送一封邮件通知他。
一般来说,这样的流程定义使用可视化建模工具建立,如Flowable Designer(Eclipse)或Flowable Web Modeler(Web应用)。
但在这里我们直接撰写XML,以熟悉BPMN 2.0及其概念。
与上面展示的流程图对应的BPMN 2.0 XML在下面显示。请注意这只包含了“流程部分”。如果使用图形化建模工具,实际的XML文件还将包含“可视化部分”,用于描述图形信息,如流程定义中各个元素的坐标(所有的图形化信息包含在XML的BPMNDiagram标签中,作为definitions标签的子元素)。
第四步:创建holiday-request.bpmn20.xml文件
将下面的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" flowable:candidateGroups="managers"/>
<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" flowable:assignee="${employee}"/>
<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>
到此为止,一个请假的流程已经走完。(所有的流程详细分析,都需要结合代码讲解,所有我都放在了注释上面)。