Activiti工作流Day08-BPMN 2.0结构

Activiti支持的BPMN 2.0结构和对BPMN标准的扩展

自定义扩展

  • BPMN 2.0标准对于各方都是一个好东西
    • 用户不用担心会绑死在供应商提供的专有解决方案上
    • 框架,特别是activiti这样的开源框架,可以提供相同功能,甚至是更好的实现,足以和大的供应商媲美
    • 按照BPMN 2.0标准,从大供应商的解决方案迁移到activiti只要经过一个简单而平滑的过程
  • BPMN 2.0标准不好的一点是
    • 它常常是不同公司之间大量讨论和妥协的结果
    • 作为开发者去阅读流程定义的BPMN 2.0xml时,有时会感觉用这种结构和方法去做事太麻烦了
    • 因此activiti把简化开发作为最优先的事情,使用一些被称为Activiti BPMN扩展的功能,这些扩展是新的结构或方法来简化对应的结构,并不属于BPMN 2.0规范
  • 根据BPMN 2.0标准开发自定义扩展的注意点:
    • 自定义扩展的前提是总有简单的方法转换成标准方法. 所以使用自定义扩展时,可以及时撤销自定义扩展
    • 当使用自定义扩展时 ,总会清楚的指明使用了新的XML元素,属性… 比如会使用activiti:命名空间前缀
    • 扩展的目标是最终加入到下一版本的BPMN规范,或者至少可以引起对特定BPMN结构的讨论

事件

  • 事件用来表明流程的生命周期中发生了什么事. 事件总是画成一个圆圈
  • 在BPMN 2.0中,事件有两大分类:捕获(catching)事件触发(throwing)事件:
    • 捕获(catching): 当流程执行到事件,会等待被触发.触发的类型是由内部图表或XML中的类型声明来决定的.捕获事件与触发事件在显示方面是根据内部图表是否被填充来区分的(白色)
    • 触发(throwing): 当流程执行到事件,会触发一个事件.触发的类型是由内部图表或XML中的类型声明来决定的.触发事件与捕获事件在显示方面是根据内部图表是否被填充来区分的(黑色)

事件定义

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

定时器事件定义

  • 定时器事件是根据指定的时间触发的事件
  • 定时器事件可以用于开始事件,中间事件和边界事件
  • 定时器定义元素:
  • timeDate: 触发事件的时间. 使用ISO8601格式指定的一个确定的时间:
<timerEventDefinition>
    <timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
  • timeDuration: 指定定时器之前要等待多长时间. timeDuration可以设置为timerEventDefinition的子元素,使用ISO8601规定的格式
<timerEventDefinition>
	<!--等待10天-->
    <timeDuration>P10D</timeDuration>
</timerEventDefinition>
  • timeCycle: 指定重复执行的间隔. 可以用来定期启动流程实例,或为超时时间发送多个提醒.timeCycle元素可以使用两种格式:
    • ISO8601标准的格式
    • cron表达式
<timerEventDefinition>
	<!--重复3次,每次间隔10小时-->
    <timeCycle>R3/PT10H</timeCycle>
</timerEventDefinition>
  • 从整点开始,每5分钟执行一次:
0 0/5 * * * ?
  • 注意:
    • 第一个数字表示秒, 而不是像通常Unix cron中那样表示分钟
    • 重复的时间周期能更好的处理相对时间, 可以计算一些特定的时间点:用户任务的开始时间
    • cron表达式可以处理绝对时间, 这对定时启动事件特别有用
  • 在定时器事件定义中使用表达式,可以通过流程变量来影响那个定时器定义: 流程定义必须包含ISO8601(或者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>

信号事件定义

  • 信号事件会引用一个已命名的信号
  • 信号全局范围的事件(广播语义).会发送给所有激活的处理器
  • 信号事件定义使用signalEventDefinition元素 .signalRef属性会引用definitions根节点里定义的signal子元素(signalEventDefinition引用相同的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>
触发信号事件
  • 可以通过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属性:
<signal id="alertSignal" name="alert" activiti:scope"processInstance"/>

默认情况下,scope的属性为global

信号事件实例
  • 不同流程使用信号交互:
  • 流程在保险规则更新或改变时启动.在修改被参与者处理时,会触发一个信号,通知规则改变:
    在这里插入图片描述
    这个事件会被所有相关的流程实例捕获
  • 订阅这个事件的流程实例:
    -
  • 信号事件是广播给所有激活的处理器的
  • 在上面的例子中,所有流程实例都会接收到这个事件,这就是我们想要的.
  • 然而,有的情况下并不想要这种广播行为,考虑下面的流程:
    在这里插入图片描述
    上述流程描述的模式activiti并不支持.这种想法是:
    • 执行[do something]任务时出现的错误
    • 被边界错误事件捕获
    • 然后使用信号传播给并发路径上的分支
    • 进而中断[do something inparallel]任务
  • 目前,activiti实际运行的结果与期望一致.信号会传播给边界事件并中断任务.但是,根据信号的广播含义,也会传播给所有其他订阅了信号事件的流程实例.所以,这就不是我们想要的结果
  • 注意:
    • 信号事件不会执行任何与特定流程实例的联系
    • 如果只想把一个信息发给指定的流程实例,需要手工关联,再使用 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

消息事件实例
  • 使用两个不同消息启动的流程实例:
    在这里插入图片描述
  • 消息事件可以用于流程需要不同的方式来区分开始事件时,最终会进入同样的路径

开始事件

  • 开始事件用来指明流程在哪里开始
  • 开始事件的类型(流程在接收事件时启动,还是在指定时间启动…),定义了流程如何启动, 这通过事件中不同的小图表来展示.在XML中,这些类型是通过声明不同的子元素来区分
  • 开始事件都是捕获事件: 最终这些事件都是一直等待着,直到对应的触发时机出现
  • 在开始事件中,可以设置activiti特定属性:
    • initiator: 当流程启动时,把当前登录的用户保存到指定的变量名中:
    <startEvent id="request" activiti:initiator="initiator" />
    
    • 登录的用户必须使用IdentityService.setAuthenticatedUserId(String) 方法设置,并包含在try-finally代码中:
 try {
 identityService.setAuthenticatedUserId("bono");
 runtimeService.startProcessInstanceByKey("someProcessKey");
} finally {
 identityService.setAuthenticatedUserId(null);
}

空开始事件

描述
  • 空开始事件技术上意味着没有指定启动流程实例的触发条件
  • 引擎不能预计什么时候流程实例会启动
  • 空开始事件用于: 当流程实例要通过API启动的场景,通过调用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内容是普通开始事件的声明**,包含一个定时定义子元素**
		<startEvent id="theStart">
            <timerEventDefinition>
            	<!--流程会启动4次,每次间隔5分钟,从2011年3月11日,12:13开始计时-->
                <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);
  • 这里的messageNamemessageEventDefinitionmessageRef属性引用的message元素的name属性
  • 启动流程实例时,要考虑以下因素:
    • 消息开始事件只支持顶级流程,消息开始事件不支持内嵌子流程
    • 如果流程定义有多个消息开始事件, runtimeService.startProcessInstanceByMessage(…) 会选择对应的开始事件
    • 如果流程定义有多个消息开始事件和一个空开始事件. runtimeService.startProcessInstanceByKey(…)和 runtimeService.startProcessInstanceById(…)会使用空开始事件启动流程实例
    • 如果流程定义有多个消息开始事件,但是没有空开始事件, runtimeService.startProcessInstanceByKey(…)和 runtimeService.startProcessInstanceById(…)会抛出异常
    • 如果流程定义只有一个消息开始事件, runtimeService.startProcessInstanceByKey(…)和 runtimeService.startProcessInstanceById(…)会使用这个消息开始事件启动流程实例
    • 如果流程被调用环节(callActivity)启动,消息开始事件只支持如下情况:
      • 在消息开始事件以外,还有一个单独的空开始事件
      • 流程只有一个消息开始事件,没有空开始事件
图形标记
  • 消息开始事件是一个圆圈,中间是一个消息事件图标.图标是白色未填充的,来表示捕获(接收)行为
    -
XML内容
  • 消息开始事件的XML内容在普通开始事件中,包含一个messageEventDefinition子元素:
<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" />

  <process id="invoiceProcess">

    <startEvent id="messageStart" >
        <messageEventDefinition messageRef="tns:newInvoice" />
    </startEvent>
    ...
  </process>

</definitions>

信号开始事件

描述
  • 信号开始事件可以通过一个已命名的信号(signal)来启动一个流程实例
    • 信号可以在流程实例内部使用中间信号抛出事务触发
    • 也可以通过API(runtimService.signalEventReceivedXXX)触发
    • 两种情况下,所有流程实例中拥有相同名称的signalStartEvent都会启动
    • 两种情况下,都可以选择同步或异步的方式启动流程实例
  • 必须向API传入signalName, 这是signal元素的name属性值,它会被signalEventDefinitionsignalRef属性引用
图形标记
  • 信号开始事件显示为一个中间包含信号事件图标的圆圈.标记是无填充的,表示捕获(接收)行为
    在这里插入图片描述
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>

错误开始事件

描述
  • 错误开始事件可以用来触发一个事件子流程.错误开始事件不能用来启动流程实例
  • 错误开始事件都是中断事件
图形标记
  • 错误开始事件是一个圆圈,包含一个错误事件标记.标记是白色未填充的,来表示捕获(接收)行为
    在这里插入图片描述
XML内容
  • 错误开始事件的XML内容是普通开始事件定义中,包含一个 errorEventDefinition子元素
<startEvent id="messageStart" >
        <errorEventDefinition errorRef="someError" />
</startEvent>

结束事件

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

空结束事件

描述
  • 空结束事件意味着到达事件时不会指定抛出的结果
  • 引擎会直接结束当前执行的分支,不会做其他事情
图形标记
  • 空结束事件是一个粗边圆圈, 内部没有小图标(无结果类型)
    在这里插入图片描述
XML内容
  • 空结束事件的XML内容是普通结束事件定义
  • 不包含子元素,其他结束事件类型都会包含声明类型的子元素
<endEvent id="end" name="my end event" />

错误结束事件

描述
  • 当流程执行到错误结束事件 ,流程的当前分支就会结束,并抛出一个错误
  • 这个错误可以被对应的中间边界错误事件捕获.如果找不到匹配的边界错误事件,就会抛出一个异常
图形标记
  • 错误结束事件是一个标准的结束事件 -粗边圆圈, 内部有错误图标, 错误图标是全黑的,表示触发语法
    在这里插入图片描述
XML内容
  • 错误结束事件的内容是一个错误事件, 子元素为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>
  • 注意errorRef必须与BPMN 2.0格式相符,必须是一个合法的QName

取消结束事件

描述
  • 取消结束事件只能与BPMN事务子流程结合使用
  • 当到达取消结束事件时,会抛出取消事件 ,它必须被取消边界事件捕获
  • 取消边界事件会取消事务,并触发补偿机制
图形标记
  • 取消结束事件显示为标准的结束事件-粗边圆圈,包含一个取消图标.取消图标是全黑的,表示触发语法
    在这里插入图片描述
XML内容
  • 取消结束事件内容是一个结束事件, 包含cancelEventDefinition子元素
<endEvent id="myCancelEndEvent">
  <cancelEventDefinition />
</endEvent>

边界事件

  • 边界事件都是捕获事件,它会附在一个环节上
  • 边界事件是捕获事件,不可能触发事件:
    • 当节点运行时,事件会监听对应的触发类型
    • 当事件被捕获,节点就会中断,同时执行事件的后续连线
  • 边界事件的定义方式都一样:
<boundaryEvent id="myBoundaryEvent" attachedToRef="theActivity">
      <XXXEventDefinition/>
</boundaryEvent>
  • 边界事件使用如下方式进行定义:
    • 唯一标识:流程范围
    • 使用caught属性引用事件的节点,注意边界事件和它们附加的节点在同一级别上:比如,边界事件不是包含在节点内的
    • 格式为XXXEventDefinition的XML子元素 (比如,TimerEventDefinition,ErrorEventDefinition…)定义了边界事件的类型

定时边界事件

描述
  • 定时边界事件就是一个暂停等待警告的时钟
  • 当流程执行到绑定了边界事件的环节,会启动一个定时器
  • 当定时器触发时(一定时间之内),环节就会中断,并沿着定时边界事件的外出连线继续执行
图形标记
  • 定时边界事件是一个标准的边界事件(边界上的一个圆圈),内部是一个定时器小图标
    在这里插入图片描述
XML内容
  • 定时器边界任务定义是一个标准的边界事件,指定类型的子元素是timerEventDefinition元素
<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
   <timerEventDefinition>
    <timeDuration>PT4H</timeDuration>
  </timerEventDefinition>
</boundaryEvent>
  • 在流程图中,圆圈边线是虚线:
    在这里插入图片描述
  • 经典应用场景: 发送通知邮件,但是不打断正常流程的执行
    • 中断和非中断的事件还是有区别的
      • 默认是中断事件
      • 非中断事件的情况,不会中断原始环节,那个环节还停留在原地
      • 对应的,会创建一个新分支,并沿着事件的流向继续执行.在XML内容中,要把cancelActivity属性设置为false
      <boundaryEvent id="escalationTimer" cancelActivity="false" attachedToRef="firstLineSupport"/>
      
  • 注意: 边界定时事件只能在job执行器启用时使用
    • 比如:把activiti.cfg.xml中的jobExecutorActivate设置为true,默认job执行器是禁用的
边界事件的问题
  • 同步问题: 边界事件后面不能有多条外出连线
  • 解决这个问题的方法是在一个连线后使用并发网关
    -

错误边界事件

描述
  • 错误边界事件: 节点边界上的中间捕获错误事件,会捕获节点范围内抛出的错误
  • 定义一个边界错误事件,大多用于内嵌子流程或者调用节点
    • 对于子流程的情况,它会为所有内部的节点创建一个作用范围
    • 错误是由错误结束事件抛出的.这个错误会传递给上层作用域,直到找到一个错误事件定义相匹配的边界错误事件
  • 当捕获了错误事件时 ,边界任务绑定的节点就会销毁,也会销毁内部所有的执行分支(同步节点,内嵌子流程…).流程执行会继续沿着边界事件的外出连线继续执行
图形标记
  • 错误边界事件显示成一个普通的中间事件(圆圈内部有一个小圆圈)放在节点的标记上,内部有一个错误小图标.错误小图标是白色的,表示它是一个捕获事件
    在这里插入图片描述
XML内容
  • 边界错误事件定义为普通的边界事件:
<boundaryEvent id="catchError" attachedToRef="mySubProcess">
  <errorEventDefinition errorRef="myError"/>
</boundaryEvent>
  • 和错误结束事件一样 ,errorRef引用了process元素外部的一个错误定义
<error id="myError" errorCode="123" />
...
<process id="myProcess">
...
  • errorCode用来匹配捕获的错误:
    • 如果没有设置errorRef,边界错误事件会捕获所有错误事件,无论错误的errorCode是什么
    • 如果设置了errorRef,并引用了一个已存在的错误,边界事件就只捕获错误代码与之相同的错误
    • 如果设置了errorRef,但是BPMN 2.0中没有定义错误,errorRef就会当做errorCode使用
错误边界事件实例
  • 如何使用错误结束事件的流程实例
    • 当完成审核盈利这个用户任务时,如果没有提供足够的信息,就会抛出错误
    • 错误会被子流程的边界任务捕获,所有回顾销售子流程中的所有节点都会销毁,即使审核客户比率还没有完成,并创建一个提供更多信息的用户任务
      在这里插入图片描述

信号边界事件

描述
  • 节点边界的中间捕获信号,会捕获信号定义引用的相同信号名的信号
  • 与其他事件(比如边界错误事件)不同,边界信号事件不只捕获绑定方位的信号.信号事件是一个全局的范围(广播语义),就是说信号可以在任何地方触发,即便是不同的流程实例
  • 和其他事件(比如边界错误事件)不同 ,捕获信号后,不会停止信号的传播. 如果有两个信号边界事件,捕获相同的信号事件,两个边界事件都会被触发,即使在不同的流程实例中
图形标记
  • 边界信号事件显示为普通的中间事件(圆圈里有个小圆圈),位置在节点的边缘, 内部有一个信号小图标.信号图标是白色的(未填充),表示捕获的意思
    在这里插入图片描述
XML内容
  • 边界信号事件定义为普通的边界事件:
<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
          <signalEventDefinition signalRef="alertSignal"/>
</boundaryEvent>

消息边界事件

描述
  • 节点边界上的中间捕获消息, 根据引用的消息定义捕获相同消息名称的消息
图形标记
  • 边界消息事件显示成一个普通的中间事件(圆圈里有个小圆圈),位于节点边缘,内部是一个消息小图标.消息图标是白色(无填充),表示捕获的意思
    在这里插入图片描述
  • 注意: 边界消息事件可能是非中断(左侧) 或者中断(右侧)
XML内容
  • 边界消息事件定义为标准的边界事件
<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
          <messageEventDefinition messageRef="newCustomerMessage"/>
</boundaryEvent>

取消边界事件

描述
  • 在事务性子流程的边界上的中间捕获取消
  • 事务取消时触发,当取消边界事件触发时:
    • 首先中断当前作用域的所有执行
    • 然后开始补偿事务内的所有激活的补偿边界事件.补偿是同步执行的:离开事务前,边界事务会等待补偿执行完毕
    • 当补偿完成后,事务子流程会沿着取消边界事务的外出连线继续执行
  • 注意:
    • 每个事务子流程只能有一个取消边界事件
    • 如果事务子流程包含内嵌子流程,补偿只会触发已经成功完成的子流程
    • 如果取消边界子流程对应的事务子流程配置为多实例,如果一个实例触发了取消,就会取消所有实例
图形标记
  • 取消边界事件显示为了一个普通的中间事件(圆圈里套小圆圈),在节点的边缘,内部是一个取消小图标.取消图标是白色(无填充),表明是捕获的意思
    在这里插入图片描述
XML内容
  • 取消边界事件定义为普通边界事件
<boundaryEvent id="boundary" attachedToRef="transaction" >
          <cancelEventDefinition />
</boundaryEvent>
  • 取消边界事件都是中断的,不需要使用cancelActivity属性

补偿边界事件

描述
  • 节点边界的中间捕获补偿
  • 用来设置一个节点的补偿处理器
  • 补偿边界事件必须使用直接引用设置唯一的补偿处理器
  • 补偿边界事件与其他边界事件的策略不同:
    • 其他边界事件(信号边界事件)当到达关联的节点就会被激活.离开节点时,就会挂起,对应的事件订阅也会取消
    • 补偿边界事件在关联的节点成功完成时激活,当补偿事件触发或对应流程实例结束时,事件订阅才会删除
  • 补偿边界事件遵循如下规则:
    • 补偿触发时,补偿边界事件对应的补偿处理器会调用相同次数,根据它对应的节点的成功次数
    • 如果补偿边界事件关联到多实例节点,补偿事件会订阅每个实例
    • 如果补偿边界事件关联的节点中包含循环,补偿事件会在每次节点执行时进行订阅
    • 如果流程实例结束,订阅的补偿事件都会结束
  • 补偿边界事件不支持内嵌子流程
图形标记
  • 补偿边界事件显示为标准中间事件(圆圈里套圆圈),位于节点边缘,内部有一个补偿小图标.补偿图标是白色的(无填充),表示捕获的意思
  • 下图演示使用无方向的关联,为边界事件设置补偿处理器:
    在这里插入图片描述
XML内容
  • 补偿边界事件定义为标准边界事件
<boundaryEvent id="compensateBookHotelEvt" attachedToRef="bookHotel" >
          <compensateEventDefinition />
</boundaryEvent>

<association associationDirection="One" id="a1"  sourceRef="compensateBookHotelEvt" targetRef="undoBookHotel" />

<serviceTask id="undoBookHotel" isForCompensation="true" activiti:class="..." />
  • 补偿边界事件在节点成功完成后激活, 不支持cancelActivity属性

中间捕获事件

  • 所有中间捕获事件都使用同样的方式定义:
<intermediateCatchEvent id="myIntermediateCatchEvent" >
      <XXXEventDefinition/>
</intermediateCatchEvent>
  • 中间捕获事件的定义:
    • 唯一标识(流程范围内)
    • 一个结构为XXXEventDefinition的XML子元素(TimerEventDefinition)定义了中间捕获事件的类型

定时中间捕获事件

描述
  • 定时中间事件作为一个监听器
  • 当执行到达捕获事件节点,就会启动一个定时器.当定时器触发(比如,一段时间之后),流程就会沿着定时中间事件的外出节点继续执行
图形标记
  • 定时器中间事件显示成标准中间捕获事件, 内部是一个定时器小图标:
    在这里插入图片描述
XML内容
  • 定时器中间事件定义为标准中间捕获事件. 指定类型的子元素为timerEventDefinition元素
		<intermediateCatchEvent id="timer">
            <timerEventDefinition>
                <timeDuration>PT5M</timeDuration>
            </timerEventDefinition>
        </intermediateCatchEvent>

信号中间捕获事件

  • 中间捕获信号事件,通过引用信号定义来捕获相同信号名称的信号
  • 信号中间捕获事件与其它事件(比如错误事件)不同:
    • 信号不会在捕获之后被消费
    • 如果有两个激活的信号边界事件捕获相同的信号事件,两个边界事件都会被触发,即便在不同的流程实例中

图形标记

  • 中间信号捕获事件显示为一个普通的中间事件(圆圈套圆圈),内部有一个信号小图标.信号小图标是白色的(无填充),表示捕获的意思
    在这里插入图片描述
XML内容
  • 信号中间事件定义为普通的中间捕获事件. 对应类型的子元素是signalEventDefinition元素
<intermediateCatchEvent id="signal">
        <signalEventDefinition signalRef="newCustomerSignal" />
</intermediateCatchEvent>

消息中间捕获事件

描述
  • 中间捕获消息事件,捕获特定名称的消息
图形标记
  • 中间捕获消息事件显示为普通中间事件(圆圈套圆圈),内部是一个消息小图标.消息图标是白色的(无填充),表示捕获的意思
    在这里插入图片描述
XML内容
  • 消息中间事件定义为标准中间捕获事件. 指定类型的子元素是messageEventDefinition元素
<intermediateCatchEvent id="message">
        <messageEventDefinition signalRef="newCustomerMessage" />
</intermediateCatchEvent>

内部触发事件

  • 所有内部触发事件的定义都是一样的:
<intermediateThrowEvent id="myIntermediateThrowEvent" >
      <XXXEventDefinition/>
</intermediateThrowEvent>
  • 内部触发事件定义包含:
    • 唯一标识(流程范围)
    • 使用格式为XXXEventDefinition的XML子元素(比如signalEventDefinition等)定义中间触发事件的类型
中间触发空事件
  • 空中间触发事件流程图,用于表示流程中的某个状态
    在这里插入图片描述
  • 可以添加执行监听器:
<intermediateThrowEvent id="noneEvent">
  <extensionElements>
    <activiti:executionListener class="org.activiti.engine.test.bpmn.event.IntermediateNoneEventTest$MyExecutionListener" event="start" />
  </extensionElements>
</intermediateThrowEvent>
  • 可以添加自己的代码,把事件发送给BAM工具或DWH.引擎不会为这个事件做任何事情,它直接径直通过

信号中间触发事件

描述
  • 信号中间触发事件为定义的信号抛出一个信号事件
  • 在activiti中,信号会广播到所有激活的处理器中.信号可以通过同步和异步方式发布
    • 默认配置下,信号是同步发送的:
      • 抛出事件的流程实例会等到信号发送给所有捕获流程实例才继续执行
      • 捕获流程实例也会在触发流程实例的同一个事务中执行
      • 如果某个监听流程出现了技术问题(抛出异常),所有相关的实例都会失败
    • 信号也可以异步发送:
      • 会在到达抛出信号事件后决定哪些处理器是激活的
      • 对这些激活的处理器,会保存一个异步提醒消息(任务),并发送给jobExecutor
图形标记
  • 中间信号触发事件显示为普通中间事件(圆圈套圆圈),内部又一个信号小图标,信号图标是黑色的(有填充),表示触发的意思
    在这里插入图片描述
XML内容
  • 消息中间事件定义为标准中间触发事件. 指定类型的子元素是signalEventDefinition元素
<intermediateThrowEvent id="signal">
        <signalEventDefinition signalRef="newCustomerSignal" />
</intermediateThrowEvent>
  • 异步信号事件如下:
<intermediateThrowEvent id="signal">
        <signalEventDefinition signalRef="newCustomerSignal" activiti:async="true" />
</intermediateThrowEvent>

补偿中间触发事件

描述
  • 用来触发补偿
  • 触发补偿:
    • 补偿可以由特定节点或包含补偿事件的作用域触发
    • 补偿是通过分配给节点的补偿处理器来完成的
      • 当补偿由节点触发,对应的补偿处理器会根据节点成功完成的次数执行相同次数
      • 如果补偿由当前作用域触发,当前作用域的所有节点都会执行补偿,也包含并发分支
      • 补偿的触发是继承式的:
        • 如果执行补偿的节点是子流程,补偿会作用到子流程中包含的所有节点
        • 如果子流程是内嵌节点,补偿会递归触发
        • 补偿不会传播到流程的上层
        • 如果补偿在子流程中触发,不会传播到子流程范围外
        • bpmn规范定义,由节点触发的流程只会作用到子流程同一级别
      • activiti的补偿执行次序与流程执行顺序相反: 最后完成的节点会最先执行补偿
      • 补偿中间触发事件可以用来补偿成功完成的事务性子流程
  • 注意:
    • 如果补偿被一个包含子流程的作用域触发,子流程还包含了关联补偿处理器的节点, 如果它已经成功完成了,补偿只会传播到子流程
    • 如果子流程中的节点也完成了,并关联了补偿处理器,如果子流程包含的这些节点还没有完成,就不会执行补偿处理器
      在这里插入图片描述
      这个流程中,我们有两个并发分支,一个分支是内嵌子流程,一个是使用信用卡节点.假设两个分支都启动了,第一个分支等待用户完成审核预定任务.第二个分支执行使用信用卡节点, 并发生了一个错误,这导致取消预定事件,并触发补偿.这时,并发子流程还没有结束,意味着补偿事件不会传播给子流程, 所以取消旅店预定这个补偿处理器不会执行.如果用户任务(就是内嵌子流程)在取消预定之前完成了,补偿就会传播给内嵌子流程
  • 流程变量:
    • 当补偿内嵌子流程时,用来执行补偿处理器的分支可以访问子流程的本地流程实例,因为这时是子流程完成的分支
    • 为了实现这个功能,流程变量的快照会分配给分支(为执行子流程而创建的分支)有以下限制条件:
      • 补偿处理器无法访问子流程内部创建的,添加到同步分支的变量
      • 分配给分支的流程变量在继承关系上层的(分配给流程实例的流程变量没有包含在快照中):补偿触发时,补偿处理器通过它们所在的地方访问这些流程变量
      • 变量快照只用于内嵌子流程,不适用其他节点
 已知限制:
 1. waitForCompletion="false"还不支持。当补偿使用中间触发补偿事件触发时, 事件没有等待,在补偿成功结束后
 2. 补偿自己由并发分支执行。并发分支的执行顺序与被补偿的节点完成次序相反。 未来activiti可能支持选项来顺序执行补偿
 3. 补偿不会传播给callActivity调用的子流程实例
图形标记
  • 中间补偿触发事件显示为标准中间事件(圆圈套圆圈),内部是一个补偿小图标.补偿图标是黑色的(有填充),表示触发的意思
    在这里插入图片描述
XML内容
  • 补偿中间事件定义为普通的中间触发事件. 对应类型的子元素是compensateEventDefinition元素
<intermediateThrowEvent id="throwCompensation">
        <compensateEventDefinition />
</intermediateThrowEvent>
  • 可选参数activityRef可以用来触发特定作用域/节点的补偿
<intermediateThrowEvent id="throwCompensation">
        <compensateEventDefinition activityRef="bookHotel" />
</intermediateThrowEvent>

顺序流

描述
  • 顺序流是连接两个流程节点的连线
  • 流程执行完一个节点后,会沿着节点的所有外出顺序流继续执行
  • BPMN 2.0默认的行为就是并发的:两个外出顺序流会创造两个单独的,并发流程分支
图形标记
  • 顺序流显示为从起点到终点的箭头.箭头总是指向终点
    在这里插入图片描述
XML内容
  • 顺序流需要流程范围内唯一的id, 以及对起点终点元素的引用
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />

条件顺序流

描述
  • 为顺序流定义一个条件
  • 离开一个BPMN 2.0节点时,默认会计算外出顺序流的条件
    • 如果条件结果为true,就会选择外出顺序流继续执行
    • 当多条顺序流被选中时,就会创建多条分支,流程会继续以并行方式继续执行
  • 注意: 不包括网关 ,网关会用特定的方式处理顺序流中的条件, 这与网关类型相关
图形标记
  • 条件顺序流显示为一个正常的顺序流,在起点有一个菱形. 条件表达式也会显示在顺序流上
    在这里插入图片描述
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发布包中,包含以下流程实例,使用了值和方法表达式
    在这里插入图片描述

默认顺序流

描述
  • 所有的BPMN 2.0任务和网关都可以设置一个默认顺序流
  • 只有在节点的其它外出顺序流不能被选中时,才会使用作为外出顺序流继续执行
  • 默认顺序流的条件设置不会生效
图形标记
  • 默认顺序流显示为普通顺序流, 起点有一个斜线标记
    在这里插入图片描述
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>
  • 对应图形:
    在这里插入图片描述

网关

  • 网关用来控制流程的流向(流程的tokens),网关可以消费也可以生成token
  • 网关显示成菱形图形,内部有有一个小图标.图标表示网关的类型:
    在这里插入图片描述

排他网关

描述
  • 排他网关: 异或XOR网关,用来在流程中实现决策
  • 当流程执行到这个网关,所有外出顺序流都会被处理一遍.其中条件解析为true的顺序流(或者没有设置条件,概念上在顺序流上定义了一个[true])会被选中,让流程继续运行
  • 注意: 通常情况下,所有条件结果为true的顺序流都会被选中,以并行方式执行,但排他网关只会选择一条顺序流执行. 就是说,虽然多个顺序流的条件结果为true,那么XML中的第一个顺序流(也只有这一条)会被选中,并用来继续运行流程.如果没有选中任何顺序流,会抛出一个异常
图形标记
  • 排他网关显示成一个普通网关(比如,菱形图形),内部是一个X图标,表示异或(XOR)语义.
  • 没有内部图标的网关,默认为排他网关
  • BPMN 2.0规范不允许在同一个流程定义中同时使用没有X和有X的菱形图形
    在这里插入图片描述
XML内容
  • 用一行定义了网关,条件表达式定义在外出顺序流中
  • 模型实例:
    在这里插入图片描述
<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>

并行网关

描述
  • 网关也可以表示流程中的并行情况
  • 允许将流程分成多条分支,也可以把多条分支汇聚到一起
  • 并行网关的功能是基于进入和外出的顺序流的:
    • 分支: 并行后的所有外出顺序流,为每个顺序流都创建一个并发分支
    • 汇聚: 所有到达并行网关,在此等待的进入分支 ,直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关
  • 同一个并行网关有多个进入和多个外出顺序流,同时具有分支和汇聚功能
  • 网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支
  • 并行网关不会解析条件: 与其他网关不同,即使顺序流中定义了条件,也会忽略
图形标记
  • 并行网关显示成一个普通网关(菱形)内部是一个 + 图标,表示与(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());

当两个任务都完成时,第二个并行网关会汇聚两个分支.因为它只有一条外出连线,不会创建并行分支,只会创建归档订单任务

  • 注意并行网关不需要是"平衡的"(对应并行网关的进入和外出节点数目相等).并行网关只是等待所有进入顺序流,并为每个外出顺序流创建并发分支,不会受到其他流程节点的影响
    在这里插入图片描述

包含网关

描述
  • 排他网关和并行网关的结合体:
    • 和排他网关一样,可以在外出顺序流上定义条件,包含网关会解析条件
    • 和并行网关一样,包含网关可以选择多于一条顺序流
  • 包含网关的功能是基于进入和外出顺序流的:
    • 分支: 所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行,会为每个顺序流创建一个分支
    • 汇聚: 所有并行分支到达包含网关,会进入等待状态,直到每个包含流程token的进入顺序流的分支都到达.这是与并行网关的最大不同.包含网关只会等待被选中执行了的进入顺序流. 在汇聚之后,流程会穿过包含网关继续执行
  • 如果同一个包含节点拥有多个进入和外出顺序流,它就会同时含有分支和汇聚功能
  • 网关会先汇聚所有拥有流程token的进入顺序流,再根据条件判断结果为true的外出顺序流,为它们生成多条并行分支
图形标记
  • 并行网关显示为一个普通网关(菱形),内部包含一个圆圈图标
    在这里插入图片描述
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== falseshipOrder == 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分钟内发出信号,定时器就会取消,流程会沿着信号执行
    • 如果信号没有出现,流程会沿着定时器的方向前进,信号订阅会被取消
      -
<definitions id="definitions"
        xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
        xmlns:activiti="http://activiti.org/bpmn"
        targetNamespace="Examples">

        <signal id="alertSignal" name="alert" />

        <process id="catchSignal">

                <startEvent id="start" />

                <sequenceFlow sourceRef="start" targetRef="gw1" />

                <eventBasedGateway id="gw1" />

                <sequenceFlow sourceRef="gw1" targetRef="signalEvent" />
                <sequenceFlow sourceRef="gw1" targetRef="timerEvent" />

                <intermediateCatchEvent id="signalEvent" name="Alert">
                        <signalEventDefinition signalRef="alertSignal" />
                </intermediateCatchEvent>

                <intermediateCatchEvent id="timerEvent" name="Alert">
                        <timerEventDefinition>
                                <timeDuration>PT10M</timeDuration>
                        </timerEventDefinition>
                </intermediateCatchEvent>

                <sequenceFlow sourceRef="timerEvent" targetRef="exGw1" />
                <sequenceFlow sourceRef="signalEvent" targetRef="task" />

                <userTask id="task" name="Handle alert"/>

                <exclusiveGateway id="exGw1" />

                <sequenceFlow sourceRef="task" targetRef="exGw1" />
                <sequenceFlow sourceRef="exGw1" targetRef="end" />

                <endEvent id="end" />
</process>
</definitions>

任务

用户任务

描述
  • 用户任务用来设置必须由人员完成的工作
  • 当流程执行到用户任务,会创建一个新任务,并把这个新任务加入到分配人或群组的任务列表中
图形标记
  • 用户任务显示成一个普通任务(圆角矩形),左上角有一个小用户图标
    在这里插入图片描述
XML内容
  • XML中的用户任务定义:id属性是必须的,name属性是可选的:
<userTask id="theTask" name="Important task" />
  • 用户任务可以设置描述,添加documentation元素可以定义描述:
<userTask id="theTask" name="Schedule meeting" >
  <documentation>
          Schedule an engineering meeting for next week with the new hire.
  </documentation>
  • 描述文本可以通过标准的java方法来获取:
task.getDescription()
持续时间
  • 任务可以用一个字段来描述任务的持续时间
  • 可以使用查询API来对持续时间进行搜索,根据在时间之前或之后进行搜索
    • Activiti提供了一个节点扩展,在任务定义中设置一个表达式,这样在任务创建时就可以设置初始持续时间
    • 表达式应该是:
      • 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元素类似**,需要指定表达式中的每个项目是人员还是群组**
<process ... >

  ...

  <userTask id='theTask' name='important task' >
    <potentialOwner>
      <resourceAssignmentExpression>
        <formalExpression>user(kermit), group(management)</formalExpression>
      </resourceAssignmentExpression>
    </potentialOwner>
  </userTask>
  • 使用potentialOwner元素定义的任务可以通过TaskService获取:
List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit");

这会获取所有kermit为候选人的任务,表达式中包含user(kermit).这也会获得所有分配包含kermit这个成员的群组(比如,group(management),前提是kermit是这个组的成员,并且使用了activiti的账号组件).用户所在的群组是在运行阶段获取的, 它们可以通过IdentityService进行管理

  • 如果没有显式指定设置的是用户还是群组,引擎会默认当做群组处理
  • 下面的设置与使用group(accountancy)一样:
<formalExpression>accountancy</formalExpression>
Activiti对任务分配的扩展
  • 当分配不复杂时,用户和组的设置非常麻烦.为避免复杂性,可以使用用户任务的自定义扩展
  • assignee属性: 直接把用户任务分配给指定用户(和使用humanPerformer 效果完全一样)
<userTask id="theTask" name="my task" activiti:assignee="kermit" />
  • candidateUsers属性: 为任务设置候选人(和使用potentialOwner效果完全一样,不需要像使用potentialOwner通过user(kermit)声明,这个属性只能用于人员)
<userTask id="theTask" name="my task" activiti:candidateUsers="kermit, gonzo" />
  • candidateGroups属性: 为任务设置候选组(和使用potentialOwner效果完全一样,不需要像使用potentialOwner通过group(management)声明,这个属性只能用于群组)
<userTask id="theTask" name="my task" activiti:candidateGroups="management, accountancy" />
  • candidateUsers和candidateGroups可以同时设置在同一个用户任务中
  • Activiti中虽然有账号管理组件和IdentityService ,账号组件不会检测设置的用户是否存在. Activiti允许与其他已存的账户管理方案集成
  • 使用创建事件的任务监听器 来实现自定义的分配逻辑:
<userTask id="task1" name="My task" >
  <extensionElements>
    <activiti:taskListener event="create" class="org.activiti.MyAssignmentHandler" />
  </extensionElements>
</userTask>
  • DelegateTask会传递给TaskListener的实现,通过它可以设置执行人,候选人和候选组
public class MyAssignmentHandler implements TaskListener {

  public void notify(DelegateTask delegateTask) {
    // Execute custom identity lookups here

    // and then for example call following methods:
    delegateTask.setAssignee("kermit");
    delegateTask.addCandidateUser("fozzie");
    delegateTask.addCandidateGroup("management");
    ...
  }

}
  • 使用spring时,使用表达式把任务监听器设置为spring代理的bean,让这个监听器监听任务的创建事件
  • 示例:执行者会通过调用ldapService这个spring bean的findManagerOfEmployee方法获得.流程变量emp会作为参数传递给bean
<userTask id="task" name="My Task" activiti:assignee="${ldapService.findManagerForEmployee(emp)}"/>
  • 可以用来设置候选人和候选组:
<userTask id="task" name="My Task" activiti:candidateUsers="${ldapService.findAllSales()}"/>
  • 方法返回类型只能为String(候选人)Collection < String >(候选组):
public class FakeLdapService {

  public String findManagerForEmployee(String employee) {
    return "Kermit The Frog";
  }

  public List<String> findAllSales() {
    return Arrays.asList("kermit", "gonzo", "fozzie");
  }

}

脚本任务

描述
  • 脚本任务是一个自动节点
  • 当流程到达脚本任务,会执行对应的脚本
图形标记
  • 脚本任务显示为标准BPMN 2.0任务(圆角矩形),左上角有一个脚本小图标
    在这里插入图片描述
XML内容
  • 脚本任务定义需要指定scriptscriptFormat
<scriptTask id="theScriptTask" name="Execute script" scriptFormat="groovy">
  <script>
    sum = 0
    for ( i in inputArray ) {
      sum += i
    }
  </script>
</scriptTask>

scriptFormat的值必须兼容JSR-223(java平台的脚本语言).默认Javascript会包含在JDK中,不需要额外的依赖.如果要使用其他的脚本引擎,必须要是JSR-223引擎兼容的.还需要把对应的jar添加到classpath下, 并使用合适的名称:activiti单元测试经常使用groovy

  • groovy脚本引擎放在groovy-all.jar中,在2.0版本之前,脚本引擎是groovy jar的一部分.使用需要添加依赖:
<dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>2.x.x<version>
</dependency>
脚本变量
  • 到达脚本任务的流程可以访问的所有流程变量,都可以在脚本中使用
<script>
    sum = 0
    for ( i in inputArray ) {
      sum += i
    }
</script>
  • 也可以在脚本中设置流程变量,直接调用execution.setVariable(“variableName”, variableValue)
    • 默认,不会自动保存变量(activiti 5.12之前)
    • 可以在脚本中自动保存任何变量,只要把scriptTaskautoStoreVariables属性设置为true
    • 最佳实践是不要使用,而是显式调用execution.setVariable()
<scriptTask id="script" scriptFormat="JavaScript" activiti:autoStoreVariables="false">

参数默认为false: 如果没有为脚本任务定义设置参数,所有声明的变量将只存在于脚本执行的阶段

  • 在脚本中设置变量: 这些命名已经被占用,不能用作变量名- out, out:print, lang:import, context, elcontext.
<script>
    def scriptVar = "test123"
    execution.setVariable("myVar", scriptVar)
</script>
脚本结果
  • 脚本任务的返回值可以通过制定流程变量的名称,分配给已存在或者一个新流程变量,需要使用脚本任务定义的’activiti:resultVariable’属性
  • 任何已存在的流程变量都会被脚本执行的结果覆盖
  • 如果没有指定返回的变量名,脚本的返回值会被忽略
<scriptTask id="theScriptTask" name="Execute script" scriptFormat="juel" activiti:resultVariable="myVar">
  <script>#{echo}</script>
</scriptTask>

脚本的结果-表达式 #{echo} 的值会在脚本完成后,设置到myVar变量中

Java服务任务

描述
  • Java服务任务用来调用外部Java类
图形标记
  • Java服务任务显示为圆角矩形,左上角有一个齿轮小图标
    在这里插入图片描述
XML内容
  • 声明Java调用逻辑有四种方式:
    • 实现JavaDelegate或者ActivityBehavior
    • 执行解析代理对象的表达式
    • 调用一个方法表达式
    • 调用一个值表达式
  • 执行一个在流程执行中调用的类,需要在activiti:class属性中设置全类名:
<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:class="org.activiti.MyJavaDelegate" />
  • 使用表达式调用一个对象,对象必须遵循一些规则,并使用activiti:delegateExpression属性进行创建:
<serviceTask id="serviceTask" activiti:delegateExpression="${delegateExpressionBean}" />

delegateExpressionBean是一个实现了JavaDelegate接口的bean,定义在实例的spring容器中
要执行指定的UEL方法表达式, 需要使用activiti:expression:

<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:expression="#{printer.printMessage()}" />

方法printMessage()会调用名为printer对象的方法

  • 为表达式中的方法传递参数:
<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:expression="#{printer.printMessage(execution, myVar)}" />

调用名为printer对象上的方法printMessage.第一个参数是DelegateExecution, 在表达式环境中默认名称为execution. 第二个参数传递的是当前流程的名为myVar的变量
要执行指定的UEL方法表达式, 需要使用activiti:expression:

<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:expression="#{split.ready}" />

ready属性的getter方法:getReady() 会作用于名为split的bean上.这个对象会被解析为流程对象spring环境中的对象

实现
  • 要在流程执行中实现一个调用的类,这个类需要实现org.activiti.engine.delegate.JavaDelegate接口,并在execute方法中提供对应的业务逻辑.当流程执行到特定阶段,会指定方法中定义好的业务逻辑,并按照默认BPMN 2.0中的方式离开节点
  • 示例:
    • 创建一个java类的例子,对流程变量中字符串转换为大写
    • 这个类需要实现org.activiti.engine.delegate.JavaDelegate接口,要求实现execute(DelegateExecution) 方法,包含的业务逻辑会被引擎调用
    • 流程实例信息:流程变量和其他信息,可以通过DelegateExecution接口访问和操作
public class ToUppercase implements JavaDelegate {

  public void execute(DelegateExecution execution) throws Exception {
    String var = (String) execution.getVariable("input");
    var = var.toUpperCase();
    execution.setVariable("input", var);
  }

}
  • serviceTask定义的class只会创建一个java类的实例
    • 所有流程实例都会共享相同的类实例,并调用execute(DelegateExecution)
    • 类不能使用任何成员变量,必须是线程安全的,必须能模拟在不同线程中执行.影响着属性注入的处理方式
  • 流程定义中引用的类(activiti:class)不会在部署时实例化
    • 只有当流程第一次执行到使用类的时候,类的实例才会被创建
    • 如果找不到类,会抛出一个ActivitiException
    • 这个原因是部署环境(更确切是的classpath)和真实环境往往是不同的:当使用ant或业务归档上传到Activiti Explorer来发布流程,classpath没有包含引用的类
  • 内部实现类也可以提供实现org.activiti.engine.impl.pvm.delegate.ActivityBehavior接口的类
    • 实现可以访问更强大的ActivityExecution,它可以影响流程的流向
    • 注意: 这应该尽量避免.只有在高级情况下并且确切知道要做什么的情况下,再使用ActivityBehavior接口
属性注入
  • 为代理类的属性注入数据. 支持如下类型的注入:
    • 固定的字符串
    • 表达式
  • 如果有效的话,数值会通过代理类的setter方法注入,遵循java bean的命名规范(比如fistName属性对应setFirstName(Xxx)方法)
  • 如果属性没有对应的setter方法,数值会直接注入到私有属性中
  • 一些环境的SecurityManager不允许修改私有属性,要把想注入的属性暴露出对应的setter方法来
  • 无论流程定义中的数据是什么类型,注入目标的属性类型都应该是 org.activiti.engine.delegate.Expression
  • 示例:
    • 把一个常量注入到属性中
    • 属性注入可以使用class属性
    • 在声明实际的属性注入之前,需要定义一个extensionElements的XML元素
<serviceTask id="javaService"
    name="Java service invocation"
    activiti:class="org.activiti.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
    <extensionElements>
      <activiti:field name="text" stringValue="Hello World" />
  </extensionElements>
</serviceTask>

ToUpperCaseFieldInjected类有一个text属性,类型是org.activiti.engine.delegate.Expression. 调用text.getValue(execution) 时,会返回定义的字符串Hello World

  • 可以使用长文字(比如,内嵌的email),使用activiti:string子元素:
<serviceTask id="javaService"
    name="Java service invocation"
    activiti:class="org.activiti.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
  <extensionElements>
    <activiti:field name="text">
        <activiti:string>
          Hello World
      </activiti:string>
    </activiti:field>
  </extensionElements>
</serviceTask>
  • 可以使用表达式,实现在运行期动态解析注入的值
  • 这些表达式可以使用流程变量spring定义的bean.
  • 服务任务中的java类实例会在所有流程实例中共享:
    • 为了动态注入属性的值,可以在org.activiti.engine.delegate.Expression中使用值和方法表达式
    • 会使用传递给execute方法的DelegateExecution参数进行解析
<serviceTask id="javaService" name="Java service invocation"
  activiti:class="org.activiti.examples.bpmn.servicetask.ReverseStringsFieldInjected">

  <extensionElements>
    <activiti:field name="text1">
      <activiti:expression>${genderBean.getGenderString(gender)}</activiti:expression>
    </activiti:field>
    <activiti:field name="text2">
       <activiti:expression>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</activiti:expression>
    </activiti:field>
  </ extensionElements>
</ serviceTask>
  • 示例:
    • 注入表达式,并使用在当前传入的DelegateExecution解析:
public class ReverseStringsFieldInjected implements JavaDelegate {

  private Expression text1;
  private Expression text2;

  public void execute(DelegateExecution execution) {
    String value1 = (String) text1.getValue(execution);
    execution.setVariable("var1", new StringBuffer(value1).reverse().toString());

    String value2 = (String) text2.getValue(execution);
    execution.setVariable("var2", new StringBuffer(value2).reverse().toString());
  }
}
  • 可以把表达式设置成一个属性,而不是子元素:
    • 因为java类实例会被重用,注入只会发生一次,当服务任务调用第一次的时候发生注入
    • 当代码中的属性改变了,值也不会重新注入,把它们看作是不变的,不用修改它们
服务任务结果
  • 服务流程返回的结果(使用表达式的服务任务)可以分配给已经存在的或新的流程变量
  • 通过指定服务任务定义的activiti:resultVariable属性来实现
    • 指定的流程变量会被服务流程的返回结果覆盖
    • 如果没有指定返回变量名,就会忽略返回结果
<serviceTask id="aMethodExpressionServiceTask"
    activiti:expression="#{myService.doSomething()}"
    activiti:resultVariable="myVar" />

服务流程的返回值(在myService上调用doSomething() 方法的返回值,myService可能是流程变量,也可能是spring的bean),在服务执行完成之后,会设置到名为myVar的流程变量里

处理异常

执行自定义逻辑时,常常需要捕获对应的业务异常,在流程内部进行处理

  • 抛出BPMN Errors:
    • 在服务任务或脚本任务的代码里抛出BPMN error:
      • 要从JavaDelegate,脚本,表达式和代理表达式中抛出名为BpmnError的特殊ActivitiExeption
      • 引擎会捕获这个异常,把它转发到对应的错误处理中:边界错误事件或错误事件子流程
public class ThrowBpmnErrorDelegate implements JavaDelegate {

  public void execute(DelegateExecution execution) throws Exception {
    try {
      executeBusinessLogic();
    } catch (BusinessException e) {
      throw new BpmnError("BusinessExceptionOccured");
    }
  }

}

构造参数是错误代码,会被用来决定哪个错误处理器会来响应这个错误
这个机制只用于业务失败,应该被流程定义中设置的边界错误事件或错误事件子流程处理. 技术上的错误应该使用其他异常类型,通常不会在流程里处理

  • 异常顺序流:
    内部实现类在一些异常发生时,让流程进入其他路径
<serviceTask id="javaService"
  name="Java service invocation"
  activiti:class="org.activiti.ThrowsExceptionBehavior">
</serviceTask>

<sequenceFlow id="no-exception" sourceRef="javaService" targetRef="theEnd" />
<sequenceFlow id="exception" sourceRef="javaService" targetRef="fixException" />

这里的服务任务有两个外出顺序流:分别叫exceptionno-exception. 异常出现时会使用顺序流的ID来决定流向

public class ThrowsExceptionBehavior implements ActivityBehavior {

  public void execute(ActivityExecution execution) throws Exception {
    String var = (String) execution.getVariable("var");

    PvmTransition transition = null;
    try {
      executeLogic(var);
      transition = execution.getActivity().findOutgoingTransition("no-exception");
    } catch (Exception e) {
      transition = execution.getActivity().findOutgoingTransition("exception");
    }
    execution.take(transition);
  }

}
JavaDelegate使用Activiti服务
  • 需要在Java服务任务中使用Activiti服务的场景: 比如,通过RuntimeService启动流程实例,而callActivity不满足需求
  • org.activiti.engine.delegate.DelegateExecution允许通过 org.activiti.engine.EngineServices接口直接获得这些服务:
public class StartProcessInstanceTestDelegate implements JavaDelegate {

  public void execute(DelegateExecution execution) throws Exception {
    RuntimeService runtimeService = execution.getEngineServices().getRuntimeService();
    runtimeService.startProcessInstanceByKey("myProcess");
  }

}
  • 所有activiti服务的API都可以通过这个接口获得
  • 使用这些API调用出现的所有数据改变,都是在当前事务中
  • 在例如spring和CDI这样的依赖注入环境也会起作用,无论是否启用了JTA数据源
  • 示例: 下面的代码功能与上面的代码一致,这是RuntimeService是通过依赖注入获得,而不是通过org.activiti.engine.EngineServices接口
@Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {

    @Autowired
    private RuntimeService runtimeService;

    public void startProcess() {
      runtimeService.startProcessInstanceByKey("oneTaskProcess");
    }

}
  • 因为服务调用是在当前事务里,数据的产生或改变,在服务任务执行完之前,还没有提交到数据库.所以API对于数据库数据的操作,意味着未提交的操作在服务任务的API调用中都是不可见的

WebService任务

描述
  • WebService任务可以用来同步调用一个外部的WebService
图形标记
  • WebService任务与Java服务任务显示效果一样(圆角矩形,左上角有一个齿轮小图标)
    在这里插入图片描述
XML内容
  • 要使用WebService需要导入操作和类型,可以使用import标签来指定WebService的WSDL
<import importType="http://schemas.xmlsoap.org/wsdl/"
        location="http://localhost:63081/counter?wsdl"
        namespace="http://webservice.activiti.org/" />

声明告诉activiti导入WSDL定义,但没有创建itemDefinitionmessage

  • 假设想调用一个名为prettyPrint的方法,必须创建为请求和响应信息对应的messageitemDefinition
<message id="prettyPrintCountRequestMessage" itemRef="tns:prettyPrintCountRequestItem" />
<message id="prettyPrintCountResponseMessage" itemRef="tns:prettyPrintCountResponseItem" />

<itemDefinition id="prettyPrintCountRequestItem" structureRef="counter:prettyPrintCount" />
<itemDefinition id="prettyPrintCountResponseItem" structureRef="counter:prettyPrintCountResponse" />
  • 在申请服务任务之前,必须定义实际引用WebService的BPMN接口和操作
  • 基本上,定义接口和必要的操作.对每个操作都会重用上面定义的信息作为输入和输出
  • 示例:
    • 定义了counter接口和prettyPrintCountOperation操作:
  <interface name="Counter Interface" implementationRef="counter:Counter">
        <operation id="prettyPrintCountOperation" name="prettyPrintCount Operation"
                        implementationRef="counter:prettyPrintCount">
                <inMessageRef>tns:prettyPrintCountRequestMessage</inMessageRef>
                <outMessageRef>tns:prettyPrintCountResponseMessage</outMessageRef>
        </operation>
</interface>

然后定义WebService任务,使用WebService实现,并引用WebService操作

<serviceTask id="webService"
        name="Web service invocation"
        implementation="##WebService"
        operationRef="tns:prettyPrintCountOperation">
WebService任务IO规范
  • 每个WebService任务可以定义任务的输入输出IO规范
<ioSpecification>
        <dataInput itemSubjectRef="tns:prettyPrintCountRequestItem" id="dataInputOfServiceTask" />
        <dataOutput itemSubjectRef="tns:prettyPrintCountResponseItem" id="dataOutputOfServiceTask" />
        <inputSet>
                <dataInputRefs>dataInputOfServiceTask</dataInputRefs>
        </inputSet>
        <outputSet>
                <dataOutputRefs>dataOutputOfServiceTask</dataOutputRefs>
        </outputSet>
</ioSpecification>
WebService任务数据输入关联
  • 指定数据输入关联有两种方式:
    • 使用表达式
    • 使用简化方式
  • 使用表达式指定数据输入关联: 需要定义来源和目的item,并指定每个item属性之间的对应关系:
<dataInputAssociation>
        <sourceRef>dataInputOfProcess</sourceRef>
        <targetRef>dataInputOfServiceTask</targetRef>
        <assignment>
                <from>${dataInputOfProcess.prefix}</from>
                <to>${dataInputOfServiceTask.prefix}</to>
        </assignment>
        <assignment>
                <from>${dataInputOfProcess.suffix}</from>
                <to>${dataInputOfServiceTask.suffix}</to>
        </assignment>
</dataInputAssociation>

分配item的前缀和后缀

  • 使用简化方式指定数据输入关联: sourceRef元素是activiti的变量名,targetRef元素是item定义的一个属性:
<dataInputAssociation>
        <sourceRef>PrefixVariable</sourceRef>
        <targetRef>prefix</targetRef>
</dataInputAssociation>
<dataInputAssociation>
        <sourceRef>SuffixVariable</sourceRef>
        <targetRef>suffix</targetRef>
</dataInputAssociation>

PrefixVariable变量的值分配给prefix属性,把SuffixVariable变量的值分配给suffix属性

WebService任务数据输出关联
  • 指定数据输出关联有两种方式:
    • 使用表达式
    • 使用简化方式
  • 使用表达式指定数据输出关联: 需要定义目的变量和来源表达式
<dataOutputAssociation>
        <targetRef>dataOutputOfProcess</targetRef>
        <transformation>${dataOutputOfServiceTask.prettyPrint}</transformation>
</dataOutputAssociation>

方法和数据输入关联完全一样

  • 使用简化方式指定数据输出关联: sourceRef元素是item定义的一个属性,targetRef元素是activiti的变量名
<dataOutputAssociation>
        <sourceRef>prettyPrint</sourceRef>
        <targetRef>OutputVariable</targetRef>
</dataOutputAssociation>

方法和数据输入关联完全一样

业务规则任务

描述
  • 业务规则任务用来同步执行一个或多个规则
  • Activiti使用drools规则引擎执行业务规则:
    • 包含业务规则的.drl文件必须和流程定义一起发布
    • 流程定义里包含了执行这些规则的业务规则任务
    • 流程使用的所有.drl文件都必须打包在流程BAR文件里
  • 如果想要自定义规则任务的实现: 想用不同方式使用drools,或者使用完全不同的规则引擎.你可以使用BusinessRuleTask上的class表达式属性
图形标记
  • 业务规则任务是一个圆角矩形,左上角使用一个表格小图标进行显示
    在这里插入图片描述
XML内容
  • 要执行部署流程定义的BAR文件中的一个或多个业务规则,需要定义输入和输出变量:
    • 对于输入变量定义,可以使用逗号分隔的一些流程变量
    • 输出变量定义只包含一个变量名,会把执行业务规则后返回的对象保存到对应的流程变量中
  • 注意: 结果变量会包含一个对象列表,如果没有指定输出变量名称,默认会使用 org.activiti.engine.rules.OUTPUT
<process id="simpleBusinessRuleProcess">

  <startEvent id="theStart" />
  <sequenceFlow sourceRef="theStart" targetRef="businessRuleTask" />

  <businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
      activiti:resultVariable="rulesOutput" />

  <sequenceFlow sourceRef="businessRuleTask" targetRef="theEnd" />

  <endEvent id="theEnd" />

</process>
  • 业务规则任务也可以配置成只执行部署的.drl文件中的一些规则.这时要设置逗号分隔的规则名,只会执行rule1和rule2:
<businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
      activiti:rules="rule1, rule2" />
  • 定义哪些规则不用执行:除了rule1和rule2以外,所有部署到流程定义同一个BAR文件中的规则都会执行:
<businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
      activiti:rules="rule1, rule2" exclude="true" />
  • 可以用一个选项修改BusinessRuleTask的实现:
<businessRuleTask id="businessRuleTask" activiti:class="${MyRuleServiceDelegate}" />

BusinessRuleTask的功能和ServiceTask一样,但是使用BusinessRuleTask的图标来表示 在这里要执行业务规则

邮件任务

  • Activiti强化了业务流程,支持自动邮件任务:
    • 可以发送邮件给一个或多个参与者,包括支持cc,bcc,HTML内容等等
    • 邮件任务不是BPMN 2.0规范定义的官方任务,Activiti中邮件任务是用专门的服务任务实现的
邮件服务器配置
  • Activiti引擎要通过支持SMTP功能的外部邮件服务器发送邮件
  • 为了实际发送邮件,引擎穾知道如何访问邮件服务器.下面的配置可以设置到activiti.cfg.xml配置文件中:
属性 是否必须 描述
mailServerHost 邮件服务器的主机名(比如:mail.mycorp.com).默认为localhost
mailServerPort
如果没有使用默认端口
邮件服务器上的SMTP传输端口.默认为25
mailServerDefaultFrom 如果用户没有指定发送邮件的邮件地址,默认设置的发送者的邮件地址。默认为[email protected]
mailServerUsername 如果服务器需要 一些邮件服务器需要认证才能发送邮件.默认不设置
mailServerPassword 如果服务器需要 一些邮件服务器需要认证才能发送邮件.默认不设置
mailServerUseSSL 如果服务器需要 一些邮件服务器需要ssl交互.默认为false
mailServerUseTLS 如果服务器需要 一些邮件服务器(比如gmail)需要支持TLS.默认为false
定义一个邮件任务
  • 邮件任务是一个专用的服务任务, 这个服务任务的type设置为mail
<serviceTask id="sendMail" activiti:type="mail">
  • 邮件任务是通过属性注入进行配置的.所有这些属性都可以使用EL表达式,可以在流程执行中解析. 下面的属性都可以设置:
属性 是否必须 描述
to 邮件的接受者.可以使用逗号分隔多个接受者
from 邮件发送者的地址.如果不提供,会使用默认配置的地址
subject 邮件的主题
cc 邮件抄送人.可以使用逗号分隔多个接收者
bcc 邮件暗送人.可以使用逗号分隔多个接收者
charset 可以修改邮件的字符集,对很多非英语语言是必须设置的
html 作为邮件内容的HTML
text 邮件的内容.,在需要使用原始文字(非富文本)的邮件时使用.可以与html一起使用,对于不支持富文本的邮件客户端.客户端会降级到仅显示文本的方式
htmlVar 使用对应的流程变量作为e-mail的内容.和html的不同之处是内容中包含的表达式会在mail任务发送之前被替换掉
textVar 使用对应的流程变量作为e-mail的纯文本内容.和text的不同之处是内容中包含的表达式会在mail任务发送之前被替换掉
ignoreException 处理邮件失败时,是否忽略异常,不抛出ActivitiException,默认为false
exceptionVariableName 当设置了ignoreException=true处理email时不抛出异常,可以指定一个变量名来存储失败信息
实例
  • 邮件任务的使用示例:
<serviceTask id="sendMail" activiti:type="mail">
  <extensionElements>
    <activiti:field name="from" stringValue="[email protected]" />
    <activiti:field name="to" expression="${recipient}" />
    <activiti:field name="subject" expression="Your order ${orderId} has been shipped" />
    <activiti:field name="html">
      <activiti: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>
        ]]>
      </activiti:expression>
    </activiti:field>
  </extensionElements>
