flowable流程引擎初体验,完成一个请假流程

flowable是一个用Java写的轻量级商业流程引擎,用它可以部署BPMN2.0(在工业界被广泛接受的XML标准)流程定义, 并且可以创建流程实例,驱动节点流转,存储相关的历史数据等等。可能更多人先是听说过activiti, 但是flowable实际上是activiti的主要成员在activiti上fork一个新的分支,添加了许多新的特性,也更加稳定实用,感兴趣的可以去了解一下这段历史。本文主要基于flowable官方文档演示如何去用flowable来走通如下的一个请假流程。

1. 创建项目

首先我们创建一个maven项目,引入以下依赖:

  • 采用最新版本的flowable流程引擎,
  • flowable存储数据默认采用h2内存数据库,但我这里还是用熟悉的mysql
  • flowable内部采用SLF4J作为其日志框架
  • 在这里我们用log4j日志实现
    <dependency>
      <groupId>org.flowable</groupId>
      <artifactId>flowable-engine</artifactId>
      <version>6.3.1</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.40</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>

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

2. 创建流程引擎

首先,我们需要做的是初始化一个流程引擎(ProcessEngine)实例。它是一个线程安全的对象在一个应用中也只需要初始化一次, 而它又由流程引擎配置(ProceeEngineConfiguration)创建,在ProcessEngineconfiguration中可以配置和调整省流程引擎。通常情况下ProcessEngineConfiguration是通过配置文件来配置的,但是也可以在程序中创建。当然,最少情况下的流程引擎配置也需要一个JDBC连接到数据库。

public class HolidayRequest {
    public static void main(String[] args) {
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable?useUnicode=true&zeroDateTimeBehavior=convertToNull")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);

        ProcessEngine processEngine = cfg.buildProcessEngine();
    }

}

在上面的代码中,创建了一个独立的配置对象,这里的‘独立’是指引擎只由它自己创建和使用(如果是在Spring环境中,则要用SpringProcessEngineConfiguration来替代).该对象先配置了一个数据库连接(注意先要创建flowable数据库),最后的配置表示数据库不存在的话就要去创建它。运行该段代码后我们就可以看到数据库中新增了许多表格,如下所示:

act_ru_: 表示的是一些流程实例运行(runtime)时的数据
act_hi_: 表示存储的是一些历史(history)数据
act_id_: 表示存储的是一些与用户身份(identity)相关的数据
act_re_: 表示的是一些流程定义(repository)相关的数据
act_ge_: 表示的是一些一般(general)数据
act_evt_log表示事件日志,act_prodef_info表示的是流程定义的一些信息。flowable还有一些其它模块中的表,到后面介绍。

3. 部署流程定义

如开篇那张图所示,我们需要实现的是一个很简单的请假流程, 表示员工的请假首先需要经理审批,经理拒绝则发邮件通知到申请人并结束该该流程,经理同意的话先注册到外部系统,然后通知给申请人进行销假,然后结束。该流程定义可以作为一个模板,每个需要请假的员工可以创建一个流程实例。当员工提供一些信息(如姓名, 请假天数和描述)就可以开始该流程。下面解释一下图中每类元素的意思:

  • 开始事件:图中用细线圆圈来表示,是流程实例的开始点
  • 箭头:表示节点之间的流转指向。
  • 用户任务: 在图中用左上角有人的圆角矩形表示,这些是需要用户来操作的节点。图中有两个,第一个表示需要经理进行审批来同意或拒绝, 第二个表示用户来确认销假。
  • 排它网关: 用叉形符号填充的菱形表示,从该图中出来的箭头往往有多个,但只有一个满足条件,流程会沿着满足条件的方向流转。
  • 自动化任务 : 左上角有齿轮形状的的圆角矩形,表示自动执行的节点。图中上面的表示请假被经理同意后自动注册通知到外部系统,下面的表示请假被经理拒绝后自动发邮件通知给申请人。
  • 结束事件: 图中用粗线圆圈表示,表示流程的结束。图中上面的结束事件表示请假成功结束,下面的表示请假失败结束。


