返回介绍

8.5. 任务

发布于 2023-09-17 23:40:35 字数 80649 浏览 0 评论 0 收藏 0

8.5.1. 用户任务

描述

“用户任务(user task)”用于对需要人工执行的任务进行建模。当流程执行到达用户任务时,会为指派至该任务的用户或组的任务列表创建一个新任务。

图示

用户任务用左上角有一个小用户图标的标准任务(圆角矩形)表示。

bpmn.user.task

XML表示

用户任务在XML中如下定义。其中id是必须属性,name是可选属性。

<userTask name="Important task" />

也可以为用户任务添加描述(description)。事实上任何BPMN 2.0元素都可以有描述。描述由documentation元素定义。


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

可以使用标准Java方式获取描述文本:

task.getDescription()

到期日期

每个任务都可以使用一个字段标志该任务的到期日期(due date)。可以使用查询API,查询在给定日期前或后到期的任务。

可以在任务定义中使用扩展指定表达式,以在任务创建时设定到期日期。该表达式必须解析为java.util.Datejava.util.String (ISO8601格式),ISO8601时间长度(例如PT50M),或者null。例如,可以使用在流程里前一个表单中输入的日期,或者由前一个服务任务计算出的日期。如果使用的是时间长度,则到期日期基于当前时间加上给定长度计算。例如当dueDate使用“PT30M”时,任务在从现在起30分钟后到期。

<userTask name="Important task" flowable:dueDate="${dateVariable}"/>

任务的到期日期也可以使用TaskService,或者在TaskListener中使用传递的DelegateTask修改。

用户指派

用户任务可以直接指派(assign)给用户。可以定义humanPerformer子元素来实现。humanPerformer需要resourceAssignmentExpression来实际定义用户。目前,只支持formalExpressions

<process >

  ...

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

只能指定一个用户作为任务的humanPerformer。在Flowable术语中,这个用户被称作办理人(assignee)。拥有办理人的任务,在其他人的任务列表中不可见,而只能在该办理人的个人任务列表中看到。

可以通过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结构的任务:

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

将获取所有kermit作为候选用户的任务,也就是说,表达式含有user(kermit)的任务。同时也将获取所有指派给kermit为其成员的组的任务(例如,kermit时management组的成员,且任务指派给management组)。组在运行时解析,并可通过身份服务管理。

如果并未指定给定字符串是用户还是组,引擎默认其为组。下列代码与声明group(accountancy)效果一样。

<formalExpression>accountancy</formalExpression>

用于任务指派的Flowable扩展

很明显,当指派关系不复杂时,这种用户与组的指派方式十分笨重。为避免这种复杂性,可以在用户任务上使用自定义扩展。

  • assignee(办理人)属性:这个自定义扩展用于直接将用户指派至用户任务。

<userTask name="my task" flowable:assignee="kermit" />

与上面定义的humanPerformer结构效果完全相同。

  • candidateUsers(候选用户)属性:这个自定义扩展用于为任务指定候选用户。

<userTask name="my task" flowable:candidateUsers="kermit, gonzo" />

与使用上面定义的potentialOwner结构效果完全相同。请注意不需要像在potentialOwner中一样,使用user(kermit)的声明,因为这个属性只能用于用户。

  • candidateGroups(候选组)attribute:这个自定义扩展用于为任务指定候选组。

<userTask name="my task" flowable:candidateGroups="management, accountancy" />

与使用上面定义的potentialOwner结构效果完全相同。请注意不需要像在potentialOwner中一样,使用group(management)的声明,因为这个属性只能用于组。

  • 可以定义在一个用户任务上同时定义candidateUserscandidateGroups

请注意:尽管Flowable提供了IdentityService身份管理组件,但并不会检查给定的用户是否实际存在。这是为了便于将Flowable嵌入应用时,与已有的身份管理解决方案进行集成。

自定义身份关联类型

在用户指派中已经介绍过,BPMN标准支持单个指派用户即hunamPerformer,或者由一组用户构成potentialOwners潜在用户池。另外,Flowable为用户任务定义了扩展属性元素,用于代表任务的办理人或者候选用户

Flowable支持的身份关联(identity link)类型有:

public class IdentityLinkType {
  /* Flowable内置角色 */
  public static final String ASSIGNEE = "assignee";
  public static final String CANDIDATE = "candidate";
  public static final String OWNER = "owner";
  public static final String STARTER = "starter";
  public static final String PARTICIPANT = "participant";
}

BPMN标准及Flowable示例中,身份认证是用户。在前一章节提到过,Flowable的身份管理实现并不适用于生产环境,而需要在支持的认证概要下自行扩展。

如果需要添加额外的关联类型,可按照下列语法,使用自定义资源作为扩展元素:

<userTask name="make profit">
  <extensionElements>
  <flowable:customResource flowable:name="businessAdministrator">
    <resourceAssignmentExpression>
    <formalExpression>user(kermit), group(management)</formalExpression>
    </resourceAssignmentExpression>
  </flowable:customResource>
  </extensionElements>
</userTask>

自定义关联表达式添加至TaskDefinition类:


protected Map<String, Set<Expression>> customUserIdentityLinkExpressions =
  new HashMap<String, Set<Expression>>();
protected Map<String, Set<Expression>> customGroupIdentityLinkExpressions =
  new HashMap<String, Set<Expression>>();

public Map<String, Set<Expression>> getCustomUserIdentityLinkExpressions() {
  return customUserIdentityLinkExpressions;
}

public void addCustomUserIdentityLinkExpression(
    String identityLinkType, Set<Expression> idList) {

  customUserIdentityLinkExpressions.put(identityLinkType, idList);
}

public Map<String, Set<Expression>> getCustomGroupIdentityLinkExpressions() {
  return customGroupIdentityLinkExpressions;
}

public void addCustomGroupIdentityLinkExpression(
    String identityLinkType, Set<Expression> idList) {

  customGroupIdentityLinkExpressions.put(identityLinkType, idList);
}

这些方法将在运行时,由UserTaskActivityBehaviorhandleAssignments方法调用。

最后,需要扩展IdentityLinkType类,以支持自定义身份关联类型:

package com.yourco.engine.task;

public class IdentityLinkType extends org.flowable.engine.task.IdentityLinkType {

  public static final String ADMINISTRATOR = "administrator";

  public static final String EXCLUDED_OWNER = "excludedOwner";
}

通过任务监听器自定义指派

如果上面的方式仍不能满足要求,可以在创建事件(create event)上使用任务监听器,调用自定义指派逻辑:

<userTask name="My task" >
  <extensionElements>
  <flowable:taskListener event="create" class="org.flowable.MyAssignmentHandler" />
  </extensionElements>
</userTask>

传递至TaskListenerDelegateTask,可用于设置办理人与候选用户/组:

public class MyAssignmentHandler implements TaskListener {

  public void notify(DelegateTask delegateTask) {
  // 在这里执行自定义身份查询

  // 然后调用如下命令:
  delegateTask.setAssignee("kermit");
  delegateTask.addCandidateUser("fozzie");
  delegateTask.addCandidateGroup("management");
  ...
  }

}

当使用Spring时,可以按上面章节的介绍使用自定义指派属性,并交由使用任务监听器、带有表达式的Spring bean,监听任务创建事件。在下面的例子中,通过调用ldapService Spring bean的findManagerOfEmployee方法设置办理人。传递的emp参数是一个流程变量。

<userTask name="My Task" flowable:assignee="${ldapService.findManagerForEmployee(emp)}"/>

也可以用于候选用户与组:

<userTask name="My Task" flowable:candidateUsers="${ldapService.findAllSales()}"/>

请注意调用方法的返回类型必须是StringCollection<String>(候选用户或组):

public class FakeLdapService {

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

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

}

8.5.2. 脚本任务

描述

脚本任务(script task)是自动执行的活动。当流程执行到达脚本任务时,会执行相应的脚本。

图示

脚本任务用左上角有一个小“脚本”图标的标准BPMN 2.0任务(圆角矩形)表示。

bpmn.scripttask

XML表示

脚本任务使用scriptscriptFormat元素定义。

<scriptTask name="Execute script" scriptFormat="groovy">
  <script>
  sum = 0
  for ( i in inputArray ) {
    sum += i
  }
  </script>
</scriptTask>

scriptFormat属性的值,必须是兼容JSR-223(Java平台脚本)的名字。默认情况下,JavaScript包含在每一个JDK中,因此不需要添加任何JAR文件。如果想使用其它(兼容JSR-223的)脚本引擎,则需要在classpath中添加相应的jar,并使用适当的名字。例如,Flowable单元测试经常使用Groovy,因为其语法与Java十分相似。

请注意Groovy脚本引擎与groovy-all JAR捆绑在一起。在Groovy 2.0版本以前,脚本引擎是Groovy JAR的一部分。因此,必须添加如下依赖:

<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-all</artifactId>
  <version>2.x.x<version>
</dependency>

脚本中的变量

到达脚本引擎的执行中,所有的流程变量都可以在脚本中使用。在这个例子里,脚本变量'inputArray'实际上就是一个流程变量(一个integer的数组)。

<script>
  sum = 0
  for ( i in inputArray ) {
    sum += i
  }
</script>

也可以简单地调用execution.setVariable("variableName", variableValue),在脚本中设置流程变量。默认情况下,变量不会自动储存(请注意,在一些早期版本中是会储存的!)。可以将scriptTaskautoStoreVariables参数设置为true,以自动保存任何在脚本中定义的变量(例如上例中的sum)。然而这并不是最佳实践。最佳实践是显式调用execution.setVariable(),因为在JDK近期的一些版本中,某些脚本语言不能自动保存变量。查看这个链接了解更多信息。

<scriptTask scriptFormat="JavaScript" flowable:autoStoreVariables="false">

