BPMN 2.0学习笔记 (基于Activiti实践学习笔记)

推荐学习网站:http://www.mossle.com/docs/activiti/index.html#bpmn20

摘一句: 生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消失,最后变得像挨了锤的牛一样。 --王小波

BPMN 2.0介绍

BPMN是BPM及workflow的建模语言标准之一

  • 1、什么是BPMN
    首先BPMN规范是由标准组织BPMI发布的.BPMN 1.0规范发布于2004年5月。此规范展示了BPMI组织两年多的努力成果。BPMN的主要目标就是要提供被所有业务用户理解的一套标记语言,包括业务分析者、软件开发者以及业务管理者与监察者。BPMN还将支持生成可执行的BPEL4WS语言。所以,BPMN在业务流程设计与流程实现之间搭建了一条标准化的桥梁。
    BPMN定义了业务流程图,其基于流程图技术,同时为创建业务流程操作的图形化模型进行了裁减。业务流程的模型就是图形化对象的网图,包括活动(也可以说工作)和定义操作顺序的流控制。
  • 2、BPMN基础
    业务流程图由一系列的图形化元素组成。这些元素简化了模型的开发,且业务分析者看上去非常熟悉。这些元素每个都有各自的特性,且与大多数的建模器类似。比如,活动是矩形,条件是菱形。应该强调的是:开发BPMN的动力就是为了在创建业务流程模型时提供一个简单的机制,同时又能够处理来自业务流程的复杂性。要处理这两个矛盾的需求的方法就是将标记的图形化方面组织分类为特定的类别。这里提供标记类别中的一小部分,以便业务流程图的读者可以简单地识别出元素的基本类型从而理解图形。以下是四种基本的类型:
    • c1)流对象(Flow)
    • 2)连接对象(Connection)
    • 3)泳道(Swimlane)
    • 4)人工信息(Artifact)
      复杂的流程处理

事件(Event)

事件用来表明流程的生命周期中发生了什么事。 事件总是画成一个圆圈。
在BPMN 2.0中, 事件有两大分类:捕获(catching)触发(throwing) 事件

  • 捕获(Catching):当流程执行到事件, 它会等待被触发。触发的类型是由内部图表或XML中的类型声明来决定的。 捕获事件与触发事件在显示方面是根据内部图表是否被填充来区分的(白色的)

  • 触发(Throwing):当流程执行到事件, 会触发一个事件。触发的类型是由内部图表或XML中的类型声明来决定的。 触发事件与捕获事件在显示方面是根据内部图表是否被填充来区分的(被填充为黑色)

事件定义

事件定义决定了事件的语义。如果没有事件定义,这个事件就不做什么特别的事情。 没有设置事件定义的开始事件不会在启动流程时做任何事情。
如果给开始事件添加了一个事件定义 (比如定时器事件定义)我们就声明了开始流程的事件 "类型 " (这时定时器事件监听器会在某个时间被触发)。

  • 定时器事件定义
  • 错误事件定义
  • 信号事件定义
  • 消息事件定义

  • 定时器事件定义

定时器事件是根据指定的时间触发的事件。可以用于 开始事件, 中间事件 或 边界事件

定时器定义必须下面介绍的一个元素:

  • timeDate。使用 ISO 8601 格式指定一个确定的时间,触发事件的时间
<timerEventDefinition>
    <timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
  • timeDuration。指定定时器之前要等待多长时间, timeDuration可以设置为timerEventDefinition的子元素。 使用ISO 8601规定的格式 (由BPMN 2.0规定)。示例(等待10天)。
<timerEventDefinition>
    <timeDuration>P10D</timeDuration>
</timerEventDefinition>
  • timeCycle。指定重复执行的间隔, 可以用来定期启动流程实例,或为超时时间发送多个提醒。 timeCycle元素可以使用两种格式。第一种是 ISO 8601 标准的格式。示例(重复3次,每次间隔10小时):
<timerEventDefinition>
    <timeCycle>R3/PT10H</timeCycle>
</timerEventDefinition>
  • 你可以使用cron表达式指定timeCycle,下面的例子是从整点开始,每5分钟执行一次:
0 0/5 * * * ?

第一个数字表示秒,而不是像通常Unix cron中那样表示分钟。

重复的时间周期能更好的处理相对时间,它可以计算一些特定的时间点 (比如,用户任务的开始时间),而cron表达式可以处理绝对时间 - 这对定时启动事件特别有用。

你可以在定时器事件定义中使用表达式,这样你就可以通过流程变量来影响那个定时器定义。 流程定义必须包含ISO 8601(或cron)格式的字符串,以匹配对应的时间类型。

 <boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
     <timerEventDefinition>
      <timeDuration>${duration}</timeDuration>
    </timerEventDefinition>
  </boundaryEvent>

注意: 只有启用job执行器之后,定时器才会被触发。 (activiti.cfg.xml中的jobExecutorActivate需要设置为true, 不过,默认job执行器是关闭的)。


  • 错误事件定义

错误事件是由指定错误触发的。
重要提醒:BPMN错误与Java异常完全不一样。 实际上,他俩一点儿共同点都没有。BPMN错误事件是为了对业务异常建模。Java异常是要 用特定方式处理。
错误事件定义会引用一个error元素。下面是一个error元素的例子,引用了一个错误声明:

<endEvent id="myErrorEndEvent">
  <errorEventDefinition errorRef="myError" />
</endEvent>
         

引用相同error元素的错误事件处理器会捕获这个错误。


  • 信号事件定义

信号事件会引用一个已命名的信号。信号全局范围的事件(广播语义)。 会发送给所有激活的处理器。
信号事件定义使用signalEventDefinition元素。 signalRef属性会引用definitions根节点里定义的signal子元素。 下面是一个流程的实例,其中会抛出一个信号,并被中间事件捕获。