</serviceTask>

在这里插入图片描述

Mule任务

  • Mule任务可以向Mule发送消息,用来强化Activiti的集成能力
  • Mule任务不是BPMN 2.0规范定义的官方任务,Activiti中Mule任务是用专门的服务任务实现的
定义Mule任务
  • Mule任务是一个专用的服务任务, 服务任务的type设置为mule
<serviceTask id="sendMule" activiti:type="mule">
  • Mule任务是通过属性注入进行配置的.属性使用EL表达式, 可以在流程执行中解析
属性 是否必须 描述
endpointUrl 需要调用的Mule终端
language 要使用解析荷载表达式(payloadExpression)属性的语言
payloadExpression 作为消息荷载的表达式
resultVariable 将要保存调用结果的变量名称
实例
  • Mule任务的使用示例:
 <extensionElements>
    <activiti:field name="endpointUrl">
      <activiti:string>vm://in</activiti:string>
    </activiti:field>
    <activiti:field name="language">
      <activiti:string>juel</activiti:string>
    </activiti:field>
    <activiti:field name="payloadExpression">
      <activiti:string>"hi"</activiti:string>
    </activiti:field>
    <activiti:field name="resultVariable">
      <activiti:string>theVariable</activiti:string>
    </activiti:field>
  </extensionElements>