这个参数的默认值为false。也就是说如果在脚本任务定义中忽略这个参数,则脚本声明的所有变量将只在脚本执行期间有效。

在脚本中设置变量的例子:

<script>
  def scriptVar = "test123"
  execution.setVariable("myVar", scriptVar)
</script>

请注意:下列名字是保留字,不能用于变量名:out,out:print,lang:import,context,elcontext

脚本任务的结果

脚本任务的返回值,可以通过为脚本任务定义的'flowable:resultVariable'属性设置为流程变量。可以是已经存在的,或者新的流程变量。如果指定为已存在的流程变量,则流程变量的值会被脚本执行的结果值覆盖。如果不指定结果变量名,则脚本结果值将被忽略。

<scriptTask name="Execute script" scriptFormat="juel" flowable:resultVariable="myVar">
  <script>#{echo}</script>
</scriptTask>

在上面的例子中,脚本执行的结果(解析表达式'#{echo}'的值),将在脚本完成后,设置为名为'myVar'的流程变量。

安全性

当使用javascript作为脚本语言时,可以使用“安全脚本(secure scripting)”。参见安全脚本章节。

8.5.3. Java服务任务

描述

Java服务任务(Java service task)用于调用Java类。

图示

服务任务用左上角有一个小齿轮图标的圆角矩形表示。

bpmn.java.service.task

XML表示

有四种方法声明如何调用Java逻辑:

  • 指定实现了JavaDelegate或ActivityBehavior的类

  • 调用解析为委托对象(delegation object)的表达式

  • 调用方法表达式(method expression)

  • 对值表达式(value expression)求值

使用flowable:class属性提供全限定类名(fully qualified classname),指定流程执行时调用的类。

<serviceTask
       name="My Java Service Task"
       flowable:class="org.flowable.MyJavaDelegate" />

查看实现章节,了解使用这种类的更多信息。

也可以使用解析为对象的表达式。该对象必须遵循的规则,与使用flowable:class创建的对象规则相同(查看更多)。

<serviceTask flowable:delegateExpression="${delegateExpressionBean}" />

delegateExpressionBean是一个实现了JavaDelegate接口的bean,定义在Spring容器中。

使用flowable:expression属性指定需要计算的UEL方法表达式。

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

将在名为printer的对象上调用printMessage方法(不带参数)。

也可以为表达式中使用的方法传递变量。

<serviceTask
       name="My Java Service Task"
       flowable:expression="#{printer.printMessage(execution, myVar)}" />

将在名为printer的对象上调用printMessage方法。传递的第一个参数为DelegateExecution,名为execution,在表达式上下文中默认可用。传递的第二个参数,是当前执行中,名为myVar变量的值。

可以使用flowable:expression属性指定需要计算的UEL值表达式。

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

会调用名为split的bean的ready参数的getter方法,getReady(不带参数)。该对象会被解析为执行的流程变量或(如果可用的话)Spring上下文中的bean。

实现

要实现可以在流程执行中调用的类,需要实现org.flowable.engine.delegate.JavaDelegate接口,并在execute方法中提供所需逻辑。当流程执行到达该活动时,会执行方法中定义的逻辑,并按照BPMN 2.0的默认方法离开活动。

下面是一个Java类的示例,用于将流程变量String改为大写。这个类需要实现org.flowable.engine.delegate.JavaDelegate接口,因此需要实现execute(DelegateExecution)方法。这个方法就是引擎将调用的方法,需要实现业务逻辑。可以通过DelegateExecution接口(点击链接获取该接口操作的详细Javadoc)访问流程实例信息,如流程变量等。

public class ToUppercase implements JavaDelegate {

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

}

请注意:只会为serviceTask上定义的Java类创建一个实例。所有流程实例共享同一个类实例,用于调用execute(DelegateExecution)。这意味着该类不能有任何成员变量,并需要是线程安全的,因为它可能会在不同线程中同时执行。这也影响了字段注入的使用方法。(译者注:原文可能较老,不正确。5.21中,flowable:class指定的类,会在流程实例启动时,为每个活动分别进行实例化。不过,当该活动在流程中重复执行,或者为多实例时,使用的都会是同一个类实例。)

在流程定义中引用(如flowable:class)的类,不会在部署时实例化。只有当流程执行第一次到达该类使用的地方时,才会创建该类的实例。如果找不到这个类,会抛出FlowableException。这是因为部署时的环境(更准确的说classpath),与实际运行的环境经常不一样。例如当使用ant或者Flowable应用中业务存档上传的方式部署的流程,其classpath中不会自动包含流程引用的类。

[内部:非公开实现类]也可以使用实现了org.flowable.engine.impl.delegate.ActivityBehavior接口的类。该实现可以访问更强大的引擎功能,例如,可以影响流程的控制流程。请注意这并不是很好的实践,需要避免这么使用。建议只有在高级使用场景下,并且你确知在做什么的时候,才使用ActivityBehavior接口。

字段注入

可以为委托类的字段注入值。支持下列注入方式:

  • 字符串常量

  • 表达式

如果可以的话,会按照Java Bean命名约定(例如,firstName成员使用setter setFirstName(…​)),通过委托类的公有setter方法,注入变量。如果该字段没有可用的setter,会直接设置该委托类的私有成员的值。有的环境中,SecurityManagers不允许修改私有字段,因此为想要注入的字段暴露一个公有setter方法,是更安全的做法。

不论在流程定义中声明的是什么类型的值,注入对象的setter/私有字段的类型,总是org.flowable.engine.delegate.Expression。解析表达式后,可以被转型为合适的类型。

'flowable:class'属性支持字段注入。也可以在使用flowable:delegateExpression属性时,进行字段注入。然而考虑到线程安全,需要遵循特殊的规则(参见下一章节)。

下面的代码片段展示了如何为类中声明的字段注入常量值。请注意按照BPMN 2.0 XML概要的要求,在实际字段注入声明前,需要先声明’extensionElements’XML元素

<serviceTask
  name="Java service invocation"
  flowable:class="org.flowable.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
  <extensionElements>
    <flowable:field name="text" stringValue="Hello World" />
  </extensionElements>
</serviceTask>

ToUpperCaseFieldInjected类有一个字段text,为org.flowable.engine.delegate.Expression类型。当调用text.getValue(execution)时,会返回配置的字符串Hello World

public class ToUpperCaseFieldInjected implements JavaDelegate {

  private Expression text;

  public void execute(DelegateExecution execution) {
  execution.setVariable("var", ((String)text.getValue(execution)).toUpperCase());
  }

}

另外,对于较长文本(例如邮件正文),可以使用'flowable:string'子元素:

<serviceTask
  name="Java service invocation"
  flowable:class="org.flowable.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
  <extensionElements>
  <flowable:field name="text">
    <flowable:string>
      This is a long string with a lot of words and potentially way longer even!
    </flowable:string>
  </flowable:field>
  </extensionElements>
</serviceTask>

可以使用表达式在运行时动态解析注入的值。这种表达式可以使用流程变量,或者(若使用Spring)Spring定义的Bean。在服务任务实现中提到过,当服务任务中使用flowable:class属性时,该Java类的实例在所有流程实例中共享。要动态地为字段注入值,可以在org.flowable.engine.delegate.Expression中注入值或方法表达式,它们会通过execute方法传递的DelegateExecution计算/调用。

下面的示例类使用了注入的表达式,并使用当前的DelegateExecution解析它们。调用genderBean方法时传递的是gender变量。完整的代码与测试可以在org.flowable.examples.bpmn.servicetask.JavaServiceTaskTest.testExpressionFieldInjection中找到

<serviceTask name="Java service invocation"
  flowable:class="org.flowable.examples.bpmn.servicetask.ReverseStringsFieldInjected">

  <extensionElements>
  <flowable:field name="text1">
    <flowable:expression>${genderBean.getGenderString(gender)}</flowable:expression>
  </flowable:field>
  <flowable:field name="text2">
     <flowable:expression>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</flowable:expression>
  </flowable:field>
  </ extensionElements>
</ serviceTask>
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());
  }
}

另外,为避免XML太过冗长,可以将表达式设置为属性,而不是子元素。

<flowable:field name="text1" expression="${genderBean.getGenderString(gender)}" />
<flowable:field name="text1" expression="Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}" />

字段注入与线程安全

通常情况下,在服务任务中使用Java委托与字段注入是线程安全的。然而,有些情况下不能保证线程安全。这取决于设置,或Flowable运行的环境。

当使用flowable:class属性时,使用字段注入总是线程安全的(译者注:仍不完全安全,如对于多实例服务任务,使用的是同一个实例)。对于引用了某个类的每一个服务任务,都会实例化新的实例,并且在创建实例时注入一次字段。在不同的任务或流程定义中多次使用同一个类没有问题。

当使用flowable:expression属性时,不能使用字段注入。只能通过方法调用传递变量。总是线程安全的。

当使用flowable:delegateExpression属性时,委托实例的线程安全性,取决于表达式解析的方式。如果该委托表达式在多个任务或流程定义中重复使用,并且表达式总是返回相同的示例,则字段注入不是线程安全的。让我们看几个例子。

假设表达式为${factory.createDelegate(someVariable)},其中factory为引擎可用的Java bean(例如使用Spring集成时的Spring bean),并在每次表达式解析时创建新的实例。这种情况下,使用字段注入时,没有线程安全性问题:每次表达式解析时,都会注入新实例的字段。

然而,如果表达式为${someJavaDelegateBean},解析为JavaDelegate的实现,并且在创建单例的环境(如Spring)中运行。当在不同的任务或流程定义中使用这个表达式时,表达式总会解析为相同的实例。这种情况下,使用字段注入不是线程安全的。例如:

<serviceTask flowable:delegateExpression="${someJavaDelegateBean}">
  <extensionElements>
    <flowable:field name="someField" expression="${input * 2}"/>
  </extensionElements>
</serviceTask>

<!-- 其它流程定义元素 -->