<definitions... >
        <!-- declaration of the signal -->
        <signal id="alertSignal" name="alert" />

        <process id="catchSignal">
                <intermediateThrowEvent id="throwSignalEvent" name="Alert">
                        <!-- signal event definition -->
                        <signalEventDefinition signalRef="alertSignal" />
                </intermediateThrowEvent>
                ...
                <intermediateCatchEvent id="catchSignalEvent" name="On Alert">
                        <!-- signal event definition -->
                        <signalEventDefinition signalRef="alertSignal" />
                </intermediateCatchEvent>
                ...
        </process>
</definitions>

signalEventDefinition引用相同的signal元素。

  • 触发信号事件

既可以通过bpmn节点由流程实例触发一个信号,也可以通过API触发。 下面的org.activiti.engine.RuntimeService中的方法 可以用来手工触发一个信号。

RuntimeService.signalEventReceived(String signalName);
RuntimeService.signalEventReceived(String signalName, String executionId);

signalEventReceived(String signalName);和 signalEventReceived(String signalName, String executionId);之间的区别是 第一个方法会把信号发送给全局所有订阅的处理器(广播语义), 第二个方法只把信息发送给指定的执行。

  • 捕获信号事件

信号事件可以被中间捕获信号事件或边界信息事件捕获。
查询信号事件的订阅,可以查询所有订阅了特定信号事件的执行:

List<Execution> executions = runtimeService.createExecutionQuery()
      .signalEventSubscriptionName("alert")
      .list();

我们可以使用signalEventReceived(String signalName, String executionId)方法把信号发送给这些执行。

  • 信号事件范围

可以查询所有订阅了特定信号事件的执行:

默认,信号会在流程引擎范围内进行广播。就是说, 你可以在一个流程实例中抛出一个信号事件,其他不同流程定义的流程实例 都可以监听到这个事件。

然而,有时只希望在同一个流程实例中响应这个信号事件。 比如一个场景是,流程实例中的同步机制,如果两个或更多活动是互斥的。

如果想要限制信号事件的范围,可以使用信号事件定义的scope 属性 (不是BPMN2.0的标准属性):

<signal id="alertSignal" name="alert" activiti:scope"processInstance"/>
  • 信号事件实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rabiH15z-1599562695866)(imgclip_9.png "imgclip_9.png")]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L0uo8nS8-1599562695868)(imgclip_10.png "imgclip_10.png")]
在这里插入图片描述

信号事件不会执行任何与特定流程实例的联系。 如果你只想把一个信息发给指定的流程实例,需要手工关联,再使用 signalEventReceived(String signalName, String executionId)和对应的 查询机制。


  • 消息事件定义

消息事件会引用一个命名的消息。每个消息都有名称和内容。和信号不同, 消息事件总会直接发送个一个接受者。
消息事件定义使用messageEventDefinition元素。 messageRef属性引用了definitions根节点下的 一个message子元素。下面是一个使用两个消息事件的流程例子, 开始事件和中间捕获事件分别声明和引用了两个消息事件。

<definitions id="definitions"
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:activiti="http://activiti.org/bpmn"
  targetNamespace="Examples"
  xmlns:tns="Examples">

  <message id="newInvoice" name="newInvoiceMessage" />
  <message id="payment" name="paymentMessage" />

  <process id="invoiceProcess">

    <startEvent id="messageStart" >
        <messageEventDefinition messageRef="newInvoice" />
    </startEvent>
    ...
    <intermediateCatchEvent id="paymentEvt" >
        <messageEventDefinition messageRef="payment" />
    </intermediateCatchEvent>
    ...
  </process>

</definitions>
  • 触发消息事件

作为一个嵌入式的流程引擎,activiti不能真正接收一个消息。这些环境相关,与平台相关的活动 比如连接到JMS(Java消息服务)队列或主题或执行WebService或REST请求。 这个消息的接收是你要在应用或架构的一层实现的,流程引擎则内嵌其中。

在你的应用接收一个消息之后,你必须决定如何处理它。 如果消息应该触发启动一个新流程实例, 在下面的RuntimeService的两个方法中选择一个执行:

ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey, Map<String, Object> processVariables);            

这些方法允许使用对应的消息系统流程实例。

如果消息需要被运行中的流程实例处理,首先要根据消息找到对应的流程实例 (参考下一节)然后触发这个等待中的流程。 RuntimeService提供了如下方法可以基于消息事件的订阅来触发流程继续执行:

void messageEventReceived(String messageName, String executionId);
void messageEventReceived(String messageName, String executionId, HashMap<String, Object> processVariables);    
  • 查询消息事件的订阅

Activiti支持消息开始事件和中间消息事件。

  • 消息开始事件的情况,消息事件订阅分配给一个特定的process definition。这个消息订阅可以使用ProcessDefinitionQuery查询到:
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
      .messageEventSubscription("newCallCenterBooking")
      .singleResult();

因为同时只能有一个流程定义关联到消息的订阅点,查询总是返回0或一个结果。 如果流程定义更新了, 那么只有最新版本的流程定义会订阅到消息事件上。

  • 中间捕获消息事件的情况,消息事件订阅会分配给特定的执行。 这个消息事件订阅可以使用ExecutionQuery查询到:
Execution execution = runtimeService.createExecutionQuery()
      .messageEventSubscriptionName("paymentReceived")
      .variableValueEquals("orderId", message.getOrderId())
      .singleResult();

这个查询可以调用对应的查询,通常是流程相关的信息 (这里,最多只能有一个流程实例对应着orderId)。

  • 消息事件实例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5p634hpF-1599562695873)(imgclip.png "imgclip.png")]


  • 开始事件