Camel任务

  • Camel任务可以从Camel发送和接收消息,用来强化activiti的集成功能
  • Camel任务不是BPMN 2.0规范定义的官方任务,Camel任务时由专用的服务任务实现的
  • 使用Camel任务功能,要把Activiti Camel包含到项目中
定义Camel任务
  • Camel任务是一个专用的服务任务, 服务任务的type设置为camel
<serviceTask id="sendCamel" activiti:type="camel">
  • 流程定义只需要在服务任务中定义Camel类型
  • 集成逻辑都会代理给Camel容器
  • 默认Activiti引擎会在spring容器中查找camelContext bean.camelContext定义了camel容器加载的路由规则
  • 路由规则是既可以从指定的java包下加载, 也可以通过spring配置直接定义路由规则
<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
  <packageScan>
    <package>org.activiti.camel.route</package>
  </packageScan>
</camelContext>
  • 定义多个Camel环境bean,并且使用不同的bean名称. 可以重载CamelTask的定义
<serviceTask id="serviceTask1" activiti:type="camel">
        <extensionElements>
                <activiti:field name="camelContext" stringValue="customCamelContext" />
        </extensionElements>
</serviceTask>
Camel调用
  • 为了激活一个特定的Camel路由:
  • 需要一个Spring环境,包含SimpleCamelCallRoute的路由的类文件,放在packageScan标签的扫描目录下