<serviceTask flowable:delegateExpression="${someJavaDelegateBean}">
  <extensionElements>
    <flowable:field name="someField" expression="${input * 2000}"/>
  </extensionElements>
</serviceTask>

这段示例代码有两个服务任务,使用同一个委托表达式,但是expression字段填写不同的值。如果该表达式解析为相同的实例,就会在并发场景下,注入someField字段时出现竞争条件

最简单的解决方法是:

  • 使用表达式代替直接使用Java委托,并将所需数据通过方法参数传递给委托。

  • 或者,在每次委托表达式解析时,返回委托类的新实例。这意味着这个bean的scope必须是prototype(原型)(例如在委托类上加上@Scope(SCOPE_PROTOTYPE)注解)。

在Flowable 5.22版本中,可以通过配置流程引擎配置,禁用在委托表达式上使用字段注入。需要设置delegateExpressionFieldInjectionMode参数(取org.flowable.engine.imp.cfg.DelegateExpressionFieldInjectionMode枚举中的值)。

可使用下列选项:

  • DISABLED(禁用):当使用委托表达式时,完全禁用字段注入。不会再尝试进行字段注入。这是最安全的方式,保证线程安全。

  • COMPATIBILITY(兼容):在这个模式下,行为与V5.21之前完全一样:可以在委托表达式中使用字段注入,如果委托类中没有定义该字段,会抛出异常。这是最不线程安全的模式,但可以保证历史版本兼容性,也可以在委托表达式只在一个任务中使用的时候(因此不会产生并发竞争条件),安全使用。

  • MIXED(混合):可以在使用委托表达式时注入,但当委托中没有定义字段时,不会抛出异常。这样可以在部分委托(比如不是单例的实例)中使用注入,而在部分委托中不使用注入。

  • Flowable 5.x版本的默认模式为COMPATIBILITY

  • Flowable 6.x版本的默认模式为MIXED

例如,假设使用MIXED模式,并使用Spring集成,在Spring配置中定义了如下bean:

<bean
  class="org.flowable.spring.test.fieldinjection.SingletonDelegateExpressionBean" />

<bean
  class="org.flowable.spring.test.fieldinjection.PrototypeDelegateExpressionBean"
  scope="prototype" />

第一个bean是一般的Spring bean,因此是单例的。第二个bean的scope为prototype,因此每次请求这个bean时,Spring容器都会返回一个新实例。

在以下流程定义中:

<serviceTask flowable:delegateExpression="${prototypeDelegateExpressionBean}">
  <extensionElements>
  <flowable:field name="fieldA" expression="${input * 2}"/>
  <flowable:field name="fieldB" expression="${1 + 1}"/>
  <flowable:field name="resultVariableName" stringValue="resultServiceTask1"/>
  </extensionElements>
</serviceTask>

<serviceTask flowable:delegateExpression="${prototypeDelegateExpressionBean}">
  <extensionElements>
  <flowable:field name="fieldA" expression="${123}"/>
  <flowable:field name="fieldB" expression="${456}"/>
  <flowable:field name="resultVariableName" stringValue="resultServiceTask2"/>
  </extensionElements>
</serviceTask>

<serviceTask flowable:delegateExpression="${singletonDelegateExpressionBean}">
  <extensionElements>
  <flowable:field name="fieldA" expression="${input * 2}"/>
  <flowable:field name="fieldB" expression="${1 + 1}"/>
  <flowable:field name="resultVariableName" stringValue="resultServiceTask1"/>
  </extensionElements>
</serviceTask>

<serviceTask flowable:delegateExpression="${singletonDelegateExpressionBean}">
  <extensionElements>
  <flowable:field name="fieldA" expression="${123}"/>
  <flowable:field name="fieldB" expression="${456}"/>
  <flowable:field name="resultVariableName" stringValue="resultServiceTask2"/>
  </extensionElements>
</serviceTask>

有四个服务任务,第一、二个使用${prototypeDelegateExpressionBean}委托表达式,第三、四个使用${singletonDelegateExpressionBean}委托表达式。

先看原型bean:

public class PrototypeDelegateExpressionBean implements JavaDelegate {

  public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);

  private Expression fieldA;
  private Expression fieldB;
  private Expression resultVariableName;

  public PrototypeDelegateExpressionBean() {
  INSTANCE_COUNT.incrementAndGet();
  }

  @Override
  public void execute(DelegateExecution execution) {

  Number fieldAValue = (Number) fieldA.getValue(execution);
  Number fieldValueB = (Number) fieldB.getValue(execution);

  int result = fieldAValue.intValue() + fieldValueB.intValue();
  execution.setVariable(resultVariableName.getValue(execution).toString(), result);
  }

}

在运行上面流程定义的流程实例后,INSTANCE_COUNT的值为2。这是因为每次解析${prototypeDelegateExpressionBean}时,都会创建新实例。可以看到三个Expression成员字段的注入没有任何问题。

单例bean则有一点区别:

public class SingletonDelegateExpressionBean implements JavaDelegate {

  public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);

  public SingletonDelegateExpressionBean() {
  INSTANCE_COUNT.incrementAndGet();
  }

  @Override
  public void execute(DelegateExecution execution) {

  Expression fieldAExpression = DelegateHelper.getFieldExpression(execution, "fieldA");
  Number fieldA = (Number) fieldAExpression.getValue(execution);

  Expression fieldBExpression = DelegateHelper.getFieldExpression(execution, "fieldB");
  Number fieldB = (Number) fieldBExpression.getValue(execution);

  int result = fieldA.intValue() + fieldB.intValue();

  String resultVariableName = DelegateHelper.getFieldExpression(execution,
    "resultVariableName").getValue(execution).toString();
  execution.setVariable(resultVariableName, result);
  }

}

在对于单例bean,INSTANCE_COUNT总是1。在这个委托中,没有Expression成员字段(使用MIXED模式)。而在COMPATIBILITY模式下,就会抛出异常,因为需要有成员字段。这个bean也可以使用DISABLED模式,但会禁用上面进行了字段注入的原型bean。

在委托的代码里,使用了org.flowable.engine.delegate.DelegateHelper。它提供了一些有用的工具方法,用于执行相同的逻辑,并且在单例中是线程安全的。与注入Expression不同,它通过getFieldExpression读取。这意味着在服务任务的XML里,字段定义与单例bean完全相同。查看上面的XML代码,可以看到定义是相同的,只是实现逻辑不同。

技术提示:getFieldExpression直接读取BpmnModel,并在方法执行时创建表达式,因此是线程安全的。

  • 在Flowable V5.x版本中,(由于架构缺陷)不能在ExecutionListenerTaskListener中使用DelegateHelper。要保证监听器的线程安全,仍需使用表达式,或确保每次解析委托表达式时,都创建新实例。

  • 在Flowable V6.x版本中,在ExecutionListenerTaskListener中可以使用DelegateHelper。例如在V6.x版本中,下列代码可以使用DelegateHelper

<extensionElements>
  <flowable:executionListener
    delegateExpression="${testExecutionListener}" event="start">
  <flowable:field name="input" expression="${startValue}" />
  <flowable:field name="resultVar" stringValue="processStartValue" />
  </flowable:executionListener>
</extensionElements>

其中testExecutionListener解析为ExecutionListener接口的一个实现的实例:

@Component("testExecutionListener")
public class TestExecutionListener implements ExecutionListener {

  @Override
  public void notify(DelegateExecution execution) {
  Expression inputExpression = DelegateHelper.getFieldExpression(execution, "input");
  Number input = (Number) inputExpression.getValue(execution);

  int result = input.intValue() * 100;

  Expression resultVarExpression = DelegateHelper.getFieldExpression(execution, "resultVar");
  execution.setVariable(resultVarExpression.getValue(execution).toString(), result);
  }

}

服务任务的结果

服务执行的返回值(仅对使用表达式的服务任务),可以通过为服务任务定义的'flowable:resultVariable'属性设置为流程变量。可以是已经存在的,或者新的流程变量。 如果指定为已存在的流程变量,则流程变量的值会被服务执行的结果值覆盖。 如果使用'flowable:useLocalScopeForResultVariable',则会将结果值设置为局部变量。 如果不指定结果变量名,则服务任务的结果值将被忽略。

<serviceTask
  flowable:expression="#{myService.doSomething()}"
  flowable:resultVariable="myVar" />

在上例中,服务执行的结果(流程变量或Spring bean中,使用'myService'名字所获取的对象,调用'doSomething()'方法的返回值),在服务执行完成后,会设置为名为'myVar'的流程变量。

可触发

一种常见的模式是发送JMS消息或HTTP调用触发外部服务,然后流程实例进入等待状态。之后外部系统会回复响应,流程实例继续执行下一个活动。在默认的BPMN中,需要使用服务任务和接收任务(receive task)。但是这样会引入竞争条件:外部服务的响应可能会早于流程实例持久化及接收任务激活。为了解决这个问题,Flowable为服务任务增加了triggerable(可触发)属性,可以将服务任务转变为执行服务逻辑,并在继续执行之前等待外部触发的任务。如果在可触发服务任务上同时设置异步(async 为 true),则流程实例会先持久化,然后在异步作业中执行服务任务逻辑。在BPMN XML中,可以这样实现可触发服务任务:

<serviceTask
  flowable:expression="#{myService.doSomething()}"
  flowable:triggerable="true"
  flowable:async="true" />

外部服务可以同步或异步地触发等待中的流程实例。为了避免乐观锁异常,最好使用异步触发。默认情况下,异步作业是排他的,也就是说流程实例会被锁定,以保证流程实例中的其他活动不会影响到触发器的逻辑。可以使用RuntimeService的triggerAsync方法,异步触发等待中的流程实例。当然还是可以使用RuntimeService的trigger方法,同步触发。

处理异常