开始事件用来指明流程在哪里开始。开始事件的类型(流程在接收事件时启动, 还是在指定时间启动,等等),定义了流程如何启动, 这通过事件中不同的小图表来展示。 在XML中,这些类型是通过声明不同的子元素来区分的。

开始事件都是捕获事件: 最终这些事件都是(一直)等待着,直到对应的触发时机出现。

在开始事件中,可以设置下面的activiti特定属性:
+ initiator:当流程启动时,把当前登录的用户保存到哪个变量名中。

<startEvent id="request" activiti:initiator="initiator" />

登录的用户必须使用IdentityService.setAuthenticatedUserId(String)方法设置(IdentityService管理用户和组的服务), 并像这样包含在try-finally代码中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-32rxBGHP-1599562695871)(imgclip_1.png "imgclip_1.png")]

try {
    
    
  identityService.setAuthenticatedUserId("bono");
  runtimeService.startProcessInstanceByKey("someProcessKey");
} finally {
    
    
  identityService.setAuthenticatedUserId(null);
}
  • 空开始事件

startProcessInstanceByXXX方法。

ProcessInstance processInstance = runtimeService.startProcessInstanceByXXX();

注意: 子流程都有一个空开始事件。
+ 空开始事件显示成一个圆圈,没有内部图表(没有触发类型)
在这里插入图片描述

  • XML结构,空开始事件的XML结构是普通的开始事件定义,没有任何子元素 (其他开始事件类型都有一个子元素来声明自己的类型)
<startEvent id="start" name="my start event" />
  • 空开始事件的自定义扩展,formKey:引用用户在启动新流程实例时需要填写的表单模板
<startEvent id="request" activiti:formKey="org/activiti/examples/taskforms/request.form" />
  • 定时开始事件

定时开始事件用来在指定的时间创建流程实例。 它可以同时用于只启动一次的流程 和应该在特定时间间隔启动多次的流程。

注意:子流程不能使用定时开始事件

注意:定时开始事件在流程发布后就会开始计算时间。 不需要调用startProcessInstanceByXXX,调用启动流程的方法, 但是那会导致调用startProcessInstanceByXXX时启动过多的流程。

注意:当包含定时开始事件的新版本流程部署时, 对应的上一个定时器就会被删除。这是因为通常不希望自动启动旧版本流程的流程实例。
在这里插入图片描述

  • XML内容
    定时开始事件的XML内容是普通开始事件的声明,包含一个定时定义子元素。
    流程会启动4次,每次间隔5分钟,从2011年3月11日,12:13开始计时。
 <startEvent id="theStart">
            <timerEventDefinition>
                <timeCycle>R4/2011-03-11T12:13/PT5M</timeCycle>
            </timerEventDefinition>
        </startEvent>

流程会根据选中的时间启动一次。

 <startEvent id="theStart">
            <timerEventDefinition>
                <timeDate>2011-03-11T12:13:14</timeDate>
            </timerEventDefinition>
        </startEvent>
  • 消息开始事件

消息开始事件可以用其使用一个命名的消息来启动流程实例。 这样可以帮助我们使用消息名称来选择正确的开始事件。

在发布包含一个或多个消息开始事件的流程定义时,需要考虑下面的条件:

  • 消息开始事件的名称在给定流程定义中不能重复。流程定义不能包含多个名称相同的消息开始事件。 如果两个或以上消息开始事件应用了相同的事件,或两个或以上消息事件引用的消息名称相同,activiti会在发布流程定义时抛出异常。

  • 消息开始事件的名称在所有已发布的流程定义中不能重复。 如果一个或多个消息开始事件引用了相同名称的消息,而这个消息开始事件已经部署到不同的流程定义中, activiti就会在发布时抛出一个异常。

  • 流程版本:在发布新版本的流程定义时,之前订阅的消息订阅会被取消。 如果新版本中没有消息事件也会这样处理。

  • 启动流程实例,消息开始事件可以使用 下列RuntimeService中的方法来触发

ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey, Map<String, Object< processVariables);

这里的messageName是messageEventDefinition的messageRef属性引用的message元素的name属性。 启动流程实例时考虑因素:

  • 消息开始事件只支持顶级流程。消息开始事件不支持内嵌子流程。

  • 如果流程定义有多个消息开始事件,runtimeService.startProcessInstanceByMessage(…) 会选择对应的开始事件。

  • 如果流程定义有多个消息开始事件和一个空开始事件。 runtimeService.startProcessInstanceByKey(…)和 runtimeService.startProcessInstanceById(…)会使用空开始事件启动流程实例。

  • 如果流程定义有多个消息开始事件,而且没有空开始事件, runtimeService.startProcessInstanceByKey(…)和 runtimeService.startProcessInstanceById(…)会抛出异常。

  • 如果流程定义只有一个消息开始事件, runtimeService.startProcessInstanceByKey(…)和 runtimeService.startProcessInstanceById(…)会使用这个消息开始事件启动流程实例。

  • 如果流程被调用环节(callActivity)启动,消息开始事件只支持如下情况:

    • 在消息开始事件以外,还有一个单独的空开始事件
    • 流程只有一个消息开始事件,没有空开始事件。
      消息开始事件是一个圆圈,中间是一个消息事件图标。图标是白色未填充的,来表示捕获(接收)行为。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ZKaC776-1599562695874)(imgclip_3.png "imgclip_3.png")]

  • 信号开始事件

signal开始事件,可以用来通过一个已命名的信号(signal)来启动一个流程实例。 信号可以在流程实例内部使用“中间信号抛出事务”触发, 也可以通过API(runtimService.signalEventReceivedXXX 方法)触发。两种情况下,所有流程实例中拥有相同名称的signalStartEvent都会启动。