<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
        <packageScan>
                <package>org.activiti.camel.examples.simpleCamelCall</package>
        </packageScan>
</camelContext>
  • 路由的定义:
public class SimpleCamelCallRoute extends RouteBuilder {

  @Override
  public void configure() throws Exception {

          from("activiti:SimpleCamelCallProcess:simpleCall").to("log: org.activiti.camel.examples.SimpleCamelCall");
  }
}

这个规则用于打印消息体

  • 终端的格式包含三部分:
    • 终端URL: 引用activiti终端
    • SimpleCamelCallProcess: 流程名
    • simpleCall: 流程中的Camel服务
  • 配置好规则后,可以让Camel进行使用.工作流如下:
<process id="SimpleCamelCallProcess">
        <startEvent id="start"/>
        <sequenceFlow id="flow1" sourceRef="start" targetRef="simpleCall"/>
                
        <serviceTask id="simpleCall" activiti:type="camel"/>
                
        <sequenceFlow id="flow2" sourceRef="simpleCall" targetRef="end"/>
        <endEvent id="end"/>
</process>

serviceTask部分,注明服务的类型是Camel, 目标规则名为simpleCall. 这与上面的Activiti终端相匹配.初始化流程后,会看到一个空的日志

乒乓实例
  • Camel和Activiti之间需要交互,向Camel发送和接收数据
  • 发送一个字符串,把变量里的消息发送给Camel,Camel进行一些处理,然后返回结果:
@Deployment
public void testPingPong() {
  Map<String, Object> variables = new HashMap<String, Object>();

  variables.put("input", "Hello");
  Map<String, String> outputMap = new HashMap<String, String>();
  variables.put("outputMap", outputMap);

  runtimeService.startProcessInstanceByKey("PingPongProcess", variables);
  assertEquals(1, outputMap.size());
  assertNotNull(outputMap.get("outputValue"));
  assertEquals("Hello World", outputMap.get("outputValue"));
}
  • 变量input是Camel规则的实际输入 ,outputMap会记录camel返回的结果
<process id="PingPongProcess">
  <startEvent id="start"/>
  <sequenceFlow id="flow1" sourceRef="start" targetRef="ping"/>
  <serviceTask id="ping" activiti:type="camel"/>
  <sequenceFlow id="flow2" sourceRef="ping" targetRef="saveOutput"/>
  <serviceTask id="saveOutput"  activiti:class="org.activiti.camel.examples.pingPong.SaveOutput" />
  <sequenceFlow id="flow3" sourceRef="saveOutput" targetRef="end"/>
  <endEvent id="end"/>
</process>
  • SaveOuput这个serviceTask, 会把Output变量的值从上下文保存到OutputMap
  • 变量提交给Camel的方法是由CamelBehavior控制的.配置一个期望的Camel操作:
<serviceTask id="serviceTask1" activiti:type="camel">
  <extensionElements>
    <activiti:field name="camelBehaviorClass" stringValue="org.activiti.camel.impl.CamelBehaviorCamelBodyImpl" />
  </extensionElements>
</serviceTask>
  • 如果特别指定一个行为,就会使用org.activiti.camel.impl.CamelBehaviorDefaultImpl. 这个行为会把变量复制成名称相同的Camel属性
  • 在返回时,无论选择什么行为,如果camel消息体是一个map,每个元素都会复制成一个变量.否则整个对象会复制到指定名称为camelBody的变量中
@Override
public void configure() throws Exception {
  from("activiti:PingPongProcess:ping").transform().simple("${property.input} World");
} 

在这个规则中,字符串world会被添加到input属性的后面,结果会写入消息体
这时可以检查javaServiceTask中的camelBody变量,复制到outputMap中,并在testcase进行判断

  • 在启动的所有camel规则中 ,流程实例ID会复制到Camel的名为PROCESS_ID_PROPERTY的属性中,后续可以用来关联流程实例和Camel规则,也可以在camel规则中直接使用
  • Activiti中可以使用三种不同Camel的行为: 可以通过在规则URL中指定来实现覆盖