Flowable引擎需要的是一个遵循BPMN2.0标准的XML文档。BPMN2.0标准在工业界被广泛认可, 它对流程中每个节点以及节点与节点之间的流转定义了一套简单清晰易用的标准,并且节点即可以是个人任务也可以是自动化任务等, 因此大家可以用都能理解的方式进行商业化的流程门交流。通常我们可以用一些工具来画出流程图然后生成BPMN2.0文档,但是在这里我们为了熟悉该标准的格式不审采用手动定义的方式。在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="com.hebaohua.workflow.delegate.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="com.hebaohua.workflow.delegate.CallExternalSystemDelegate"/>
        <sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>

        <endEvent id="approveEnd"/>

        <endEvent id="rejectEnd"/>

    </process>

</definitions>

XML文件中最开始几行是一些兼容BPMN2.0标准的约束,每个流程定义都一样。
每一个activity都有一个id属性作为唯一性标识, name属性是为了增加可读性的可选项。
连接activitys的有向箭头称之为sequence flow,有一个起始点和目标点。从排它网关出来的箭头比较特别,这两个都有一个条件标签,表示只有满足里面条件时才会走这条路线。条件表达式${approved}实际上是${approved == true}的缩写,approved被称之为流程变量。流程变量会随着流程实例进行持久化存储并且在整个流程实例的生命周期中都可以被用到, 因此我们必须在用到它之前的某个点对其进行赋值(本例中是当经理的用户任务完成时对approved进行true或false赋值),而不是在流程实例一开始就对其赋值。
现在我们有了流程定义的BPMN2.0 XML文件,下一步就是要把它部署到引擎中。部署流程引擎也就意味着:

  • 流程引擎会把XML文件存储在数据库中,因此可以需要的时候查询到它
  • 流程定义被解析为一个内部可执行的对象模型,所以我们可以以它为模板启动一个流程实例
    部署流程定义到Flowable引擎需要用RepositoryService,可以在ProcessEngine对象中得到它。通过RepositoryService, 指定XML文件位置并调用deploy()方法后即可执行部署这个过程。
RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
  .addClasspathResource("holiday-request.bpmn20.xml")
  .deploy();

通过如下打印的日志可知,在部署的过程中分别在ACT_RE_PROCDEF, ACT_RE_DEPLOYMENT, ACT_GE_BYTEARRAY中插入了相关的记录。


现在我们就可以通过RepositoryService创建的ProcessDefinitionQuery查询到相关的流程定义信息了,如下代码:

ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
  .deploymentId(deployment.getId())
  .singleResult();
System.out.println("Found process definition : " + processDefinition.getName());

4. 启动流程实例

现在我们有了流程定义,以它为模板就可启动一个流程实例了。
在启动流程实之前,我们需要提供一些流程变量作为输入。通常通过表单或REST API让用户来填写这些信息,这里为了方便直接定定义相关变量放在Map中。
下一步就需要通过RuntimeService(同样由processEngine产生)启动流程了。如下代码所示,通过key来启动流程实例,这里的key对应BPMN2.0 XML文件中的process id属性,本例中也就是holidayRequest

RuntimeService runtimeService = processEngine.getRuntimeService();

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employee", "jack");
variables.put("nrOfHolidays", 3);
variables.put("description", "回家看看");
ProcessInstance processInstance =
        runtimeService.startProcessInstanceByKey("holidayRequest", variables);

如下日志,可以看到插入了15条记录,分别是ProcessInstance, Execution, IdentityLInk, Variable, Task相关的运行和历史数据,Execution和Task是运行时的核心表。注意在这一步流程通过启动事件已经到达manager审批结点了,因此会插入两条HistoryActivityInstanceEntity记录。

5. 编写JavaDelegate实现自动化任务