注意,在两种情况下,都可以选择同步或异步的方式启动流程实例。

必须向API传入signalName, 这是signal元素的name属性值, 它会被signalEventDefinition的signalRef属性引用。

信号开始事件显示为一个中间包含信号事件图标的圆圈。标记是无填充的,表示捕获(接收)行为。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EabnpENs-1599562695875)(imgclip_4.png "imgclip_4.png")]

+ XML格式;signalStartEvent的XML格式是标准的startEvent声明,其中包含一个signalEventDefinition子元素:
 <signal id="theSignal" name="The Signal" />
    <process id="processWithSignalStart1">
        <startEvent id="theStart">
          <signalEventDefinition id="theSignalEventDefinition" signalRef="theSignal"  />
        </startEvent>
        <sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
        <userTask id="theTask" name="Task in process A" />
        <sequenceFlow id="flow2" sourceRef="theTask" targetRef="theEnd" />
        <endEvent id="theEnd" />
    </process>
  • 错误开始事件

错误开始事件可以用来触发一个事件子流程。 错误开始事件不能用来启动流程实例。

错误开始事件都是中断事件。

错误开始事件是一个圆圈,包含一个错误事件标记。标记是白色未填充的,来表示捕获(接收)行为。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MzcZWQF3-1599562695875)(imgclip_5.png "imgclip_5.png")]

+ XML内容
<startEvent id="messageStart" >
        <errorEventDefinition errorRef="someError" />
</startEvent>

  • 结束事件

结束事件表示(子)流程(分支)的结束。 结束事件都是触发事件。 这是说当流程达到结束事件,会触发一个结果。 结果的类型是通过事件的内部黑色图标表示的。 在XML内容中,是通过包含的子元素声明的。

  • 空结束事件

空结束事件意味着到达事件时不会指定抛出的结果。 这样,引擎会直接结束当前执行的分支,不会做其他事情。
空结束事件是一个粗边圆圈,内部没有小图表(无结果类型)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xLWDyryW-1599562695876)(imgclip_6.png "imgclip_6.png")]

  • XML内容:空结束事件的XML内容是普通结束事件定义,不包含子元素 (其他结束事件类型都会包含声明类型的子元素)。
<endEvent id="end" name="my end event" />
  • 错误结束事件

当流程执行到错误结束事件, 流程的当前分支就会结束,并抛出一个错误。 这个错误可以被对应的中间边界错误事件捕获。 如果找不到匹配的边界错误事件,就会抛出一个异常。

错误结束事件是一个标准的结束事件(粗边圆圈),内部有错误图标。 错误图表是全黑的,表示触发语法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YYbYgtIX-1599562695876)(imgclip_8.png "imgclip_8.png")]

错误结束事件的内容是一个错误事件, 子元素为errorEventDefinition。

<endEvent id="myErrorEndEvent">
  <errorEventDefinition errorRef="myError" />
</endEvent>

errorRef属性引用定义在流程外部的error元素:

<error id="myError" errorCode="123" />
...
<process id="myProcess">
...

error的errorCode用来查找 匹配的捕获边界错误事件。 如果errorRef与任何error都不匹配, 就会使用errorRef来作为errorCode的缩写。 这是activiti特定的缩写。

<error id="myError" errorCode="error123" />
...
<process id="myProcess">
...
  <endEvent id="myErrorEndEvent">
    <errorEventDefinition errorRef="myError" />
  </endEvent>

等同于

<endEvent id="myErrorEndEvent">
  <errorEventDefinition errorRef="error123" />
</endEvent>
  • 取消结束事件

取消结束事件只能与BPMN事务子流程结合使用。 当到达取消结束事件时,会抛出取消事件,它必须被取消边界事件捕获。 取消边界事件会取消事务,并触发补偿机制。

取消结束事件显示为标准的结束事件(粗边圆圈),包含一个取消图标。 取消图标是全黑的,表示触发语法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v3XYp2GP-1599562695877)(imgclip_7.png "imgclip_7.png")]

  • XML内容:取消结束事件内容是一个结束事件, 包含cancelEventDefinition子元素。
<endEvent id="myCancelEndEvent">
  <cancelEventDefinition />
</endEvent>

还有些其他的Event,暂时用不到,感兴趣可以看手册学习。路径在最前。


顺序流

顺序流是连接两个流程节点的连线。 流程执行完一个节点后,会沿着节点的所有外出顺序流继续执行。 就是说,BPMN 2.0默认的行为就是并发的: 两个外出顺序流会创造两个单独的,并发流程分支。

  • 图形标记:顺序流显示为从起点到终点的箭头。 箭头总是指向终点。

  • 在这里插入图片描述

  • XML内容:顺序流需要流程范围内唯一的id, 以及对起点与 终点元素的引用。

<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />

条件顺序流

描述可以为顺序流定义一个条件。离开一个BPMN 2.0节点时, 默认会计算外出顺序流的条件。 如果条件结果为true, 就会选择外出顺序流继续执行。当多条顺序流被选中时, 就会创建多条分支, 流程会继续以并行方式继续执行。
注意:上面的讨论仅涉及BPMN 2.0节点(和事件), 不包括网关。网关会用特定的方式处理顺序流中的条件, 这与网关类型相关。
图形标记:条件顺序流显示为一个正常的顺序流,不过在起点有一个菱形。 条件表达式也会显示在顺序流上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yea5XIIr-1599650636514)(imgclip_1.png “imgclip_1.png”)]
XML内容:条件顺序流定义为一个正常的顺序流, 包含conditionExpression子元素。 注意目前只支持tFormalExpressions, 如果没有设置xsi:type="", 就会默认值支持目前支持的表达式类型。