from("activiti:asyncCamelProcess:serviceTaskAsync2?copyVariablesToProperties=true").

Activiti变量如何传递给camel:

行为 URL 描述
CamelBehaviorDefaultImpl copyVariablesToProperties 把Activiti变量复制为Camel属性
CamelBehaviorCamelBodyImpl copyCamelBodyToBody 只把名为"camelBody"Activiti变量复制成Camel的消息体
CamelBehaviorBodyAsMapImpl copyVariablesToBodyAsMap 把Activiti的所有变量复制到一个map里,作为Camel的消息体

Camel的变量如何返回给Activiti,只能配置在规则URL中:

URL 描述
默认 如果Camel消息体是一个map,把每个元素复制成Activiti的变量.否则把整个Camel消息体作为Activiti的camelBody变量
copyVariablesFromProperties 把Camel属性以相同名称复制为Activiti变量
copyCamelBodyToBodyAsString 和默认一样,但是如果camel消息体不是map时,先把它转换成字符串,再设置为camelBody
copyVariablesFromHeader 额外把Camel头部以相同名称复制成Activiti变量
异步乒乓实例
  • 同步的乒乓实例,流程会等到Camel规则返回之后才会停止
  • 某些情况下,需要Activiti工作流继续运行,就要使用camelServiceTask的异步功能
  • 通过设置camelServiceTaskasync属性来启用这个功能
<serviceTask id="serviceAsyncPing" activiti:type="camel" activiti:async="true"/>

Camel规则会被Activiti的jobExecutor异步执行
当在Camel规则中定义了一个队列,Activiti流程会在camelServiceTask执行时继续运行
camel规则以完全异步的方式执行

  • 可以使用一个receiveTask等待camelServiceTask的返回值,流程实例会等到接收一个来自camel的signal:
<receiveTask id="receiveAsyncPing" name="Wait State" />
  • 在Camel中可以发送一个signal给流程实例,通过对应的Activiti终端发送消息:
from("activiti:asyncPingProcess:serviceAsyncPing").to("activiti:asyncPingProcess:receiveAsyncPing");
  • 在Activiti终端中,会使用冒号分隔的三个部分:
    • 常量字符串activiti
    • 流程名称
    • 接收任务名
Camel规则中实例化工作流
  • 一般情况下,Activiti工作流会先启动,然后在流程中启动Camel规则
  • 在已经启动的Camel规则中启动一个工作流,会触发一个receiveTask
  • 十分类似,除了最后的部分.实例规则如下:
from("direct:start").to("activiti:camelProcess");

url有两个部分:

  • 常量字符串activiti
  • 流程的名称
    流程已经部署完成,并且是可以启动的

手工任务

描述
  • 手工任务定义了BPMN引擎外部的任务
  • 表示工作需要某人完成,而引擎不需要知道, 也没有对应的系统和UI接口
  • 对于BPMN引擎而言,手工任务是直接通过的活动,流程到达它之后会自动向下执行
图形标记
  • 手工任务显示为普通任务(圆角矩形),左上角是一个手型小图标
    在这里插入图片描述
XML内容
<manualTask id="myManualTask" name="Call client for more information" />

Java接收任务

描述
  • 接收任务是一个简单任务,会等待对应消息的到达
  • 当流程达到接收任务,流程状态会保存到存储里.意味着流程会等待在这个等待状态,直到引擎接收了一个特定的消息,触发流程穿过接收任务继续执行
图形标记
  • 接收任务显示为一个任务(圆角矩形),右上角有一个消息小标记.消息是白色的(黑色图标表示发送语义)
    在这里插入图片描述
XML内容
<receiveTask id="waitState" name="wait" /> 
  • 要在接收任务等待的流程实例继续执行,可以调用runtimeService.signal(executionId), 传递接收任务上流程的id:
ProcessInstance pi = runtimeService.startProcessInstanceByKey("receiveTask");
Execution execution = runtimeService.createExecutionQuery()
  .processInstanceId(pi.getId())
  .activityId("waitState")
  .singleResult();
assertNotNull(execution);

runtimeService.signal(execution.getId());

Shell任务

描述
  • Shell任务可以执行Shell脚本和命令
  • Shell任务不是BPMN 2.0规范定义的官方任务,没有对应的图标
定义Shell任务
  • Shell任务是一个专用的服务任务,这个服务任务的type设置为shell
<serviceTask id="shellEcho" activiti:type="shell">
  • Shell任务使用属性注入进行配置,所有属性都可以包含EL表达式, 会在流程执行过程中解析
属性 是否必须 类型 描述 默认值
command String 执行的shell命令
arg0-5 String 参数0至5
wait true/false 是否需要等待到shell进程结束 true
redirectError true/false 把标准错误打印到标准流中 false
cleanEnv true/false Shell进行不继承当前环境 false
outputVariable String 保存输出的变量名 不会记录输出结果
errorCodeVariable String 包含结果错误代码的变量名 不会注册错误级别
directory String Shell进程的默认目录 当前目录
应用实例
  • 执行shell脚本cmd /c echo EchoTest, 等到它结束,再把输出结果保存到resultVar中:
<serviceTask id="shellEcho" activiti:type="shell" >
  <extensionElements>
    <activiti:field name="command" stringValue="cmd" />
    <activiti:field name="arg1" stringValue="/c" />
    <activiti:field name="arg2" stringValue="echo" />
    <activiti:field name="arg3" stringValue="EchoTest" />
    <activiti:field name="wait" stringValue="true" />
    <activiti:field name="outputVariable" stringValue="resultVar" />
  </extensionElements>
</serviceTask>           

执行监听器

  • 执行监听器可以在流程定义中发生了某个事件时执行外部Java代码或执行表达式
  • 执行监听器可以捕获的事件有:
    • 流程实例的启动和结束
    • 选中一条连线
    • 节点的开始和结束
    • 网关的开始和结束
    • 中间事件的开始和结束
    • 开始时间结束或结束事件开始
  • 下面的流程定义定义了3个流程监听器:
 <process id="executionListenersProcess">

    <extensionElements>
      <activiti:executionListener class="org.activiti.examples.bpmn.executionlistener.ExampleExecutionListenerOne" event="start" />
    </extensionElements>

    <startEvent id="theStart" />
    <sequenceFlow sourceRef="theStart" targetRef="firstTask" />

    <userTask id="firstTask" />
    <sequenceFlow sourceRef="firstTask" targetRef="secondTask">
    <extensionElements>
      <activiti:executionListener class="org.activiti.examples.bpmn.executionListener.ExampleExecutionListenerTwo" />
    </extensionElements>
    </sequenceFlow>

    <userTask id="secondTask" >
    <extensionElements>
      <activiti:executionListener expression="${myPojo.myMethod(execution.event)}" event="end" />
    </extensionElements>
    </userTask>
    <sequenceFlow sourceRef="secondTask" targetRef="thirdTask" />

    <userTask id="thirdTask" />
    <sequenceFlow sourceRef="thirdTask" targetRef="theEnd" />

    <endEvent id="theEnd" />

  </process>
  • 第一个流程监听器监听流程开始. 监听器是一个外部Java类(例如ExampleExecutionListenerOne),需要实现org.activiti.engine.delegate.ExecutionListener接口.当事件发生时(end事件),会调用notify(ExecutionListenerExecution execution) 方法:
public class ExampleExecutionListenerOne implements ExecutionListener {

  public void notify(ExecutionListenerExecution execution) throws Exception {
    execution.setVariable("variableSetInExecutionListener", "firstValue");
    execution.setVariable("eventReceived", execution.getEventName());
  }
}

也可以使用实现org.activiti.engine.delegate.JavaDelegate接口的代理类(代理类可以在结构中重用,比如serviceTask的代理)

  • 第二个流程监听器在连线执行时调用. 注意这个listener元素不能定义event, 因为连线只能触发take事件,为连线定义的监听器的event属性会被忽略
  • 第三个流程监听器在节点secondTask结束时调用. 使用expression代替class来在事件触发时执行或调用
<activiti:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />

流程变量可以处理和使用
流程实现对象有一个保存事件名称的属性,在方法中使用execution.eventName获的事件名称

  • 流程监听器也支持delegateExpression, 和服务任务相同
<activiti:executionListener event="start" delegateExpression="${myExecutionListenerBean}" />
  • 脚本流程监听器org.activiti.engine.impl.bpmn.listener.ScriptExecutionListener可以为某个流程监听事件执行一段脚本
<activiti:executionListener event="start" class="org.activiti.engine.impl.bpmn.listener.ScriptExecutionListener" >
  <activiti:field name="script">
    <activiti:string>
      def bar = "BAR";  // local variable
      foo = "FOO"; // pushes variable to execution context
      execution.setVariable("var1", "test"); // test access to execution instance
      bar // implicit return value
    </activiti:string>
  </activiti:field>
  <activiti:field name="language" stringValue="groovy" />
  <activiti:field name="resultVariable" stringValue="myVar" />
<activiti:executionListener>
流程监听器的属性注入
  • 流程监听器时,可以配置class属性,使用属性注入.这和使用服务任务属性注入相同
  • 使用属性注入的流程监听器的流程示例:
<process id="executionListenersProcess">
    <extensionElements>
      <activiti:executionListener class="org.activiti.examples.bpmn.executionListener.ExampleFieldInjectedExecutionListener" event="start">
        <activiti:field name="fixedValue" stringValue="Yes, I am " />
        <activiti:field name="dynamicValue" expression="${myVar}" />
      </activiti:executionListener>
    </extensionElements>

    <startEvent id="theStart" />
    <sequenceFlow sourceRef="theStart" targetRef="firstTask" />

    <userTask id="firstTask" />
    <sequenceFlow sourceRef="firstTask" targetRef="theEnd" />

    <endEvent id="theEnd" />
  </process>     
public class ExampleFieldInjectedExecutionListener implements ExecutionListener {

  private Expression fixedValue;

  private Expression dynamicValue;

  public void notify(ExecutionListenerExecution execution) throws Exception {
    execution.setVariable("var", fixedValue.getValue(execution).toString() + dynamicValue.getValue(execution).toString());
  }
}

ExampleFieldInjectedExecutionListener类串联了两个注入的属性(一个是固定的,一个是动态的),把他们保存到流程变量var

@Deployment(resources = {"org/activiti/examples/bpmn/executionListener/ExecutionListenersFieldInjectionProcess.bpmn20.xml"})
public void testExecutionListenerFieldInjection() {
  Map<String, Object> variables = new HashMap<String, Object>();
  variables.put("myVar", "listening!");

  ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("executionListenersProcess", variables);

  Object varSetByListener = runtimeService.getVariable(processInstance.getId(), "var");
  assertNotNull(varSetByListener);
  assertTrue(varSetByListener instanceof String);

  // Result is a concatenation of fixed injected field and injected expression
  assertEquals("Yes, I am listening!", varSetByListener);
}

任务监听器

  • 任务监听器可以在发生对应的任务相关事件时执行自定义Java逻辑或表达式
  • 任务监听器只能添加到流程定义中的用户任务中. 必须定义在BPMN 2.0 extensionElements的子元素中,并使用activiti命名空间, 因为任务监听器是activiti独有的结构
<userTask id="myTask" name="My Task" >
  <extensionElements>
    <activiti:taskListener event="create" class="org.activiti.MyTaskCreateListener" />
  </extensionElements>
</userTask>
  • 任务监听器支持的属性:
    • event(必选): 任务监听器会被调用的任务类型
      • create: 任务创建并设置所有属性后触发
      • assignment: 任务分配给一些人时触发.当流程到达userTask,assignment事件会在create事件之前发生(当获得create时间时,我们想获得任务的所有属性,包括执行人)
      • complete: 当任务完成,并尚未从运行数据中删除时触发
      • delete: 只在任务删除之前发生,在通过completeTask正常完成时,也会执行
    • class: 必须调用的代理类, 类要实现org.activiti.engine.delegate.TaskListener接口
       public class MyTaskCreateListener implements TaskListener {
      
      public void notify(DelegateTask delegateTask) {
      // Custom logic goes here
      }
      
      }
      
    可以使用属性注入把流程变量或执行传递给代理类
    代理类的实例是在部署时创建的,所有流程实例都会共享同一个实例
    • expression: 指定事件发生时执行的表达式.无法同时与class属性一起使用. 可以把DelegateTask对象和事件名称(task.eventName)作为参数传递给调用的对象
      <activiti:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
      
    • delegateExpression: 指定一个表达式,解析一个实现了TaskListener接口的对象,与服务任务一致
       <activiti:taskListener event="create" delegateExpression="${myTaskListenerBean}" />
      
  • 脚本任务监听器org.activiti.engine.impl.bpmn.listener.ScriptTaskListener可以为任务监听器事件执行脚本
<activiti:taskListener event="complete" class="org.activiti.engine.impl.bpmn.listener.ScriptTaskListener" >
  <activiti:field name="script">
    <activiti: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
    </activiti:string>
  </activiti:field>
  <activiti:field name="language" stringValue="groovy" />
  <activiti:field name="resultVariable" stringValue="myVar" />
<activiti:taskListener>

多实例(循环)