当执行自定义逻辑时,通常需要捕获并在流程中处理特定的业务异常。Flowable提供了多种选择。

抛出BPMN错误

可以在服务任务或脚本任务的用户代码中抛出BPMN错误。可以在Java委托、脚本、表达式与委托表达式中,抛出特殊的FlowableException:BpmnError。引擎会捕获这个异常,并将其转发至合适的错误处理器,如错误边界事件或错误事件子流程。

public class ThrowBpmnErrorDelegate implements JavaDelegate {

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

}

构造函数的参数是错误代码。错误代码决定了处理这个错误的错误处理器。参见错误边界事件了解如何捕获BPMN错误。

这个机制只应该用于业务错误,需要通过流程中定义的错误边界事件或错误事件子流程处理。技术错误应该通过其他异常类型表现,并且通常不在流程内部处理。

异常映射

也可以使用mapException扩展,直接将Java异常映射至业务异常(错误)。单映射是最简单的形式:

<serviceTask name="Service Task" flowable:class="...">
  <extensionElements>
  <flowable:mapException
      errorCode="myErrorCode1">org.flowable.SomeException</flowable:mapException>
  </extensionElements>
</serviceTask>

在上面的代码中,如果服务任务抛出org.flowable.SomeException的实例,引擎会捕获该异常,并将其转换为带有给定errorCode的BPMN错误。然后就可以像普通BPMN错误完全一样地处理。其他的异常没有映射,仍将抛出至API调用处。

也可以在单行中使用includeChildExceptions属性,映射特定异常的所有子异常。

<serviceTask name="Service Task" flowable:class="...">
  <extensionElements>
  <flowable:mapException errorCode="myErrorCode1"
       includeChildExceptions="true">org.flowable.SomeException</flowable:mapException>
  </extensionElements>
</serviceTask>

上面的代码中,Flowable会将SomeException的任何直接或间接的子类,转换为带有指定错误代码的BPMN错误。 当未指定includeChildExceptions时,视为“false”。

默认映射最泛用。默认映射是一个不指定类的映射,可以匹配任何Java异常:

<serviceTask name="Service Task" flowable:class="...">
  <extensionElements>
  <flowable:mapException errorCode="myErrorCode1"/>
  </extensionElements>
</serviceTask>

除了默认映射,会按照从上至下的顺序检查映射,使用第一个匹配的映射。只在所有映射都不能成功匹配时使用默认映射。 只有第一个不指定类的映射会作为默认映射。默认映射忽略includeChildExceptions

异常顺序流

[内部:非公开实现类]

也可以选择在发生异常时,将流程执行路由至另一条路径。下面是一个例子。

<serviceTask
  name="Java service invocation"
  flowable:class="org.flowable.ThrowsExceptionBehavior">
</serviceTask>

<sequenceFlow sourceRef="javaService" targetRef="theEnd" />
<sequenceFlow sourceRef="javaService" targetRef="fixException" />

服务任务有两条出口顺序流,命名为exceptionno-exception。在发生异常时,使用顺序流ID控制流程流向:

public class ThrowsExceptionBehavior implements ActivityBehavior {

  public void execute(DelegateExecution execution) {
  String var = (String) execution.getVariable("var");

  String sequenceFlowToTake = null;
  try {
    executeLogic(var);
    sequenceFlowToTake = "no-exception";
  } catch (Exception e) {
    sequenceFlowToTake = "exception";
  }
  DelegateHelper.leaveDelegate(execution, sequenceFlowToTake);
  }

}

在JavaDelegate中使用Flowable服务

有的时候,需要在Java服务任务中使用Flowable服务(例如调用活动(call activity)不满足需要的场景下,使用RuntimeService启动流程实例)。

public class StartProcessInstanceTestDelegate implements JavaDelegate {

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

}

可以使用这个接口访问所有Flowable服务API。

使用这些API调用造成的所有数据变更都在当前事务中。在依赖注入的环境(如Spring或CDI,无论是否使用启用JTA的数据源)下也可以使用这个接口。例如,下面的代码片段与上面的代码具有相同功能,但通过注入而不是org.flowable.engine.EngineServices接口获得RuntimeService。

@Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {

  @Autowired
  private RuntimeService runtimeService;

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

}

重要技术提示:由于服务调用是在当前事务中完成的,因此在服务任务执行前产生或修改的数据尚未存入数据库。所有API调用都基于数据库数据处理,这意味着这些未提交的修改在服务任务的API调用中“不可见”。

8.5.4. Web服务任务

描述

Web服务任务(Web service task)用于同步地调用外部的Web服务。

图示

Web服务任务与Java服务任务图标一样。

bpmn.web.service.task

XML表示

使用Web服务之前,需要导入其操作及复杂的类型。可以使用导入标签(import tag)指向Web服务的WSDL,自动处理:

<import importType="http://schemas.xmlsoap.org/wsdl/"
	location="http://localhost:63081/counter?wsdl"
	namespace="http://webservice.flowable.org/" />

按照上面的声明,Flowable会导入定义,但不会创建条目定义(item definition)与消息。如果需要调用一个名为’prettyPrint’的方法,则需要先为请求及回复消息创建对应的消息与条目定义:

<message itemRef="tns:prettyPrintCountRequestItem" />
<message itemRef="tns:prettyPrintCountResponseItem" />

<itemDefinition structureRef="counter:prettyPrintCount" />
<itemDefinition structureRef="counter:prettyPrintCountResponse" />

在声明服务任务前,需要定义实际引用Web服务的BPMN接口与操作。基本上,是定义“接口”与所需的“操作”。对每一个操作都可以重复使用之前定义的“传入”与“传出”消息。例如,下面的声明定义了“counter”接口及“prettyPrintCountOperation”操作:

<interface name="Counter Interface" implementationRef="counter:Counter">
	<operation name="prettyPrintCount Operation"
			implementationRef="counter:prettyPrintCount">
		<inMessageRef>tns:prettyPrintCountRequestMessage</inMessageRef>
		<outMessageRef>tns:prettyPrintCountResponseMessage</outMessageRef>
	</operation>
</interface>

这样就可以使用##WebService实现,声明Web服务任务,并引用Web服务操作。

<serviceTask
	name="Web service invocation"
	implementation="##WebService"
	operationRef="tns:prettyPrintCountOperation">

Web服务任务IO规范

除非使用简化方法处理输入与输出数据关联(见下),否则需要为每个Web服务任务声明IO规范,指出任务的输入与输出是什么。这个方法很简单,也兼容BPMN 2.0。在prettyPrint例子中,根据之前声明的条目定义,定义输入与输出:

<ioSpecification>
  <dataInput itemSubjectRef="tns:prettyPrintCountRequestItem" />
  <dataOutput itemSubjectRef="tns:prettyPrintCountResponseItem" />
  <inputSet>
    <dataInputRefs>dataInputOfServiceTask</dataInputRefs>
  </inputSet>
  <outputSet>
    <dataOutputRefs>dataOutputOfServiceTask</dataOutputRefs>
  </outputSet>
</ioSpecification>

Web服务任务数据输入关联

有两种指定数据输入关联的方式:

  • 使用表达式

  • 使用简化方法

使用表达式指定数据输入关联,需要定义源及目标条目,并指定每个条目与字段的关联。下面的例子中,我们针对每个条目,指定prefix与suffix字段:

<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>

也可以使用更简单明了的简化方法。'sourceRef’元素是一个Flowable变量名,'targetRef’是条目定义的参数。在下面的例子里,将’PrefixVariable’变量的值关联至’prefix’字段,并将’SuffixVariable’变量的值关联至’suffix’字段。

<dataInputAssociation>
  <sourceRef>PrefixVariable</sourceRef>
  <targetRef>prefix</targetRef>
</dataInputAssociation>
<dataInputAssociation>
  <sourceRef>SuffixVariable</sourceRef>
  <targetRef>suffix</targetRef>
</dataInputAssociation>

Web服务任务数据输出关联

有两种指定数据输出关联的方式:

  • 使用表达式

  • 使用简化方法

使用表达式指定数据输出关联,需要定义目标变量及源表达式。这种方法很简单,与数据输入关联类似:

<dataOutputAssociation>
  <targetRef>dataOutputOfProcess</targetRef>
  <transformation>${dataOutputOfServiceTask.prettyPrint}</transformation>
</dataOutputAssociation>

也可以使用更简单明了的简化方法。'sourceRef’是条目定义的参数,'targetRef’元素是Flowable变量名。这种方法很简单,与数据输入关联类似:

<dataOutputAssociation>
  <sourceRef>prettyPrint</sourceRef>
  <targetRef>OutputVariable</targetRef>
</dataOutputAssociation>

8.5.5. 业务规则任务

描述

业务规则任务(business rule task)用于同步地执行一条或多条规则。Flowable使用名为Drools Expert的Drools规则引擎执行业务规则。目前,业务规则中包含的.drl文件,必须与定义了业务规则服务并执行规则的流程定义一起部署。这意味着流程中使用的所有.drl文件都需要打包在流程BAR文件中,与任务表单等类似。要了解如何为Drools Expert创建业务规则,请访问位于JBoss Drools的Drools文档。

如果想要使用自己的规则任务实现,比如希望通过不同方法使用Drools,或者想使用完全不同的规则引擎,则可以使用BusinessRuleTask的class或expression属性。这样它会与服务任务的行为完全相同。

图示

业务规则任务显示为带有表格图标的圆角矩形。

bpmn.business.rule.task

XML表示

要执行与流程定义在同一个BAR文件中部署的一条或多条业务规则,需要定义输入与结果变量。输入变量可以用流程变量的列表定义,使用逗号分隔。输出变量只能有一个变量名,将执行业务规则后的输出对象存储至流程变量。请注意结果变量会包含对象的List。如果没有指定结果变量名,默认为org.flowable.engine.rules.OUTPUT。