<sequenceFlow id="flow" sourceRef="theStart" targetRef="theTask">
  <conditionExpression xsi:type="tFormalExpression">
    <![CDATA[${order.price > 100 && order.price < 250}]]>
  </conditionExpression>
</sequenceFlow>

当前条件表达式只能使用UEL, 可以参考表达式章节获取更多信息。 使用的表达式需要返回boolean值,否则会在解析表达式时抛出异常。

  • 下面的例子引用了流程变量的数据, 通过getter调用JavaBean。
<conditionExpression xsi:type="tFormalExpression">
  <![CDATA[${order.price > 100 && order.price < 250}]]>
</conditionExpression>
  • 这个例子通过调用方法返回一个boolean值。
<conditionExpression xsi:type="tFormalExpression">
  <![CDATA[${order.isStandardOrder()}]]>
</conditionExpression>

在activiti发布包中,包含以下流程实例,使用了值和方法表达式 (参考org.activiti.examples.bpmn.expression)包):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XoDABkbD-1599650636518)(imgclip_2.png “imgclip_2.png”)]

默认顺序流

描述:所有的BPMN 2.0任务和网关都可以设置一个默认顺序流。 只有在节点的其他外出顺序流不能被选中是,才会使用它作为外出顺序流继续执行默认顺序流的条件设置不会生效。
图形标记:默认顺序流显示为了普通顺序流,起点有一个“斜线”标记。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kxZAdDrF-1599650636523)(imgclip_3.png “imgclip_3.png”)]
XML内容:默认顺序流通过对应节点的default属性定义。 下面的XML代码演示了排他网关设置了默认顺序流flow 2。 只有当conditionA和conditionB都返回false时, 才会选择它作为外出连线继续执行。

<exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" default="flow2" />
<sequenceFlow id="flow1" sourceRef="exclusiveGw" targetRef="task1">
  <conditionExpression xsi:type="tFormalExpression">${conditionA}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="task2"/>
<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="task3">
  <conditionExpression xsi:type="tFormalExpression">${conditionB}</conditionExpression>
</sequenceFlow>

对应下面的图形显示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QcnYbhX4-1599650636526)(imgclip_4.png “imgclip_4.png”)]

网关

网关用来控制流程的流向(或像BPMN 2.0里描述的那样,流程的tokens。) 网关可以消费也可以生成token。

网关显示成菱形图形,内部有有一个小图标。 图标表示网关的类型。

排他网关

描述:排他网关(也叫异或(XOR)网关,或更技术性的叫法 基于数据的排他网关),用来在流程中实现决策。 当流程执行到这个网关,所有外出顺序流都会被处理一遍。 其中条件解析为true的顺序流(或者没有设置条件,概念上在顺序流上定义了一个'true') 会被选中,让流程继续运行。

注意这里的外出顺序流 与BPMN 2.0通常的概念是不同的。通常情况下,所有条件结果为true的顺序流 都会被选中,以并行方式执行,但排他网关只会选择一条顺序流执行。 就是说,虽然多个顺序流的条件结果为true, 那么XML中的第一个顺序流(也只有这一条)会被选中,并用来继续运行流程。 如果没有选中任何顺序流,会抛出一个异常。
图形标记:排他网关显示成一个普通网关(比如,菱形图形), 内部是一个“X”图标,表示异或(XOR)语义。 注意,没有内部图标的网关,默认为排他网关。 BPMN 2.0规范不允许在同一个流程定义中同时使用没有X和有X的菱形图形。

XML内容:排他网关的XML内容是很直接的:用一行定义了网关, 条件表达式定义在外出顺序流中。 参考条件顺序流 获得这些表达式的可用配置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wjc2JpHV-1599650636528)(imgclip_5.png “imgclip_5.png”)]
参考下面模型实例:

<exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" />

<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="theTask1">
  <conditionExpression xsi:type="tFormalExpression">${input == 1}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="theTask2">
  <conditionExpression xsi:type="tFormalExpression">${input == 2}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="flow4" sourceRef="exclusiveGw" targetRef="theTask3">
  <conditionExpression xsi:type="tFormalExpression">${input == 3}</conditionExpression>
</sequenceFlow>

并行网关

描述:网关也可以表示流程中的并行情况。最简单的并行网关是 并行网关,它允许将流程分成多条分支,也可以把多条分支 汇聚到一起。 of execution.

并行网关的功能是基于进入和外出的顺序流的:
+ 分支: 并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
+ 汇聚: 所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。
注意,如果同一个并行网关有多个进入和多个外出顺序流, 它就同时具有分支和汇聚功能。 这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。
与其他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tGV2609V-1599650636529)(imgclip_6.png “imgclip_6.png”)]
图形标记:并行网关显示成一个普通网关(菱形)内部是一个“加号”图标, 表示“与(AND)”语义。
XML内容:定义并行网关只需要一行XML:

<parallelGateway id="myParallelGateway" />

实际发生的行为(分支,聚合,同时分支聚合), 要根据并行网关的顺序流来决定。

    <startEvent id="theStart" />
    <sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />

    <parallelGateway id="fork" />
    <sequenceFlow sourceRef="fork" targetRef="receivePayment" />
    <sequenceFlow sourceRef="fork" targetRef="shipOrder" />

    <userTask id="receivePayment" name="Receive Payment" />
    <sequenceFlow sourceRef="receivePayment" targetRef="join" />

    <userTask id="shipOrder" name="Ship Order" />
    <sequenceFlow sourceRef="shipOrder" targetRef="join" />

    <parallelGateway id="join" />
    <sequenceFlow sourceRef="join" targetRef="archiveOrder" />

    <userTask id="archiveOrder" name="Archive Order" />
    <sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />

    <endEvent id="theEnd" />