描述
  • 多实例节点是在业务流程中定义重复环节的方法
  • 多实例和循环是一样的:它可以根据给定的集合,为每个元素执行一个环节甚至一个完整的子流程,既可以顺序依次执行也可以并发同步执行
  • 多实例是在一个普通的节点上添加了额外的属性定义(所以叫做’多实例特性),这样运行时节点就会执行多次
  • 可以设置成多实例节点的节点:
    • User Task
    • Script Task
    • Java Service Task
    • WebService Task
    • Business Rule Task
    • Email Task
    • Manual Task
    • Receive Task
    • (Embedded) Sub-Process [(嵌入子)流程]
    • Call Activity [调用子流程]
  • 网关和事件不能设置多实例
  • 每个上级流程为每个实例创建分支时都要提供下列变量:
    • nrOfInstances: 实例总数
    • nrOfActiveInstances: 当前活动,还没完成的实例数量. 顺序执行的多实例,值一直为1
    • nrOfCompletedInstances: 已经完成实例的数目
  • 通过execution.getVariable(Xx) 方法获得这些变量
  • 每个创建的分支都会有分支级别的本地变量(例如其他实例不可见,不会保存到流程实例级别):
    • loopCounter- 特定实例的在循环的索引值
    • 使用activitielementIndexVariable属性修改loopCounter的变量名
图形标记
  • 多实例的节点,会在节点底部显示三条短线.三条竖线表示实例会并行执行. 三条横线表示顺序执行
    在这里插入图片描述

XML内容

  • 要把一个节点设置为多实例,节点xml元素必须设置一个multiInstanceLoopCharacteristics子元素
<multiInstanceLoopCharacteristics isSequential="false|true">
 ...
</multiInstanceLoopCharacteristics>

isSequential属性表示节点是进行顺序执行还是并行执行

  • 实例的数量会在进入节点时计算一次:
    • 一种方法是使用loopCardinality子元素
      可以使用loopCardinality子元素中直接指定一个数字
      <multiInstanceLoopCharacteristics isSequential="false|true">
         <loopCardinality>5</loopCardinality>
      </multiInstanceLoopCharacteristics>
      
      也可以使用loopCardinality子元素中结果为整数的表达式
      <multiInstanceLoopCharacteristics isSequential="false|true">
         <loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
      </multiInstanceLoopCharacteristics>
      
    • 另一个方法是通过loopDataInputRef子元素,设置一个类型为集合的流程变量名.对于集合中的每个元素,都会创建一个实例.也可以通过inputDataItem子元素指定集合
      <userTask id="miTasks" name="My Task ${loopCounter}" activiti:assignee="${assignee}">
      	<multiInstanceLoopCharacteristics isSequential="false">
      		 <loopDataInputRef>assigneeList</loopDataInputRef>
               <inputDataItem name="assignee" />
          </multiInstanceLoopCharacteristics>
      </userTask>
      
      假设assigneeList变量包含这些值[kermit, gonzo, foziee],三个用户任务会同时创建.每个分支都会拥有一个用名为assignee的流程变量,这个变量会包含集合中的对应元素,上面是用来设置用户任务的分配者
  • loopDataInputRefinputDataItem的缺点:
    • 名字难于记忆
    • 根据BPMN 2.0格式定义 ,不能包含表达式
  • 在activiti中可以在multiInstanceCharacteristics中设置collectionelementVariable
<userTask id="miTasks" name="My Task" activiti:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="true"
     activiti:collection="${myService.resolveUsersForTask()}" activiti:elementVariable="assignee" >
  </multiInstanceLoopCharacteristics>
</userTask>
  • 多实例节点在所有实例都完成时才会结束
  • 可以指定一个表达式在每个实例结束时执行,如果表达式返回true,所有其它的实例都会销毁,多实例节点也会结束.流程会继续执行. 表达式必须定义在completionCondition子元素中
<userTask id="miTasks" name="My Task" activiti:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="false"
     activiti:collection="assigneeList" activiti:elementVariable="assignee" >
    <completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

assigneeList集合的每个元素都会创建一个并行的实例,当60%的任务完成时,其他任务就会删除,流程继续执行

边界事件和多实例
  • 多实例是一个普通节点,可以在边缘使用边界事件
  • 对于中断型边界事件,当捕获事件时,所有激活的实例都会销毁
    -
    子流程的所有实例都会在定时器触发时销毁,无论有多少实例,也不论内部节点没有完成

补偿处理器

描述
  • 如果一个节点用来补偿另一个节点的业务, 可以声明为一个补偿处理器
  • 补偿处理器不包含普通的流,只在补偿事件触发时执行
  • 补偿处理器不能包含进入和外出顺序流
  • 补偿处理器必须使用直接关联分配给一个补偿边界事件
图形标记
  • 节点是补偿处理器,补偿事件图标会显示在中间底部区域
  • 补偿处理器图形示例:一个服务任务,附加了一个补偿边界事件,并分配了一个补偿处理器.注意cancel hotel reservation服务任务中间底部区域显示的补偿处理器图标
    在这里插入图片描述
XML内容
  • 声明作为补偿处理器的节点,需要把isForCompensation设置为true:
<serviceTask id="undoBookHotel" isForCompensation="true" activiti:class="...">
</serviceTask>

子流程

子流程

描述
  • 子流程(Sub-process)是一个包含其他节点,网关,事件等等的节点
  • 本身就是一个流程,同时是更大流程的一部分.子流程是完全定义在父流程里的,所以叫做内嵌子流程
  • 子流程的两种主要场景:
    • 子流程可以使用继承式建模: 很多建模工具的子流程可以折叠,把子流程的内部细节隐藏,显示一个高级别的端对端的业务流程总览
    • 子流程会创建一个新的事件作用域: 子流程运行过程中抛出的事件,可以被子流程边缘定义的边界事件捕获,就可以创建一个仅限于这个子流程的事件作用范围
  • 使用子流程的限制:
    • 子流程只能包含一个空开始事件, 不能使用其他类型的开始事件,子路程必须至少有一个结束节点
    • 顺序流不能跨越子流程的边界
图形标记
  • 子流程显示为标准的节点(圆角矩形),下面子流程是折叠的,只显示名称和一个加号标记,展示了高级别的流程总览:
    在这里插入图片描述
  • 下面子流程是展开的,子流程的步骤都显示在子流程边界内:
    在这里插入图片描述
  • 使用子流程主要是为了定义对应事件的作用域
  • 示例:
    • 调查软件/调查引荐任务需要同步执行,两个任务需要在同时完成,在二线支持解决之前.这里,定时器的作用域(比如节点需要及时完成)是由子流程限制的:

在这里插入图片描述

XML内容
  • 子流程定义为subprocess元素.所有节点,网关,事件,等等.是子流程的一部分,都需要放在这个元素里
<subProcess id="subProcess">

  <startEvent id="subProcessStart" />

  ... other Sub-Process elements ...

  <endEvent id="subProcessEnd" />

 </subProcess>

事件子流程