下面的业务规则任务,执行与流程定义一起部署的所有业务规则:

<process>

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

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

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

  <endEvent />

</process>

也可以将业务规则任务配置为只执行部署的.drl文件中的一组规则。要做到这一点,需要指定规则名字的列表,用逗号分隔。

<businessRuleTask flowable:ruleVariablesInput="${order}"
    flowable:rules="rule1, rule2" />

这样只会执行rule1与rule2。

也可以定义需要从执行中排除的规则列表。

<businessRuleTask flowable:ruleVariablesInput="${order}"
    flowable:rules="rule1, rule2" exclude="true" />

这个例子中,除了rule1与rule2之外,其它所有与流程定义一起部署在同一个BAR文件中的规则都会被执行。

前面提到过,还可以自行指定BusinessRuleTask的实现:

<businessRuleTask flowable:class="${MyRuleServiceDelegate}" />

这样配置的业务规则任务与服务任务的行为完全一样,但仍保持业务规则任务的图标,代表在这里处理业务规则。

8.5.6. 邮件任务

Flowable让你可以通过自动的邮件服务任务(email task),增强业务流程。可以向一个或多个收信人发送邮件,支持cc,bcc,HTML文本,等等。请注意邮件任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,邮件任务实现为一种特殊的服务任务。

配置邮件服务器

Flowable引擎使用支持SMTP的外部邮件服务器发送邮件。为了发送邮件,引擎需要了解如何连接邮件服务器。可以在flowable.cfg.xml配置文件中设置下面的参数:

参数必填?描述
mailServerHost邮件服务器的主机名(如mail.mycorp.com)。默认为localhost
mailServerPort是,如果不使用默认端口邮件服务器的SMTP端口。默认值为25
mailServerDefaultFrom若用户没有提供地址,默认使用的邮件发件人地址。默认为flowable@flowable.org
mailServerUsername若服务器需要部分邮件服务器发信时需要进行认证。默认为空。
mailServerPassword若服务器需要部分邮件服务器发信时需要进行认证。默认为空。
mailServerUseSSL若服务器需要部分邮件服务器要求ssl通信。默认设置为false。
mailServerUseTLS若服务器需要部分邮件服务器要求TLS通信(例如gmail)。默认设置为false。

定义邮件任务

邮件任务实现为特殊的服务任务,将服务任务的type定义为'mail'进行设置。

<serviceTask flowable:type="mail">

邮件任务通过字段注入配置。这些参数的值可以使用EL表达式,并将在流程执行运行时解析。可以设置下列参数:

参数必填?描述
to邮件的收信人。可以使用逗号分隔的列表定义多个接收人
from邮件的发信人地址。如果不设置,会使用默认配置的地址
subject邮件的主题
cc邮件的抄送人。可以使用逗号分隔的列表定义多个接收人
bcc邮件的密送人。可以使用逗号分隔的列表定义多个接收人
charset可以指定邮件的字符集,对许多非英语语言很必要。
html邮件的HTML文本
text邮件的内容,用于纯文本邮件。对于不支持富文本内容的客户端,可以与html一起使用。邮件客户端可以回退为显式纯文本格式。
htmlVar存储邮件HTML内容的流程变量名。与html参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。
textVar存储邮件纯文本内容的流程变量名。与text参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。
ignoreException处理邮件失败时,是忽略还是抛出FlowableException。默认设置为false。
exceptionVariableName如果设置ignoreException = true,而处理邮件失败时,则使用给定名字的变量保存失败信息

示例 usage

下面的XML代码片段是使用邮件任务的示例。

<serviceTask flowable:type="mail">
  <extensionElements>
  <flowable:field name="from" stringValue="order-shipping@thecompany.com" />
  <flowable:field name="to" expression="${recipient}" />
  <flowable:field name="subject" expression="Your order ${orderId} has been shipped" />
  <flowable:field name="html">
    <flowable: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>
    ]]>
    </flowable:expression>
  </flowable:field>
  </extensionElements>
</serviceTask>

8.5.7. Http任务

Http任务(Http task)用于发出HTTP请求,增强了Flowable的集成能力。请注意Http任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Http任务实现为一种特殊的服务任务。

配置Http客户端

Flowable使用可配置的Http客户端发出Http请求。如果不进行设置,会使用默认配置。

示例配置:

  <bean
    class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
  <!-- http客户端配置 -->
  <property name="httpClientConfig" ref="httpClientConfig"/>
  </bean>

  <bean class="org.flowable.engine.cfg.HttpClientConfig">
  <property name="connectTimeout" value="5000"/>
  <property name="socketTimeout" value="5000"/>
  <property name="connectionRequestTimeout" value="5000"/>
  <property name="requestRetryLimit" value="5"/>
  </bean>
参数必填?描述
connectTimeout连接超时时间,以毫秒计。
默认值 5000。
socketTimeoutSocket超时时间,以毫秒计。
默认值 5000。
connectionRequestTimeout请求连接超时时间。以毫秒计
默认值 5000。
requestRetryLimit请求重试次数(“0”代表不重试)
默认值 3。
disableCertVerify禁用SSL证书验证。
默认值 false。

定义Http任务

Http任务实现为特殊的服务任务,将服务任务的type定义为'http'进行设置。

<serviceTask flowable:type="http">

可以使用自定义的实现,覆盖Http任务的默认行为。 需要扩展org.flowable.http.HttpActivityBehavior,并覆盖perform()方法。

需要在任务定义中设置httpActivityBehaviorClass字段(默认值为 org.flowable.http.impl.HttpActivityBehaviorImpl)。

当前使用的默认实现HttpActivityBehaviorImpl基于Apache Http Client。尽管Apache Http Client可以使用很多方法自定义,但我们并没有在Http客户端配置中使用全部选项。

参考 Http Client builder 创建自定义客户端。

<serviceTask flowable:type="http">
  <extensionElements>
  <flowable:field name="httpActivityBehaviorClass">
    <flowable:string>
      <![CDATA[org.example.flowable.HttpActivityBehaviorCustomImpl]]>
    </flowable:string>
  </flowable:field>
  </extensionElements>
</sericeTask>

配置Http任务

Http任务通过字段注入配置。所有参数都可以配置为EL表达式,在运行时进行解析。可以设置下列参数:

参数必填?描述
requestMethod请求方法
(GET,POST,PUT,DELETE)。
requestUrlyes请求URL
(例如 - http://flowable.org)。
requestHeaders行分隔的Http请求头。
例如 -
Content-Type: application/json
Authorization: Basic aGFRlc3Q=
requestBody请求体
例如 - ${sampleBody}
requestTimeout请求超时时间。单位为毫秒
(例如 - 5000)。
默认值为“0”,即没有超时。
链接相关的超时设置为配置Http客户端。
disallowRedirects是否禁用Http重定向。
默认为false。
(例如 - true)。
failStatusCodes逗号分隔的Http状态码,将令请求失败并抛出FlowableException。
例如:400, 404, 500, 503
例如:400, 5XX
handleStatusCodes逗号分隔的Http状态码,将令任务抛出BpmnError,并可用错误边界事件捕获。
BpmnError的错误码为HTTP<statuscode>
例如,404状态码会将错误码设置为HTTP404
仅当disallowRedirects字段设置为true时,3XX状态码才会被抛出。若同一个状态码在handleStatusCodesfailStatusCodes中都有配置,则handleStatusCodes生效。
例如:400, 404, 500, 503
例如:3XX, 4XX, 5XX
ignoreException是否忽略异常。异常将被捕获,并存储在名为<taskId>.errorMessage的变量中。
saveRequestVariables是否保存请求变量。
默认情况下,只会保存将响应相关的变量。
saveResponseParameters是否保存全部的响应变量,包括HTTP状态码,响应头等。
默认情况下,只会将响应体保存为变量。
resultVariablePrefix执行变量名的前缀。
如果不设置前缀,变量名为<taskId>.fieldName
例如,对于id为task7的任务,其请求URL将保存为task7.requestUrl
saveResponseParametersTransient若为true,则会将响应体变量(如果设置了保存响应头,状态码,也包括在内)设置为瞬时变量。
saveResponseVariableAsJson若为true,则响应体会保存为JSON变量,而非String。如果HTTP服务返回JSON,并且想使用点注记方式使用字段(如myResponse.user.name),这个配置就很有用。
httpActivityBehaviorClassorg.flowable.http.HttpActivityBehavior类的自定义扩展的全限定类名。

除了上面提到的字段,使用saveResponseParameters还会在执行成功后设置下列变量。

变量可选?描述
responseProtocolHttp版本。
responseReasonHttp响应原因短语。
responseStatusCodeHttp响应状态码(例如 - 200)。
responseHeaders行分隔的Http响应头。
例如 -
Content-Type: application/json
Content-Length: 777
responseBody字符串形式的响应体,若有。
errorMessage被忽略的异常信息,若有。

结果变量

请注意上述执行变量名都会使用resultVariablePrefix前缀。 例如,可以在其他活动中,使用task7.responseStatusCode获取响应状态码。 其中task7是服务任务的id。可以设置resultVariablePrefix覆盖这个行为。

示例

下面的XML片段是使用Http任务的例子。

<serviceTask flowable:type="http">
  <extensionElements>
  <flowable:field name="requestMethod" stringValue="GET" />
  <flowable:field name="requestUrl" stringValue="http://flowable.org" />
  <flowable:field name="requestHeaders">
    <flowable:expression>
    <![CDATA[
      Accept: text/html
      Cache-Control: no-cache
    ]]>
    </flowable:expression>
  </flowable:field>
  <flowable:field name="requestTimeout">
    <flowable:expression>
    <![CDATA[
      ${requestTimeout}
    ]]>
    </flowable:expression>
  </flowable:field>
  <flowable:field name="resultVariablePrefix">
    <flowable:string>task7</flowable:string>
  </flowable:field>
  </extensionElements>
</serviceTask>

错误处理

默认情况下,当发生链接、IO或其他未处理的异常时,Http任务抛出FlowableException。 默认情况下,不会处理任何重定向/客户端/服务端错误状态码。

可以设置failStatusCodes及/或handleStatusCodes字段,配置Http任务处理异常及Http状态的方式。参见配置Http任务。

handleStatusCodes抛出的BpmnError与其他BPMN异常一样,需要由对应的错误边界事件处理。 下面是一些Http任务错误处理及重试的例子。

400及5XX失败,异步执行,并按照failedJobRetryTimeCycle重试的Http任务

<serviceTask name="Fail test" flowable:async="true" flowable:type="http">
  <extensionElements>
  <flowable:field name="requestMethod">
    <flowable:string><![CDATA[GET]]></flowable:string>
  </flowable:field>
  <flowable:field name="requestUrl">
    <flowable:string><![CDATA[http://localhost:9798/api/fail]]></flowable:string>
  </flowable:field>
  <flowable:field name="failStatusCodes">
    <flowable:string><![CDATA[400, 5XX]]></flowable:string>
  </flowable:field>
  <flowable:failedJobRetryTimeCycle>R3/PT5S</flowable:failedJobRetryTimeCycle>
  </extensionElements>
</serviceTask>

400处理为BmpnError

<serviceTask name="HTTP Task" flowable:type="http">
  <extensionElements>
  <flowable:field name="requestMethod">
    <flowable:string><![CDATA[GET]]></flowable:string>
  </flowable:field>
  <flowable:field name="requestUrl">
    <flowable:string><![CDATA[http://localhost:9798/api/fail]]></flowable:string>
  </flowable:field>
  <flowable:field name="handleStatusCodes">
    <flowable:string><![CDATA[4XX]]></flowable:string>
  </flowable:field>
  </extensionElements>
</serviceTask>
<boundaryEvent attachedToRef="handleGet">
  <errorEventDefinition errorRef="HTTP400"></errorEventDefinition>
</boundaryEvent>

忽略异常

<serviceTask name="Fail test" flowable:type="http">
  <extensionElements>
  <flowable:field name="requestMethod">
    <flowable:string><![CDATA[GET]]></flowable:string>
  </flowable:field>
  <flowable:field name="requestUrl">
    <flowable:string><![CDATA[http://nohost:9798/api]]></flowable:string>
  </flowable:field>
  <flowable:field name="ignoreException">
    <flowable:string><![CDATA[true]]></flowable:string>
  </flowable:field>
  </extensionElements>
</serviceTask>

异常映射

参见异常映射

8.5.8. Mule任务

Mule任务可以向Mule发送消息,增强Flowable的集成特性。请注意Mule任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Mule任务实现为一种特殊的服务任务。

定义Mule任务

Mule任务实现为特殊的服务任务,将服务任务的type定义为'mule'进行设置。

<serviceTask flowable:type="mule">

Mule任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:

参数必填?描述
endpointUrl希望调用的Mule端点(endpoint)。
language计算payloadExpression字段所用的语言。
payloadExpression消息载荷的表达式。
resultVariable存储调用结果的变量名。

示例

下面的XML代码片段是使用Mule任务的例子。

<extensionElements>
  <flowable:field name="endpointUrl">
  <flowable:string>vm://in</flowable:string>
  </flowable:field>
  <flowable:field name="language">
  <flowable:string>juel</flowable:string>
  </flowable:field>
  <flowable:field name="payloadExpression">
  <flowable:string>"hi"</flowable:string>
  </flowable:field>
  <flowable:field name="resultVariable">
  <flowable:string>theVariable</flowable:string>
  </flowable:field>
</extensionElements>

8.5.9. Camel任务

Camel任务(Camel task)可以向Camel发送消息,增强Flowable的集成特性。请注意Camel任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Camel任务实现为一种特殊的服务任务。还请注意,需要在项目中包含Flowable Camel模块才能使用Camel任务。

定义Camel任务

Camel任务实现为特殊的服务任务,将服务任务的type定义为'camel'进行设置。

<serviceTask flowable:type="camel">

只需要在流程定义的服务任务上定义Camel类型即可。集成逻辑都通过Camel容器委托。默认情况下Flowable引擎在Spring容器中查找camelContext Bean。camelContext Bean定义由Camel容器装载的Camel路由。在下面的例子中,按照指定的Java包装载路由。但也可以自行在Spring配置中直接定义路由。

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <packageScan>
  <package>org.flowable.camel.route</package>
  </packageScan>
</camelContext>

可以在Camel网站找到关于Camel路由的更多文档。下面只通过几个小例子展示基本概念。在第一个例子中,在Flowable工作流中进行最简单的Camel调用。叫做SimpleCamelCall。

如果想要定义多个Camel上下文Bean,或想使用不同的Bean名字,可以在Camel任务定义中像这样覆盖:

<serviceTask flowable:type="camel">
  <extensionElements>
  <flowable:field name="camelContext" stringValue="customCamelContext" />
  </extensionElements>
</serviceTask>

简单Camel调用示例

这个例子相关的所有文件,都可以在flowable-camel模块的org.flowable.camel.examples.simpleCamelCall包中找到。目标是简单地启动一个Camel路由。首先需要一个配置了上面提到的路由的Spring上下文。下面的代码实现这个目的:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <packageScan>
  <package>org.flowable.camel.examples.simpleCamelCall</package>
  </packageScan>
</camelContext>
public class SimpleCamelCallRoute extends RouteBuilder {

  @Override
  public void configure() throws Exception {
  from("flowable:SimpleCamelCallProcess:simpleCall").to("log:org.flowable.camel.examples.SimpleCamelCall");
  }
}

路由只是记录消息体,不做更多事情。请注意from端点(endpoint)的格式,包含冒号分隔的三个部分:

端点URL部分描述
flowable指向引擎端点
SimpleCamelCallProcess流程名
simpleCall流程中Camel服务的名字

现在已经配置好路由,可以访问Camel。下面需要像这样定义工作流:

<process>
  <startEvent/>
  <sequenceFlow sourceRef="start" targetRef="simpleCall"/>

  <serviceTask flowable:type="camel"/>

  <sequenceFlow sourceRef="simpleCall" targetRef="end"/>
  <endEvent/>
</process>

连通性测试示例

示例已经可以工作,但实际上Camel与Flowable之间并没有通信,因此没有太多价值。在这个例子里,将试着从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>
  <startEvent/>
  <sequenceFlow sourceRef="start" targetRef="ping"/>
  <serviceTask flowable:type="camel"/>
  <sequenceFlow sourceRef="ping" targetRef="saveOutput"/>
  <serviceTask flowable:class="org.flowable.camel.examples.pingPong.SaveOutput" />
  <sequenceFlow sourceRef="saveOutput" targetRef="end"/>
  <endEvent/>
</process>

请注意SaveOutput服务任务会从上下文中取出“Output”变量,并存储至上面提到的OutputMap。现在需要了解变量如何发送至Camel,以及如何返回。这就需要了解Camel行为(Behavior)的概念。变量与Camel通信的方式可以通过CamelBehavior配置。在这个例子里使用默认配置,其它配置在后面会进行简短介绍。可以使用类似的代码配置期望的Camel行为:

<serviceTask flowable:type="camel">
  <extensionElements>
  <flowable:field name="camelBehaviorClass" stringValue="org.flowable.camel.impl.CamelBehaviorCamelBodyImpl" />
  </extensionElements>
</serviceTask>

如果不指定行为,则会设置为org.flowable.camel.impl.CamelBehaviorDefaultImpl。这个行为会将变量复制到相同名字的Camel参数。无论选择什么行为,对于返回值:如果Camel消息体是一个map,则其中的每个元素都将复制为变量;否则整个对象将复制为名为"camelBody"的特定变量。以第二个例子作为Camel路由的总结:

@Override
public void configure() throws Exception {
  from("flowable:PingPongProcess:ping").transform().simple("${property.input} World");
}

在这个路由中,字符串"world"会在结尾连接上名为“input”的参数,并将结果作为消息体返回。可以通过Java服务任务访问"camelBody"变量。也可以访问“outputMap”获取。除了这个例子中使用的默认行为之外,我们还可以看看其他的方式。在每个Camel路由开始时,流程实例ID会复制为名为"PROCESS_ID_PROPERTY"的Camel参数。之后会用于将流程实例与Camel路由相关联。也可以在Camel路由中直接使用。

Flowable提供了三种不同的行为。可以通过修改路由URL中特定的部分覆写行为。这里有个在URL中覆写已有行为的例子:

from("flowable:asyncCamelProcess:serviceTaskAsync2?copyVariablesToProperties=true").

下表展示了三种可用的Camel行为:

行为URL中描述
CamelBehaviorDefaultImplcopyVariablesToProperties将Flowable变量复制为Camel参数。
CamelBehaviorCamelBodyImplcopyCamelBodyToBody只将名为"camelBody"的Flowable变量复制为Camel消息体。
CamelBehaviorBodyAsMapImplcopyVariablesToBodyAsMap将一个map中的所有Flowable变量复制为Camel消息体。

上表展示了Flowable变量如何传递给Camel。下表展示Camel变量如何返回至Flowable。需要在路由URL中进行配置。

URL描述
Default如果Camel消息体是一个map,则将其中每一对象复制为Flowable变量;否则将整个Camel消息体复制为"camelBody" Flowable变量。
copyVariablesFromProperties将Camel参数以同名复制为Flowable变量。
copyCamelBodyToBodyAsString与default相同,但如果Camel消息体不是map,则首先将其转换为字符串,然后再复制为"camelBody"。
copyVariablesFromHeader额外地,将Camel头复制为Flowable的同名变量。

返回变量

上面提到的变量传递,不论是从Camel到Flowable还是反向,都只用于变量传递的起始侧。 要特别注意,由于Flowable的行为是非阻塞的,Flowable不会自动向Camel返回变量。 为此提供了特殊的语法。可以在Camel路由URL中,以var.return.someVariableName的格式,指定一个或多个参数。与这些参数同名(但没有var.return部分)的变量会作为输出变量。因此将会以相同的名字复制回Camel参数。
例如在如下路由中:

from("direct:start").to("flowable:process?var.return.exampleVar").to("mock:result");

名为exampleVar的Flowable变量会作为输出变量。因此会以同名复制回Camel参数。

异步连通性测试示例

上面的例子全都是同步的。流程实例等待,直到Camel路由结束并返回。有时,需要Flowable流程实例继续运行。这时可以使用Camel服务任务的异步功能。可以将Camel服务任务的async参数设置为true,启用这个功能。

<serviceTask flowable:type="camel" flowable:async="true"/>

设置这个参数后,Camel路由会由Flowable作业执行器异步启动。如果在Camel路由中定义了队列,Flowable流程实例会继续执行流程定义中Camel服务任务之后的活动。Camel路由会与流程执行完全异步地执行。如果需要在流程定义的某处等待Camel服务任务的响应,可以使用接收任务(receive task)。

<receiveTask name="Wait State" />

流程实例会等待,直到接收到来自Camel的信号。可以在Camel中向特定的Flowable端点发送消息,来为流程实例发送信号。

 from("flowable:asyncPingProcess:serviceAsyncPing").to("flowable:asyncPingProcess:receiveAsyncPing");
  • “flowable”字符串常量

  • 流程名

  • 接收任务名

使用Camel路由实例化工作流

上面的例子都是先启动Flowable流程实例,然后在流程实例中启动Camel路由。也可以反过来,在已经启动的Camel路由中启动或调用流程实例。类似于为接收任务发送消息。例如,一个简单的路由:

from("direct:start").to("flowable:camelProcess");

可以看到,URL包含两部分:第一部分是“flowable”字符串常量,第二部分是流程定义的名字。当然,需要提前在Flowable引擎中部署这个流程定义。

也可以在Camel头中,将流程实例起动人设置为某个已认证用户ID。为此,首先需要在流程定义中指定启动人变量:

<startEvent flowable:initiator="initiator" />

然后使用Camel头CamelProcessInitiatorHeader指定用户ID。Camel路由定义如下:

from("direct:startWithInitiatorHeader")
  .setHeader("CamelProcessInitiatorHeader", constant("kermit"))
  .to("flowable:InitiatorCamelCallProcess?processInitiatorHeaderName=CamelProcessInitiatorHeader");

8.5.10. 手动任务

描述

手动任务(manual task)定义在BPM引擎之外的任务。它用于建模引擎不需要了解,也不需要提供系统或用户界面的工作。对于引擎来说,手动任务将按直接穿过活动处理,在流程执行到达手动任务时,自动继续执行流程。

图示

手动任务用左上角有一个小“手”图标的标准BPMN 2.0任务(圆角矩形)表示。

bpmn.manual.task

XML表示

<manualTask name="Call client for more information" />

8.5.11. Java接收任务

描述

接收任务(receive task),是等待特定消息到达的简单任务。目前,我们只为这个任务实现了Java语义。当流程执行到达接收任务时,流程状态将提交至持久化存储。这意味着流程将保持等待状态,直到引擎接收到特定的消息,触发流程穿过接收任务继续执行。

图示

接收任务用左上角有一个消息图标的标准BPMN 2.0任务(圆角矩形)表示。消息图标是白色的(对应的黑色消息图标代表发送的含义)。

bpmn.receive.task

XML表示

<receiveTask name="wait" />

要使流程实例从接收任务的等待状态中继续执行,需要使用到达接收任务的执行id,调用runtimeService.signal(executionId)。下面的代码片段展示了如何操作:

ProcessInstance pi = runtimeService.startProcessInstanceByKey("receiveTask");
Execution execution = runtimeService.createExecutionQuery()
  .processInstanceId(pi.getId())
  .activityId("waitState")
  .singleResult();
assertNotNull(execution);

runtimeService.trigger(execution.getId());

8.5.12. Shell任务

描述

Shell任务(Shell task)可以运行Shell脚本与命令。请注意Shell任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。

定义Shell任务

Shell任务实现为特殊的服务任务,将服务任务的type定义为'shell'进行设置。

<serviceTask flowable:type="shell">

Shell任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:

参数必填?类型描述默认值
commandString要执行的Shell命令。
arg0-5String参数0至参数5
waittrue/false是否等待Shell进程终止。true
redirectErrortrue/false是否将标准错误(standard error)并入标准输出(standard output)。false
cleanEnvtrue/false是否避免Shell进程继承当前环境。false
outputVariableString保存输出的变量名不会记录输出。
errorCodeVariableString保存结果错误码的变量名不会记录错误码。
directoryStringShell进程的默认目录当前目录

示例

下面的XML代码片段是使用Shell任务的例子。将会运行"cmd /c echo EchoTest" Shell脚本,等待其结束,并将其结果存入resultVar

<serviceTask flowable:type="shell" >
  <extensionElements>
  <flowable:field name="command" stringValue="cmd" />
  <flowable:field name="arg1" stringValue="/c" />
  <flowable:field name="arg2" stringValue="echo" />
  <flowable:field name="arg3" stringValue="EchoTest" />
  <flowable:field name="wait" stringValue="true" />
  <flowable:field name="outputVariable" stringValue="resultVar" />
  </extensionElements>
</serviceTask>

8.5.13. 执行监听器

执行监听器(execution listener)可以在流程执行中发生特定的事件时,执行外部Java代码或计算表达式。可以被捕获的事件有:

  • 流程实例的启动和结束。

  • 流程执行转移。

  • 活动的启动和结束。

  • 网关的启动和结束。

  • 中间事件的启动和结束。

  • 启动事件的结束,和结束事件的启动。

下面的流程定义包含了三个执行监听器:

<process>

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

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

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

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

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

  <endEvent />

</process>

第一个执行监听器将在流程启动时收到通知。这个监听器是一个外部Java类(ExampleExecutionListenerOne),并且需要实现org.flowable.engine.delegate.ExecutionListener接口。当该事件发生时(这里是start事件),会调用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.flowable.engine.delegate.JavaDelegate接口的委托类。这些委托类也可以用于其他的结构,如服务任务的委托。

第二个执行监听器在流程执行转移时被调用。请注意listener元素并未定义event,因为在转移上只会触发take事件。当监听器定义在转移上时,event属性的值将被忽略。

最后一个执行监听器在secondTask活动结束时被调用。监听器声明中没有使用class,而是定义了expression。这个表达式将在事件触发时计算/调用。

<flowable:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />

与其他表达式一样,可以使用与解析execution变量。execution对象提供了露事件名参数,可以使用execution.eventName向你的方法传递事件名。

与服务任务类似,执行监听器也支持使用delegateExpression

<flowable:executionListener event="start" delegateExpression="${myExecutionListenerBean}" />

较早之前,我们也引入了新的执行监听器类型,org.flowable.engine.impl.bpmn.listener.ScriptExecutionListener。这个脚本执行监听器可以为执行监听器事件执行一段脚本代码。

<flowable:executionListener event="start"
  class="org.flowable.engine.impl.bpmn.listener.ScriptExecutionListener">

  <flowable:field name="script">
  <flowable:string>
    def bar = "BAR";  // 局部变量
    foo = "FOO"; // 将变量放入执行上下文
    execution.setVariable("var1", "test"); // 测试访问执行实例
    bar // 隐式返回值
  </flowable:string>
  </flowable:field>
  <flowable:field name="language" stringValue="groovy" />
  <flowable:field name="resultVariable" stringValue="myVar" />

</flowable:executionListener>

执行监听器上的字段注入

使用通过class属性配置的执行监听器时,可以使用字段注入。与服务任务字段注入使用完全相同的机制,可以在那里看到字段注入的各种用法。

下面的代码片段展示了一个简单的示例流程,带有一个使用了字段注入的执行监听器。

<process>
  <extensionElements>
  <flowable:executionListener
    class="org.flowable.examples.bpmn.executionListener.ExampleFieldInjectedExecutionListener"
    event="start">

    <flowable:field name="fixedValue" stringValue="Yes, I am " />
    <flowable:field name="dynamicValue" expression="${myVar}" />

  </flowable:executionListener>
  </extensionElements>

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

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

  <endEvent />
</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类将连接两个字段(一个是固定值-fixedValue,另一个是动态值-dynamicValue),并将其存储在'var'流程变量中。

@Deployment(resources = {
  "org/flowable/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);

  // 结果为固定注入字段及注入表达式的连接
  assertEquals("Yes, I am listening!", varSetByListener);
}

请注意,与服务任务使用相同的线程安全规则。请阅读相应章节了解更多信息。

8.5.14. 任务监听器

任务监听器(task listener)用于在特定的任务相关事件发生时,执行自定义的Java逻辑或表达式。

任务监听器只能在流程定义中作为用户任务的子元素。请注意,任务监听器是一个Flowable自定义结构,因此也需要作为BPMN 2.0 extensionElements,放在flowable命名空间下。

<userTask name="My Task" >
  <extensionElements>
  <flowable:taskListener event="create" class="org.flowable.MyTaskCreateListener" />
  </extensionElements>
</userTask>

任务监听器包含下列属性:

  • event(事件)(必填):触发任务监听器的任务事件类型。可用的事件有:

    • create(创建):当任务已经创建,并且所有任务参数都已经设置时触发。

    • assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,在触发create事件之前,会首先触发assignment事件。这顺序看起来不太自然,但是有实际原因的:当收到create事件时,我们通常希望能看到任务的所有参数,包括办理人。

    • complete(完成):当任务已经完成,从运行时数据中删除前触发。

    • delete(删除):在任务即将被删除前触发。请注意任务由completeTask正常完成时也会触发。

  • class:需要调用的委托类。这个类必须实现org.flowable.engine.delegate.TaskListener接口。

public class MyTaskCreateListener implements TaskListener {

  public void notify(DelegateTask delegateTask) {
  // 这里是要实现的业务逻辑
  }
}

也可以使用字段注入,为委托类传递流程变量或执行。请注意委托类的实例在流程部署时创建(与Flowable中其它的委托类一样),这意味着该实例会在所有流程实例执行中共享。

  • expression:(不能与class属性一起使用):指定在事件发生时要执行的表达式。可以为被调用的对象传递DelegateTask对象与事件名(使用task.eventName)作为参数。

<flowable:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
  • delegateExpression:指定一个能够解析为TaskListener接口实现类的对象的表达式。与服务任务类似。

<flowable:taskListener event="create" delegateExpression="${myTaskListenerBean}" />
  • 较早之前,我们也引入了新的执行监听器类型,org.flowable.engine.impl.bpmn.listener.ScriptTaskListener。这个脚本任务监听器可以为一个任务监听器事件执行一段脚本代码。

<flowable:taskListener event="complete" class="org.flowable.engine.impl.bpmn.listener.ScriptTaskListener" >
  <flowable:field name="script">
  <flowable:string>
    def bar = "BAR";  // 局部变量
    foo = "FOO"; // 将变量放入执行上下文
    task.setOwner("kermit"); // 测试访问任务实例
    bar // 隐式返回值
  </flowable:string>
  </flowable:field>
  <flowable:field name="language" stringValue="groovy" />
  <flowable:field name="resultVariable" stringValue="myVar" />
</flowable:taskListener>

8.5.15. 多实例(for each)

描述

多实例活动(multi-instance activity)是在业务流程中,为特定步骤定义重复的方式。在编程概念中,多实例类似for each结构:可以为给定集合中的每一条目,顺序或并行地,执行特定步骤,甚至是整个子流程。

多实例是一个普通活动,加上定义(被称作“多实例特性的”)额外参数,会使得活动在运行时被多次执行。下列活动可以成为多实例活动:

  • 用户任务

  • 脚本任务

  • Java服务任务

  • Web服务任务

  • 业务规则任务

  • 邮件任务

  • 人工任务

  • 接收任务

  • (嵌入式)子流程

  • 调用活动

网关与事件不能设置为多实例。

按照BPMN2.0规范的要求,用于为每个实例创建执行的父执行,会提供下列变量:

  • nrOfInstances:实例总数。

  • nrOfActiveInstances:当前活动的(即未完成的),实例数量。对于顺序多实例,这个值总为1。

  • nrOfCompletedInstances:已完成的实例数量。

可以调用execution.getVariable(x)方法获取这些值。

另外,每个被创建的执行,都有局部变量(对其他执行不可见,也不存储在流程实例级别):

  • loopCounter:给定实例在for-each循环中的index。可以通过Flowable的elementIndexVariable属性为loopCounter变量重命名。

图示

如果一个活动是多实例,将通过在该活动底部的三条短线表示。三条线代表实例会并行执行,而三条线代表顺序执行。

bpmn.multi.instance

XML表示

要将活动变成多实例,该活动的XML元素必须有multiInstanceLoopCharacteristics子元素

<multiInstanceLoopCharacteristics isSequential="false|true">
 ...
</multiInstanceLoopCharacteristics>

isSequential属性代表了活动的实例为顺序还是并行执行。

实例的数量在进入活动时,计算一次。有几种不同方法可以配置数量。一个方法是通过loopCardinality子元素,直接指定数字。

<multiInstanceLoopCharacteristics isSequential="false|true">
  <loopCardinality>5</loopCardinality>
</multiInstanceLoopCharacteristics>

也可以使用解析为正整数的表达式:

<multiInstanceLoopCharacteristics isSequential="false|true">
  <loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
</multiInstanceLoopCharacteristics>

另一个定义实例数量的方法,是使用loopDataInputRef子元素,指定一个集合型流程变量的名字。对集合中的每一项,都会创建一个实例。可以使用inputDataItem子元素,将该项设置给该实例的局部变量。在下面的XML示例中展示:

<userTask name="My Task ${loopCounter}" flowable:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="false">
  <loopDataInputRef>assigneeList</loopDataInputRef>
  <inputDataItem name="assignee" />
  </multiInstanceLoopCharacteristics>
</userTask>

假设变量assigneeList包含[kermit, gonzo, fozzie]。上面的代码会创建三个并行的用户任务。每一个执行都有一个名为assignee的(局部)流程变量,含有集合中的一项,并在这个例子中被用于指派用户任务。

loopDataInputRefinputDataItem的缺点是名字很难记,并且由于BPMN 2.0概要的限制,不能使用表达式。Flowable通过在multiInstanceCharacteristics上提供collectionelementVariable属性解决了这些问题:

<userTask name="My Task" flowable:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="true"
   flowable:collection="${myService.resolveUsersForTask()}" flowable:elementVariable="assignee" >
  </multiInstanceLoopCharacteristics>
</userTask>

请注意collection属性会作为表达式进行解析。如果表达式解析为字符串而不是一个集合,不论是因为本身配置的就是静态字符串值,还是表达式计算结果为字符串,这个字符串都会被当做变量名,在流程变量中用于获取实际的集合。

例如,下面的代码片段会要求集合存储在assigneeList流程变量中:

<userTask name="My Task" flowable:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="true"
   flowable:collection="assigneeList" flowable:elementVariable="assignee" >
  </multiInstanceLoopCharacteristics>
</userTask>

假如myService.getCollectionVariableName()返回字符串值,引擎就会用这个值作为变量名,获取流程变量保存的集合。

<userTask name="My Task" flowable:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="true"
   flowable:collection="${myService.getCollectionVariableName()}" flowable:elementVariable="assignee" >
  </multiInstanceLoopCharacteristics>
</userTask>

多实例活动在所有实例都完成时结束。然而,也可以指定一个表达式,在每个实例结束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例活动,继续执行流程。这个表达式必须通过completionCondition子元素定义。

<userTask name="My Task" flowable:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="false"
   flowable:collection="assigneeList" flowable:elementVariable="assignee" >
  <completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

在这个例子里,会为assigneeList集合中的每个元素创建并行实例。当60%的任务完成时,其他的任务将被删除,流程继续运行。

边界事件与多实例

多实例是普通活动,因此可以在其边界定义边界事件。如果是中断边界事件,当其捕获事件时,会销毁活动中的所有实例。以下面的多实例子流程为例:

bpmn.multi.instance.boundary.event

当定时器触发时,子流程的所有实例都会被销毁,无论有多少实例,或者实例的内部活动是否完成。

多实例与执行监听器

执行监听器与多实例一起使用时需要特别注意。以下面的BPMN 2.0 XML代码片段为例。这段XML定义在与multiInstanceLoopCharacteristics XML元素相同的级别:

<extensionElements>
  <flowable:executionListener event="start" class="org.flowable.MyStartListener"/>
  <flowable:executionListener event="end" class="org.flowable.MyEndListener"/>
</extensionElements>

对于普通的BPMN活动,会在活动开始与结束时调用一次监听器。

但是当该活动为多实例时,行为有区别:

  • 当进入多实例活动时,在任何内部活动执行前,抛出一个启动事件。loopCounter变量还未设置(为null)。

  • 进入每个实际执行的活动时,抛出一个启动事件。loopCounter变量已经设置。

结束事件类似:

  • 离开每个实际执行的活动后,抛出一个结束事件。loopCounter变量已经设置。

  • 多实例活动整体完成后,抛出一个结束事件。loopCounter变量未设置。

例如:

<subProcess name="Sub Process">
  <extensionElements>
  <flowable:executionListener event="start" class="org.flowable.MyStartListener"/>
  <flowable:executionListener event="end" class="org.flowable.MyEndListener"/>
  </extensionElements>
  <multiInstanceLoopCharacteristics isSequential="false">
  <loopDataInputRef>assignees</loopDataInputRef>
  <inputDataItem name="assignee"></inputDataItem>
  </multiInstanceLoopCharacteristics>
  <startEvent name="Start"></startEvent>
  <endEvent name="End"></endEvent>
  <sequenceFlow name="" sourceRef="startevent2" targetRef="endevent2"></sequenceFlow>
</subProcess>

在这个例子中,假设assignees有三项。在运行时会发生如下事情:

  • 多实例整体抛出一个启动事件。调用一次start执行监听器,loopCounterassignee变量均未设置(即为null)。

  • 每一个活动实例抛出一个启动事件。调用三次start执行监听器,loopCounterassignee变量均已设置(也就是说不为null)。

  • 因此启动执行监听器总共被调用四次。

请注意,即使multiInstanceLoopCharacteristics不是定义在子流程上,也是一样。例如,如果上面的例子中只是一个简单的用户任务,抛出事件的行为也是一样。

8.5.16. 补偿处理器

描述

如果要使用一个活动补偿另一个活动的影响,可以将其声明为补偿处理器(compensation handler)。补偿处理器不在正常流程中执行,而只在流程抛出补偿事件时才会执行。

补偿处理器不得有入口或出口顺序流。

补偿处理器必须通过单向的连接,关联一个补偿边界事件。

图示

如果一个活动是补偿处理器,则会在其下部中间显示补偿事件图标。下面摘录的流程图展示了一个带有补偿边界事件的服务任务,并关联至一个补偿处理器。请注意补偿处理器图标显示在"cancel hotel reservation(取消酒店预订)"服务任务的下部中间。

bpmn.boundary.compensation.event

XML表示

要将一个活动声明为补偿处理器,需要将isForCompensation属性设置为true:

<serviceTask isForCompensation="true" flowable:class="...">
</serviceTask>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文