上面例子中,流程启动之后,会创建两个任务:

ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
                         .processInstanceId(pi.getId())
                         .orderByTaskName()
                         .asc();

List<Task> tasks = query.list();
assertEquals(2, tasks.size());

Task task1 = tasks.get(0);
assertEquals("Receive Payment", task1.getName());
Task task2 = tasks.get(1);
assertEquals("Ship Order", task2.getName());

当两个任务都完成时,第二个并行网关会汇聚两个分支,因为它只有一条外出连线, 不会创建并行分支, 只会创建归档订单任务。
注意并行网关不需要是“平衡的”(比如, 对应并行网关的进入和外出节点数目相等)。 并行网关只是等待所有进入顺序流,并为每个外出顺序流创建并发分支, 不会受到其他流程节点的影响。 所以下面的流程在BPMN 2.0中是合法的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y7Dshxs1-1599650636530)(imgclip_7.png “imgclip_7.png”)]

包含网关

描述:包含网关可以看做是排他网关并行网关的结合体。 和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。 但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。

包含网关的功能是基于进入和外出顺序流的:

  • 分支: 所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。
  • 汇聚: 所有并行分支到达包含网关,会进入等待章台, 直到每个包含流程token的进入顺序流的分支都到达。 这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。 在汇聚之后,流程会穿过包含网关继续执行。
    注意,如果同一个包含节点拥有多个进入和外出顺序流, 它就会同时含有分支和汇聚功能。 这时,网关会先汇聚所有拥有流程token的进入顺序流, 再根据条件判断结果为true的外出顺序流,为它们生成多条并行分支。
    图形标记:并行网关显示为一个普通网关(菱形),内部包含一个圆圈图标。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zl7cZHtX-1599650636531)(imgclip_8.png “imgclip_8.png”)]

XML内容:定义一个包含网关需要一行XML:

<inclusiveGateway id="myInclusiveGateway" />

实际的行为(分支,汇聚或同时分支汇聚), 是由连接在包含网关的顺序流决定的。

    <startEvent id="theStart" />
    <sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />

    <inclusiveGateway id="fork" />
    <sequenceFlow sourceRef="fork" targetRef="receivePayment" >
    <conditionExpression xsi:type="tFormalExpression">${paymentReceived == false}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow sourceRef="fork" targetRef="shipOrder" >
    <conditionExpression xsi:type="tFormalExpression">${shipOrder == true}</conditionExpression>
    </sequenceFlow>

    <userTask id="receivePayment" name="Receive Payment" />
    <sequenceFlow sourceRef="receivePayment" targetRef="join" />

    <userTask id="shipOrder" name="Ship Order" />
    <sequenceFlow sourceRef="shipOrder" targetRef="join" />

    <inclusiveGateway id="join" />
    <sequenceFlow sourceRef="join" targetRef="archiveOrder" />

    <userTask id="archiveOrder" name="Archive Order" />
    <sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />

    <endEvent id="theEnd" />

在上面的例子中,流程开始之后,如果流程变量为paymentReceived == false和shipOrder == true, 就会创建两个任务。如果,只有一个流程变量为true,就会只创建一个任务。如果没有条件为true,就会抛出一个异常。 如果想避免异常,可以定义一个默认顺序流。下面的例子中,会创建一个任务,发货任务:

HashMap<String, Object> variableMap = new HashMap<String, Object>();
          variableMap.put("receivedPayment", true);
          variableMap.put("shipOrder", true);
          ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
                         .processInstanceId(pi.getId())
                         .orderByTaskName()
                         .asc();

List<Task> tasks = query.list();
assertEquals(1, tasks.size());

Task task = tasks.get(0);
assertEquals("Ship Order", task.getName());

当任务完成后,第二个包含网关会汇聚两个分支, 因为只有一个外出顺序流,所以不会创建并行分支, 只有归档订单任务会被激活。

注意包含网关不需要“平衡”(比如, 对应包含网关的进入和外出数目需要相等)。 包含网关会等待所有进入顺序流完成, 并为每个外出顺序流创建并行分支, 不会受到流程中其他元素的影响。

基于事件网关

描述:基于事件网关允许根据事件判断流向。网关的每个外出顺序流都要连接到一个中间捕获事件。 当流程到达一个基于事件网关,网关会进入等待状态:会暂停执行。 与此同时,会为每个外出顺序流创建相对的事件订阅。

注意基于事件网关的外出顺序流和普通顺序流不同。这些顺序流不会真的"执行"。 相反,它们让流程引擎去决定执行到基于事件网关的流程需要订阅哪些事件。 要考虑以下条件:
基于事件网关必须有两条或以上外出顺序流。
基于事件网关后,只能使用intermediateCatchEvent类型。 (activiti不支持基于事件网关后连接ReceiveTask。)
连接到基于事件网关的intermediateCatchEvent只能有一条进入顺序流。

图形标记:基于事件网关和其他BPMN网关一样显示成一个菱形, 内部包含指定图标。
XML内容:用来定义基于事件网关的XML元素是eventBasedGateway。

实例:下面的流程是一个使用基于事件网关的例子。当流程执行到基于事件网关时, 流程会暂停执行。与此同时,流程实例会订阅警告信号事件,并创建一个10分钟后触发的定时器。 这会产生流程引擎为一个信号事件等待10分钟的效果。如果10分钟内发出信号,定时器就会取消,流程会沿着信号执行。 如果信号没有出现,流程会沿着定时器的方向前进,信号订阅会被取消。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r2CbjRSy-1599650636531)(imgclip_9.png “imgclip_9.png”)]

任务

用户任务