描述
  • 事件子流程是由事件触发的子流程.是BPMN 2.0中的新元素
  • 事件子流程可以添加到流程级别或任意子流程级别
  • 用于触发事件子流程的事件是使用开始事件配置的,所以事件子流程是不支持空开始事件的
  • 事件子流程可以被消息事件,错误事件,信号事件,定时器事件,或补偿事件触发.开始事件的订阅在包含事件子流程的作用域(流程实例或子流程)创建时就会创建.当作用域销毁也会删除订阅。
  • 事件子流程可以是中断的或非中断的
    • 一个中断的子流程会取消当前作用域内的所有流程
    • 非中断事件子流程会创建一个新的同步分支
    • 中断事件子流程只会被每个激活状态的宿主触发一次
    • 非中断事件子流程可以触发多次
    • 子流程是否是中断的,使用事件子流程的开始事件配置
  • 事件子流程不能有任何进入和外出流程
    • 当事件触发一个事件子流程时,输入顺序流是没有意义的
    • 当事件子流程结束时,无论当前作用域已经结束(中断事件子流程的情况或为非中断,子流程生成同步分支会结束
  • 事件子流程的限制:
    • Activiti只支持中断事件子流程
    • Activiti只支持使用错误开始事件或消息开始事件的事件子流程
图像标记
  • 事件子流程可以显示为边框为虚线的内嵌子流程
    在这里插入图片描述
XML内容
  • 事件子流程的XML内容与内嵌子流程一样,但是要把triggeredByEvent属性设置为true
<subProcess id="eventSubProcess" triggeredByEvent="true">
        ...
</subProcess>
实例
  • 使用错误开始事件触发的事件子流程的实例,事件子流程是放在流程级别的,作用于流程实例
    在这里插入图片描述
    事件子流程的XML:
<subProcess id="eventSubProcess" triggeredByEvent="true">
        <startEvent id="catchError">
                <errorEventDefinition errorRef="error" />
        </startEvent>
        <sequenceFlow id="flow2" sourceRef="catchError" targetRef="taskAfterErrorCatch" />
        <userTask id="taskAfterErrorCatch" name="Provide additional data" />
</subProcess>

事件子流程也可以添加成内嵌子流程.如果添加为内嵌子流程,其实是边界事件的一种替代方案

  • 示例:
    • 下面两个流程图,两种情况内嵌子流程会抛出一个错误事件,两种情况错误都会被捕获并使用一个用户任务处理
      在这里插入图片描述
      相对于
      在这里插入图片描述
    • 两种场景都会执行相同的任务,但是两种建模的方式是不同的:
      • 内嵌子流程是使用与执行作用域宿主相同的流程执行的:
        • 意思是内嵌子流程可以访问它作用域内的内部变量
        • 当使用边界事件时,执行内嵌子流程的流程会删除, 并生成一个流程根据边界事件的顺序流继续执行,这意味着内嵌子流程创建的变量不再起作用
      • 当使用事件子流程时,事件是完全由它添加的子流程处理的.
        • 当使用边界事件时,事件由父流程处理
  • 这两个不同点可以帮助决定是使用边界事件(内嵌子流程)还是内嵌事件子流程(事件子流程) 来解决特定的流程建模或者实现问题

事务子流程

描述
  • 事务子流程是内嵌子流程, 可以用来把多个流程放到一个事务里
  • 事务是一个逻辑单元, 可以把一些单独的节点放在一起, 这样它们就可以一起成功或一起失败
  • 事务的可能结果有三种:
    • 事务成功,没有取消也没有因为问题终结
      • 如果事务子流程是成功的,就会使用外出顺序流继续执行
      • 如果流程后来抛出了一个补偿事件,成功的事务可能被补偿
      • 和普通内嵌子流程一样,事务可能在成功后,使用中间补偿事件进行补偿
    • 事务取消,流程到达取消结束事件
      • 所有流程都会终结和删除,触发补偿的一个单独的流程,会通过取消边界事件继续执行
      • 在补偿完成之后,事务子流程会使用取消边界事务的外出顺序流向下执行
    • 事务被问题结束,抛出一个错误事件而且没有在事务子流程中捕获(如果错误被事务子流程的边界事件处理了,也会这样应用)
      • 不会执行补偿
  • 事务三种不同的结果:
    在这里插入图片描述
  • BPMN事务与ACID(技术)事务的关系: BPMN事务子流程与技术(ACID)事务不能互相混淆,BPMN事务子流程不是技术(ACID)事务领域的
  • BPMN事务和技术事务有以下不同点:
    • ACID事务一般是短期的.BPMN事务可能持续几小时,几天,甚至几个月才能完成:
      • 考虑事务中包含的节点可能有用户任务,一般人员响应的时间比应用时间要长
      • 在其他情况下,bpmn事务可能要等待发生一些事务事件,例如要根据某种次序执行
      • 这种操作通常要相比更新数据库的一条数据,或把一条信息保存到事务性队列中,消耗更长的时间来完成
    • BPMN事务一般要跨越多个ACID事务,因为不能在整个业务节点的过程中保持一个技术性的事务
    • BPMN事务会跨越多个ACID事务,所以会丧失ACID的特性:
      • 比如,在上述例子中,假设预订旅店和刷信用卡操作在单独的ACID事务中执行,假设预定旅店节点已经成功了
      • 现在处于一个中间不稳定状态,因为我们预定了酒店,但是还没有刷信用卡
      • 在一个ACID事务中,要依次执行不同的操作,也会有一个中间不稳定状态
      • 不同的是,这个中间状态对事务的外部是可见的.比如,如果通过外部预定服务进行了预定,其他使用相同预定服务的部分就可以看到旅店被预定了.这意味着实现业务事务时,我们完全失去了隔离属性(放弃隔离性,可以为ACID事务获得更高的并发,是可以完全控制,中间不稳定状态也只持续很短的时间)
    • BPMN业务事务也不能使用通常的方式回滚:
      • BPMN事务跨越了多个事务,BPMN事务取消时一些ACID事务可能已经提交了.这时不能被回滚
  • BPMN事务运行时间很长,缺乏隔离性和回滚机制都需要被区别对待:
    • 使用补偿执行回滚:
      • 如果事务范围抛出了取消事件,会影响已经执行成功的节点,并使用补偿处理器执行补偿
    • 隔离性的缺乏通常使用特定领域的解决方法来解决:
      • 在上面的例子中,一个旅店房间可能会展示给第二个客户,在我们确认第一个客户付费之前.虽然这可能与业务预期不符,预定服务可能选择允许一些过度的预约
    • 事务会因为风险而中断,服务必须处理这种情况:
      • 已经预定了旅店,但是一直没有付款的情况(因为事务被中断了),这时预定服务需要选择一个策略,在旅店房间预定超过最大允许时间后,如果还没有付款,预定就会取消
  • 综上所述,ACID处理的是通常问题:回滚,隔离级别和启发式结果,在实现业务事务时,需要找到特定领域的解决方案来处理这些问题
  • BPMN事务目前的限制:
    • BPMN规范要求流程引擎能根据底层事务的协议处理事件:
      • 比如如果底层协议触发了取消事件,事务就会取消
  • ACID事务顶层的一致性和优化并发:
    • BPMN事务保证一致性:
      • 要么所有节点都成功
      • 一些节点成功,对其他成功的节点进行补偿
      • 无论哪种方式,都会有一致性的结果
      • 要讨论一些activiti内部的情况BPMN事务的一致性模型是叠加在流程的一致性模型之上的
    • Activiti执行流程是事务性的,并发使用了乐观锁.在Activiti中,BPMN错误,取消和补偿事件都建立在同样的ACID事务与乐观锁之上:
      • 取消结束事件只能触发它实际到达的补偿
      • 如果之前服务任务抛出了未声明的异常
      • 补偿处理器的效果无法提交,如果底层的acid事务的参与者把事务设置成必须回滚.
      • 当两个并发流程到达了取消结束事件
      • 可能会触发两次补偿,并因为乐观锁异常失败
      • 说明Activiti中实现BPMN事务时,相同的规则也作用域普通的流程和子流程
      • 为了保证一致性,重要的是使用一种方式考虑实现乐观事务性的执行模型
图形标记
  • 事务子流程显示为内嵌子流程, 使用双线边框
    在这里插入图片描述
XML内容
  • 事务子流程使用transaction标签
<transaction id="myTransaction" >
        ...
</transaction>
实例

在这里插入图片描述

调用活动(子流程)

描述
  • BPMN 2.0区分了普通子流程(内嵌子流程)和调用节点:
    • 相同点: 当流程抵达节点时两者都会调用子流程
    • 不同点:
      • 调用节点引用流程定义外部的一个流程
      • 子流程会内嵌到原始的流程定义中
      • 使用调用节点的主要场景: 需要重用流程定义,这个流程定义需要被很多其他流程定义调用
  • 当流程执行到调用节点,会创建一个新分支,是到达调用节点的流程的分支
  • 这个分支会用来执行子流程,默认创建并行子流程,就像一个普通的流程
  • 上级流程会等待子流程完成,然后才会继续向下执行
图形标记
  • 调用节点显示与子流程相同,但是粗边框(无论是折叠和展开的). 根据不同的建模工具,调用节点也可以展开,但是显示为折叠的子流程
    在这里插入图片描述
XML内容
<callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
  • 子流程的流程定义是在执行阶段解析的,就是说子流程可以与调用的流程分开部署
传递变量
  • 可以把流程变量传递给子流程,反之亦然: 当它启动的时候, 数据会复制给子流程,并在它结束的时候复制回主流程
<callActivity id="callSubProcess" calledElement="checkCreditProcess" >
  <extensionElements>
          <activiti:in source="someVariableInMainProcess" target="nameOfVariableInSubProcess" />
          <activiti:out source="someVariableInSubProcss" target="nameOfVariableInMainProcess" />
  </extensionElements>
</callActivity>

这里使用Activiti扩展来简化BPMN标准元素调用dataInputAssociationdataOutputAssociation, 只在使用BPMN 2.0标准方式声明流程变量有效

  • 也可以使用表达式:
<callActivity id="callSubProcess" calledElement="checkCreditProcess" >
        <extensionElements>
          <activiti:in sourceExpression="${x+5}"" target="y" />
          <activiti:out source="${y+5}" target="z" />
        </extensionElements>
</callActivity>
z = y + 5 = x + 5 + 5
实例
  • 订单处理流程图:先判断客户端信用,检查信用阶段设计成调用节点
    在这里插入图片描述
  • 流程XML:
<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="receiveOrder" />

<manualTask id="receiveOrder" name="Receive Order" />
<sequenceFlow id="flow2" sourceRef="receiveOrder" targetRef="callCheckCreditProcess" />

<callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
<sequenceFlow id="flow3" sourceRef="callCheckCreditProcess" targetRef="prepareAndShipTask" />

<userTask id="prepareAndShipTask" name="Prepare and Ship" />
<sequenceFlow id="flow4" sourceRef="prepareAndShipTask" targetRef="end" />

<endEvent id="end" />
  • 子流程:
    在这里插入图片描述

事务和并发

异步操作

  • Activiti通过事务方式执行流程,可以根据需求定制
  • Activiti处理事务:
    • 如果触发了Activiti的操作(开始流程,完成任务,触发流程继续执行),activiti会推进流程,直到每个分支都进入等待状态
    • 抽象的说,会从流程图执行深度优先搜索,如果每个分支都遇到等待状态,就会返回
    • 等待状态是稍后需要执行任务,Activiti会把当前状态保存到数据库中,然后等待下一次触发
    • 触发可能来自外部,比如用户任务或接收到一个消息,也可能来自Activiti本身(定时器事件)
      在这里插入图片描述
      流程包含用户任务,服务任务和定时器事件
      完成用户任务和校验地址是在同一个工作单元中,两者的成功和失败是原子性的.意味着如果服务任务抛出异常,要回滚当前事务,这样流程会退回到用户任务,用户任务就依然在数据库里
      这就是activiti默认的行为.在(1)中应用或客户端线程完成任务.这会执行服务,流程推进,直到遇到一个等待状态,就是定时器(2),然后它会返回给调用者(3),并提交事务(如果事务是由Activiti开启的)
  • 有时需要自定义控制流程中事务的边界,把业务逻辑包裹在一起.这就需要使用异步执行:
    在这里插入图片描述
    完成了用户任务,生成一个发票,把发票发送给客户
    生成发票不在同一个工作单元内了.如果生成发票出错不需要对用户任务进行回滚
    Activiti实现的是完成用户任务(1),提交事务,返回给调用者应用.然后在后台的线程中,异步执行生成发票.
    后台线程就是Activiti的Job执行器(一个线程池)周期对数据库的Job进行扫描:当到达"generate invoice"任务,为Activiti创建一个稍后执行的Job"消息",并保存到数据库.Job会被Job执行器获取并执行.也会给本地Job执行器一个提醒,告诉有一个新Job,来增加性能
  • 要想使用这个特性,要使用activiti:async=“true” 扩展
<serviceTask id="service1" name="Generate Invoice" activiti:class="my.custom.Delegate" activiti:async="true" />
  • activiti:async可以使用到以下BPMN任务类型中:
    • task
    • serviceTask,
    • scriptTask
    • businessRuleTask
    • sendTask
    • receiveTask
    • userTask
    • subProcess
    • callActivity
  • 对于userTask,receiveTask和其他等待状态,异步执行的作用是让开始流程监听器运行在一个单独的线程或者事务中

排他任务

  • 从Activiti 5.9开始 ,JobExecutor能保证同一个流程实例中的Job不会并发执行
排他任务的产生背景

在这里插入图片描述

  • 一个并行网关,后面有三个服务任务,都设置为异步执行:
    • 这样会添加三个job到数据库里.一旦job进入数据库,就可以被jobExecutor执行了.JobExecutor会获取job,代理到工作线程的线程池中,在那里真正执行job
    • 就是说,使用异步执行,可以把任务分配给这个线程池(在集群环境,可能会使用多个线程池)
    • 产生一致性问题:
      • 考虑一下服务任务后的汇聚:当服务任务完成后,到达并发汇聚节点,需要决定是等待其他分支,还是继续向下执行
      • 就是说,对每个到达并行汇聚的分支,都需要判断是继续还是等待其他分支的一个或多个分支
  • 为什么会产生这样的问题:
    • 因为服务任务配置成使用异步执行,可能相关的job都在同一时间被获取,被JobExecutor分配给不同的工作线程执行
    • 结果是,三个单独的服务执行使用的事务在到达并发汇聚时可能重叠:
      • 如果出现了这个问题,这些事务是互相不可见的,其他事务同时到达了相同的并发汇聚,假设都在等待其他分支
      • 然而,每个事务都假设在等待其他分支,所以没有分支会越过并发汇聚继续执行,流程实例会一直在等待状态,无法继续执行
  • Activiti解决这个问题方式:
    • Activiti使用了乐观锁:
      • 当基于判断的数据看起来不是最新的时候 (因为其他事务可能在提交之前进行了修改,会在每个事务里增加数据库同一行的版本),这个时候,第一个提交的事务会成功,其他会因为乐观锁异常导致失败
      • 这就解决了上面流程的问题:
        • 如果多个分支同步到达并行汇聚,会假设都在登录,并增加父流程的版本号(流程实例)然后尝试提交
        • 第一个分支会成功提交,其他分支会因为乐观锁导致失败
        • 因为流程是被job触发的,Activiti会尝试在等待一段时间后尝试执行同一个job,这段时间可以同步网关的状态
  • Activiti乐观锁是一个很好的解决方案吗?
    • 乐观锁允许Activiti避免非一致性,确定流程不会"堵在汇聚网关": 或者所有分支都通过网关,或者数据库中的job正在尝试通过
    • 虽然这是一个对于持久性和一致性的完美解决方案,但对于上层来说不一定是期望的行为:
      • Activiti只会对同一个job重试估计次数(默认配置为3).之后,job还会在数据库里,但是不会再重试了.意味着这个操作必须手工执行job的触发
      • 如果job有非事务方面的效果,不会因为失败的事务回滚:如果“预定演唱会门票”服务没有与Activiti共享事务,重试job可能导致我们预定了过多门票
  • 针对这些问题,在Activiti中推出了新的概念:排他job
排他Job
  • 对于一个流程实例,排他任务不能同时执行两个
  • 考虑上面的流程:如果我们把服务任务申请为排他任务,JobExecutor会保证对应的job不会并发执行.
  • 会保证无论什么时候获取一个流程实例的排他任务,都会把同一个流程实例的其他任务都取出来,放在同一个工作线程中执行.保证job是顺序执行的
  • 从activiti 5.9开始,排他任务已经是默认配置.所以异步执行和定时器事件默认都是排他任务
  • 如果你想把job设置为非排他,可以使用activiti:exclusive=“false” 进行配置:
<serviceTask id="service" activiti:expression="${myService.performBooking(hotel, dates)}" activiti:async="true" activiti:exclusive="false" />
  • 排他任务没有性能问题:
    • 在高负载的情况下性能是个问题,高负载意味着JobExecutor的所有工作线程都一直在忙碌着
    • 使用排他任务,Activiti可以简单的分布不同的负载.排他任务意味着同一个流程实例的异步执行会由相同的线程顺序执行
    • 但是要考虑:如果有多个流程实例时.所有其他流程实例的job也会分配给其他线程同步执行
    • 意味着虽然Activiti不会同时执行一个流程实例的排他job,但是还会同步执行多个流程实例的异步执行
    • 通过一个总体的预测,在大多数场景下,排他任务都会让单独的实例运行的更迅速.而且,对于同一流程实例中的job,需要用到的数据也会利用执行的集群节点的缓存.如果任务没有在同一个节点执行,数据就必须每次从数据库重新读取了

流程实例授权

  • 默认所有人在部署的流程定义上启动一个新流程实例,通过流程初始化授权功能定义的用户和组,web客户端可以限制哪些用户可以启动一个新流程实例
  • Activiti引擎不会校验授权定义: 这个功能只是为减轻web客户端开发者实现校验规则的难度
  • 设置方法与用户任务用户分配类似,用户或组可以使用activiti:potentialStarter标签分配为流程的默认启动者:
 <process id="potentialStarter">
     <extensionElements>
       <activiti:potentialStarter>
         <resourceAssignmentExpression>
           <formalExpression>group2, group(group3), user(user3)</formalExpression>
         </resourceAssignmentExpression>
       </activiti:potentialStarter>
     </extensionElements>
   <startEvent id="theStart"/>
   ...

user(user3)是直接引用了用户user3,group(group3)是引用了组group3.如果没显示设置,默为群组

  • 也可以使用process标签的属性activiti:candidateStarterUsersactiviti:candidateStarterGroups
 <process id="potentialStarter" activiti:candidateStarterUsers="user1, user2"
                                activiti:candidateStarterGroups="group1">
      ...
  

可以同时使用这两个属性

  • 定义流程初始化授权后,开发者可以使用如下方法获得授权定义.可以获得给定的用户能够启动哪些流程定义:
 processDefinitions = repositoryService.createProcessDefinitionQuery().startableByUser("userxxx").list();
  • 可以获得指定流程定义设置的潜在启动者对应的IdentityLink:
 identityLinks = repositoryService.getIdentityLinksForProcessDefinition("processDefinitionId"); 
  • 获得可以启动给定流程的用户列表的示例:
  List<User> authorizedUsers =  identityService().createUserQuery().potentialStarter("processDefinitionId").list(); 
  • 获得可以启动给定流程配置的群组的示例:
 List<Group> authorizedGroups =  identityService().createGroupQuery().potentialStarter("processDefinitionId").list();  

数据对象

  • BPMN提供了一种功能,可以在流程定义或子流程中定义数据对象
  • 根据BPMN规范,流程定义可以包含复杂XML结构,可以导入XSD定义
  • 对于Activiti来说 ,作为Activiti首次支持的数据对象, 可以支持如下的XSD类型
	<dataObject id="dObj1" name="StringTest" itemSubjectRef="xsd:string"/>
	<dataObject id="dObj2" name="BooleanTest" itemSubjectRef="xsd:boolean"/>
	<dataObject id="dObj3" name="DateTest" itemSubjectRef="xsd:datetime"/>
	<dataObject id="dObj4" name="DoubleTest" itemSubjectRef="xsd:double"/>
	<dataObject id="dObj5" name="IntegerTest" itemSubjectRef="xsd:int"/>
	<dataObject id="dObj6" name="LongTest" itemSubjectRef="xsd:long"/>
  • 数据对象定义会自动转换为流程变量,名称与name属性对应
  • 除了数据对象的定义之外,Activiti支持使用扩展元素来为这个变量赋予默认值:
<process id="dataObjectScope" name="Data Object Scope" isExecutable="true">
          <dataObject id="dObj123" name="StringTest123" itemSubjectRef="xsd:string">
            <extensionElements>
              <activiti:value>Testing123</activiti:value>
            </extensionElements>
          </dataObject>
发布了113 篇原创文章 · 获赞 95 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/JewaveOxford/article/details/102909745