现在虽然任务到了第一个用户任务节点,但该任务节点执行后就会走自动化任务,在XML文件中我们定义对应的类为com.hebaohua.flowable.delegate.CallExternalSystemDelegate, 如果找不到该类程序流程引擎就会报异常,所以我们先看看如何编写这样的一个自动化执行类。
很简单,实现org.flowable.engine.delegate.JavaDelegate接口及其execute方法。通过得到的DelegateExecution对象我们可以得到流程中所有变量及相关信息,因此想要做什么就在这个方法里来实现。如下代码只是一个简单演示:

public class CallExternalSystemDelegate implements JavaDelegate {
    @Override
    public void execute(DelegateExecution execution) {
        System.out.println("Calling the external system for employee "
                + execution.getVariable("employee"));
    }
}

6. 查询并完成任务

在一个实际的应用中,用户通过登录系统可以以界面的方式查看他们的任务列表,并且可以看到流程实例的数据(流程运行状态,流程变量等), 以此来对该任务作相应处理。在本例中,我们通过API调用来模拟这个过程。
现在流程运行到了第一个用户任务, 在上面的XML文件中我们指定的完成该用户的为"managers"这个分组,首先我们可以通过TaskService创建TaskQuery对象来查询managers分组下的任务。如下:

TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("managers").list();
System.out.println("You have " + tasks.size() + " tasks:");
for (int i=0; i<tasks.size(); i++) {
    System.out.println((i+1) + ") " + tasks.get(i).getName());
}

Task task = tasks.get(0);
Map<String, Object> processVariables = taskService.getVariables(task.getId());
System.out.println(processVariables.get("employee") + " wants " +
        processVariables.get("nrOfHolidays") + " of holidays. Do you approve this?");

返回:

You have 1 tasks:
1) Approve or reject request
jack wants 3 of holidays. Do you approve this?

现在managers可以完成这个任务了。同样的在完成任务时,虽然现实中是通过用户界面的表单来完成,在这里通过传递包含approved变量(这个变量很重要,决定后面流程的路线, 这里我们赋值为true)的map来进行。
variables = new HashMap<String, Object>(); variables.put("approved", true); taskService.complete(task.getId(), variables);
通过执行上面的代码managers完成了该任务,细心的你肯定在打印出的日志中发现了如下红框中的内容。对,没错,这就是自动化任务节点中打印出来的。也就是说,现在自动化任务节点已经完成,流程已经到达第二个用户任务了。


同样的,根据XML中的定义, 我们知道第二个用户应该由jack自己(${employee}对就流程变量的值)来完成,通过以下代码即可查出jack需要完成的任务。

List<Task> tasks = taskService.createTaskQuery().taskCandidateOrAssigned("jack").list();

用和managers同样的方法,jack就可以完成任务。至此,流程已经到达结束事件,整个流程也就走完了。

7. 查询历史数据

Flowable为我们存储了丰富的流程实例执行的历史数据。例如我们想看到流程实例每个节点耗时情况,首先通过ProcessEngine获得HistoryService对象并且创建一个查询作如下限制:

  • 查询待定流程实例的节点情况
  • 只查看已完成的节点
    结果会按照结束时间排序,也就是执行顺序:
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");
}

输出:

startEvent took 3 milliseconds
approveTask took 3234 milliseconds
decision took 16 milliseconds
externalSystemCall took 4 milliseconds
holidayApprovedTask took 5888 milliseconds
approveEnd took 2 milliseconds

也就是对应数据库表act_hi_actinst中的如下记录:

全文完。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,898评论 2 89
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,601评论 18 139
  • 周末没能休息,期待已久的户外也未能成行,而是被派到了大博中监考高三一模,早6:30就冒着春寒坐上车出发了。到了博中...
    lxy255028阅读 866评论 1 3
  • 最初决定写下这篇文章时,我内心是有犹豫的。又要不停的码字了。虽然我跟那些写手相比太渺小。但是对我这种沉浸在带娃中不...
    溪边萱草长阅读 502评论 2 1
  • 好久没有过这种感觉了:屋外下着雨,身边没有人,手头没有什么不能拖的事,就开始冷,从心里到身体。 可能是南方下雨...
    在我少年阅读 436评论 0 0