描述:用户任务用来设置必须由人员完成的工作。 当流程执行到用户任务,会创建一个新任务, 并把这个新任务加入到分配人或群组的任务列表中。
图形标记:用户任务显示成一个普通任务(圆角矩形),左上角有一个小用户图标。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6yXpGwTA-1599650636532)(imgclip_10.png “imgclip_10.png”)]
XML内容:XML中的用户任务定义如下。id属性是必须的。 name属性是可选的。

<userTask id="theTask" name="Important task" />

用户任务也可以设置描述。实际上所有BPMN 2.0元素都可以设置描述。 添加documentation元素可以定义描述。

<userTask id="theTask" name="Schedule meeting" >
  <documentation>
          Schedule an engineering meeting for next week with the new hire.
  </documentation>
xml

描述文本可以通过标准的java方法来获得:
```java

task.getDescription()
  • 持续时间:任务可以用一个字段来描述任务的持续时间。可以使用查询API来对持续时间进行搜索, 根据在时间之前或之后进行搜索。

我们提供了一个节点扩展,在任务定义中设置一个表达式, 这样在任务创建时就可以为它设置初始持续时间。表达式应该是java.util.Date, java.util.String (ISO8601格式),ISO8601 持续时间 (比如PT50M)或null。 例如:你可以在流程中使用上述格式输入日期,或在前一个服务任务中计算一个时间。 这里使用了持续时间,持续时间会基于当前时间进行计算,再通过给定的时间段累加。 比如,使用"PT30M"作为持续时间,任务就会从现在开始持续30分钟。

<userTask id="theTask" name="Important task" activiti:dueDate="${dateVariable}"/>

任务的持续时间也可以通过TaskService修改, 或在TaskListener中通过传入的DelegateTask参数修改。

  • 用户分配:用户任务可以直接分配给一个用户。 这可以通过humanPerformer元素定义。 humanPerformer定义需要一个 resourceAssignmentExpression来实际定义用户。 当前,只支持formalExpressions。
<process ... >

  ...

  <userTask id='theTask' name='important task' >
    <humanPerformer>
      <resourceAssignmentExpression>
        <formalExpression>kermit</formalExpression>
      </resourceAssignmentExpression>
    </humanPerformer>
  </userTask>

只有一个用户可以坐拥任务的执行者分配给用户。 在activiti中,用户叫做执行者。 拥有执行者的用户不会出现在其他人的任务列表中, 只能出现执行者的个人任务列表中。
直接分配给用户的任务可以通过TaskService像下面这样获取:

List<Task> tasks = taskService.createTaskQuery().taskAssignee("kermit").list();

任务也可以加入到人员的候选任务列表中。 这时,需要使用potentialOwner元素。 用法和humanPerformer元素类似。注意它需要指定表达式中的每个项目是人员还是群组 (引擎猜不出来)。

牛刀小试

配置头,(定义约束,xsd.DTD)

BPMN 2.0根节点是definitions节点。 这个元素中,可以定义多个流程定义(不过我们建议每个文件只包含一个流程定义, 可以简化开发过程中的维护难度)。 一个空的流程定义看起来像下面这样。注意,definitions元素 最少也要包含xmlns 和 targetNamespace的声明。 targetNamespace可以是任意值,它用来对流程实例进行分类。

<definitions
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:activiti="http://activiti.org/bpmn"
  targetNamespace="Examples">
  <process id="myProcess" name="My First Process">
    ..
  </process>
</definitions>

也可以选择添加线上的BPMN 2.0格式位置

<xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL
                    http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd>

process元素有两个属性:

  • id:这个属性是必须的, 它对应着Activiti ProcessDefinition对象的key属性。 ‘id可以用来启动流程定义的流程实例, 通过RuntimeService的startProcessInstanceByKey方法。 这个方法会一直使用最新发布版本的流程定义(译者注:实际中一般都使用这种方式启动流程)。
  • name:这个属性是可选的, 对应ProcessDefinition的name属性。 引擎自己不会使用这个属性,它可以用来在用户接口显示便于阅读的名称。
    • 注意,它和startProcessInstanceById方法不同。 这个方法期望使用Activiti引擎在发布时自动生成的id。, 可以通过调用processDefinition.getId()方法获得这个值。 生成的id的格式为’key:version’, 最大长度限制为64个字符, 如果你在启动时抛出了一个ActivitiException,说明生成的id太长了, 需要限制流程的key的长度。
<definitions id="definitions"
  targetNamespace="http://activiti.org/bpmn20"
  xmlns:activiti="http://activiti.org/bpmn"
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">

        <process id="financialReport" name="Monthly financial report reminder process">

          <startEvent id="theStart" />

          <sequenceFlow id='flow1' sourceRef='theStart' targetRef='writeReportTask' />

          <userTask id="writeReportTask" name="Write monthly financial report" >
            <documentation>
              用户任务- 制作月度报表
            </documentation>
            <potentialOwner>
              <resourceAssignmentExpression>
                <formalExpression>accountancy</formalExpression>
              </resourceAssignmentExpression>
            </potentialOwner>
          </userTask>

          <sequenceFlow id='flow2' sourceRef='writeReportTask' targetRef='verifyReportTask' />

          <userTask id="verifyReportTask" name="Verify monthly financial report" >
            <documentation>
             用户任务-审核月度报表
            </documentation>
            <potentialOwner>
              <resourceAssignmentExpression>
                <formalExpression>management</formalExpression>
              </resourceAssignmentExpression>
            </potentialOwner>
          </userTask>

          <sequenceFlow id='flow3' sourceRef='verifyReportTask' targetRef='theEnd' />

          <endEvent id="theEnd" />

        </process>

</definitions>

在这里插入图片描述

现在我们创建好了业务流程的流程定义。 有了这个流程定义,我们可以创建流程实例了。 这时,一个流程实例对应了特定月度财报的创建和审批。 所有流程实例都共享同一个流程定义。
步骤:

  • 发布业务流程

    • 流程定义会保存到持久化的数据存储里, 是为你的Activiti引擎特别配置。所以部署好你的业务流程, 我们就能确认引擎重启后还能找到流程定义。
    • BPMN 2.0流程文件会解析成内存对象模型, 可以通过Activiti API操作。
    • 发布方式:一种方式是通过下面的API。注意所有与Activiti引擎的交互都是通过services。
   Deployment deployment = repositoryService.createDeployment()
		 .addClasspathResource("FinancialReportProcess.bpmn20.xml")
 		.deploy();
  • 启动一个新流程实例:

    • 使用我们定义在流程定义里的id(对应XML文件中的process元素)。 注意这里的id对于Activiti来说, 应该叫做key
 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("financialReport");  

创建一个流程实例,首先进入开始事件。 开始事件之后,它会沿着所有的外出连线(这里只有一条)执行, 到达第一个任务(“制作月度财报”)。

Activiti会把一个任务保存到数据库里。 这时,分配到这个任务的用户或群组会被解析,也会保存到数据库里。

Activiti引擎会继续执行流程的环节,除非遇到一个 等待状态,比如用户任务.

在等待状态下,当前的流程实例的状态会保存到数据库中。 直到用户决定完成任务才能改变这个状态。这时,引擎会继续执行, 直到遇到下一个等待状态,或流程结束。 如果中间引擎重启或崩溃, 流程状态也会安全的保存在数据库里。

任务创建之后,startProcessInstanceByKey会在到达用户任务 这个等待状态之后才会返回。这时,任务分配给了一个组, 这意味着这个组是执行这个任务的候选组。

public static void main(String[] args) {
    
    

  //创建一个Activiti引擎。
  ProcessEngine processEngine = ProcessEngineConfiguration
    .createStandaloneProcessEngineConfiguration()
    .buildProcessEngine();

  // 从仓库里的一个得到一个流程执行服务类
  RepositoryService repositoryService = processEngine.getRepositoryService();
  RuntimeService runtimeService = processEngine.getRuntimeService();

  // 部署流程实例
  repositoryService.createDeployment()
    .addClasspathResource("FinancialReportProcess.bpmn20.xml")
    .deploy();

  //启动流程实例
  runtimeService.startProcessInstanceByKey("financialReport");
}
  • 任务列表

    • 通过TaskService来获取任务。
List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit").list();

我们传入的用户必须是accountancy组的一个成员, 要和流程定义中向对应:
也可以使用群组名称,通过任务查询API来获得相关的结果

TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("accountancy").list();
  • 领取任务

现在一个会计要认领这个任务。 认领以后,这个用户就会成为任务的执行人 , 任务会从会计组的其他成员的任务列表中消失。 认领任务的代码如下所示:

taskService.claim(task.getId(), "fozzie");
  • 任务会进入认领任务人的个人任务列表中

List<Task> tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
  • 完成任务

taskService.complete(task.getId());

对于Activiti引擎,需要一个外部信息来让流程实例继续执行。 任务会把自己从运行库中删除。 流程会沿着单独一个外出连线执行,移动到第二个任务 (‘审批报告’)。 与第一个任务相同的机制会使用到第二个任务上, 不同的是任务是分配给 management组。

  • 结束流程

审批任务可以像之前介绍的一样查询和领取。 完成第二个任务会让流程执行到结束事件,就会结束流程实例。 流程实例和所有相关的运行数据都会从数据库中删除。

  • 通过程序,你也可以使用historyService判断流程已经结束了。
HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult();
System.out.println("Process instance end time: " + historicProcessInstance.getEndTime());
  • 代码总结

public class TenMinuteTutorial {
    
    

  public static void main(String[] args) {
    
    

    // Create Activiti process engine
    ProcessEngine processEngine = ProcessEngineConfiguration
      .createStandaloneProcessEngineConfiguration()
      .buildProcessEngine();

    // Get Activiti services
    RepositoryService repositoryService = processEngine.getRepositoryService();
    RuntimeService runtimeService = processEngine.getRuntimeService();

    // Deploy the process definition
    repositoryService.createDeployment()
      .addClasspathResource("FinancialReportProcess.bpmn20.xml")
      .deploy();

    // Start a process instance
    String procId = runtimeService.startProcessInstanceByKey("financialReport").getId();

    // Get the first task
    TaskService taskService = processEngine.getTaskService();
    List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("accountancy").list();
    for (Task task : tasks) {
    
    
      System.out.println("Following task is available for accountancy group: " + task.getName());

      // claim it
      taskService.claim(task.getId(), "fozzie");
    }

    // Verify Fozzie can now retrieve the task
    tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
    for (Task task : tasks) {
    
    
      System.out.println("Task for fozzie: " + task.getName());

      // Complete the task
      taskService.complete(task.getId());
    }

    System.out.println("Number of tasks for fozzie: "
            + taskService.createTaskQuery().taskAssignee("fozzie").count());

    // Retrieve and claim the second task
    tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
    for (Task task : tasks) {
    
    
      System.out.println("Following task is available for accountancy group: " + task.getName());
      taskService.claim(task.getId(), "kermit");
    }

    // Completing the second task ends the process
    for (Task task : tasks) {
    
    
      taskService.complete(task.getId());
    }

    // verify that the process is actually finished
    HistoryService historyService = processEngine.getHistoryService();
    HistoricProcessInstance historicProcessInstance =
      historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult();
    System.out.println("Process instance end time: " + historicProcessInstance.getEndTime());
  }

}

猜你喜欢

转载自blog.csdn.net/sanhewuyang/article/details/108454444