服务任务

概览

服务任务(Service task) 用于调用 Java 逻辑、外部服务、API 或自动化程序。

图形表示法

服务任务在图中显示为一个圆角矩形,左上角带一个齿轮图标。

service task
XML 表示

根据不同的实现,服务任务的 XML 可能不同。请参阅相应任务类型介绍中的示例。

属性

服务任务有下列属性:

service task properties

设置任务属性:

  • 首先需要设置 通用任务属性

  • 然后,定义一个 任务类型(Task type) 并根据所选的 类型 设置必要的参数。

  • 配置 失败重试 或者使用默认设置(留空)。

  • 如果需要,创建 执行监听器(Execution listeners)。参阅 监听器

  • 确定是否需要 扩展属性

  • 如果需要创建多实例,参阅 多实例

  • 关于 异步(Async),参阅 事务

事实上,建议总是使用 Async 任务。 在某些情况下,可以提升性能。

任务的类型

任务类型 声明 Java 逻辑从服务任务中调用的方式:

  • Spring Bean:指定一个 Spring Bean、方法和方法参数。

  • Java 代理类:提供一个类,需要实现 JavaDelegateFutureJavaDelegate

  • 表达式:调用一个方法表达式。

  • 代理表达式:计算一个表达式,但是这个表达式的结果应该为一个代理对象。

Spring Bean

Jmix BPM 支持从服务任务中调用 Spring bean 方法并为其提供参数值。

可以分别使用 Bean NameMethod 字段选择现有的 bean 及其关联的方法。

选择方法后,会出现更多字段用于输入方法参数的值。

请注意 is variable 复选框。主要对字符串参数有意义。 如果未勾选该复选框,则形成的表达式中,该参数将作为字符串输入(带单引号)。 如果勾选该复选框,则不会添加单引号,并且使用同名变量值作为参数。

  • ${smpl_MyBean.someMethod('description')} — 该表达式将使用字符串值 description

  • ${smpl_MyBean.someMethod(description)} — 该表达式将使用名为 description 的变量值。

结果变量

如果选择的方法有返回值,则会显示 Result variable(结果变量) 字段。 可以在此处使用一个已有的流程变量,也可以输入名称创建一个新的流程变量。

使用已有变量时请注意变量类型。 如果结果类型与已有变量的类型不同,则将创建一个相同名称的新流程变量。 例如,如果已经有一个 String 变量 a1,但是选择用它保存数值结果 100L,则将创建一个类型为 Long 且值为 100 的新变量 a1

Result variable(结果变量) 旁边有一个 Use local scope(使用本地作用域) 复选框。如果设置为 true,则服务任务创建的结果变量作用域只是在本地,即当前任务的上下文中。 也就是说,该变量只能在当前执行过程中访问,不会传播到父执行过程或流程实例。

该设置可以将变量与服务任务的特定执行过程隔离开来。 如果同一服务任务的 多实例 同时运行, 则每个实例都有自己的本地变量,不会发生变量互相干扰的情况。

XML 表示

下面是 Spring bean 服务任务参数在 XML 中的表示:

<serviceTask id="set-status-service-task" name="Set order status"
    flowable:async="true" (1)
    flowable:expression="${smpl_OrderStatusBean.setStatus(orderId,&#39;Sent&#39;)}" (2)
    flowable:resultVariable="quantity" (3)
    flowable:useLocalScopeForResultVariable="true" (4)
    jmix:taskType="springBean" jmix:beanName="smpl_OrderStatusBean"> (5)
  <extensionElements>
    <jmix:springBean beanName="smpl_OrderStatusBean"
     methodName="setStatus"> (6)
      <jmix:methodParam name="orderId" type="java.lang.String"
        isVariable="true">orderId</jmix:methodParam> (7)
      <jmix:methodParam name="status" type="java.lang.String"
        isVariable="false">Sent</jmix:methodParam> (8)
    </jmix:springBean>
  </extensionElements>
</serviceTask>
1  — 异步(Async) 标记,默认是 false 并省略。
2  — 通用表达式,单引号用 ' 转义。
3  — 结果变量。
4  — 本地变量,默认是 false 并省略。
5  — 任务类型。
6  — Spring bean 和方法。
7  — 流程变量传递参数。
8  — 值传递参数。

“execution” 流程变量

Spring bean 看不到流程执行的上下文。 但是在很多情况下需要获取流程执行的上下文。 例如,访问流程变量和当前的任务属性。

有一个名为 “execution” 的嵌入式流程变量,其类型为 DelegateExecution,可以用作 Spring bean 的方法参数。 创建一个这样的方法,例如:

@Component("MyProcessBean")
public class MyProcessBean {

    public void mySampleMethod(DelegateExecution execution) { (1)
        String currentActivityId = execution.getCurrentActivityId();
        Set<String> variableNames = execution.getVariableNames();
        // etc.
    }
}
1  — execution 参数

然后在服务任务中设置:

execution as parameter

Java 代理类

在这种类型的服务任务中,业务逻辑将由一个实现了 org.flowable.engine.delegate.JavaDelegateorg.flowable.engine.delegate.FutureJavaDelegate 接口的类的 execute() 方法执行。方法接收一个 execution 对象作为参数,因此可以访问流程上下文,包括全部流程变量。

如果在 Task type 下拉框中选择 JavaDelegate class,则可以继续选择需要使用的类:

create java delegate

例如,我们实现一个带有随机值的流程变量:

public class RandomIndexJavaDelegate implements JavaDelegate {
    @Override
    public void execute(DelegateExecution execution) {
        long randomIndex = new Random().nextLong(100L);
        execution.setVariable("randomIndex", randomIndex);
    }
}
XML 表示

在 XML 中,类名需要在 flowable:class 属性中设置, 且使用全限定名称。

<serviceTask id="Activity_java_delegate" name="Java delegate"
    flowable:class="com.company.jmixbpmtraining.delegate.RandomIndexJavaDelegate" (1)
    jmix:taskType="javaDelegateClass"> (2)
  <extensionElements />
</serviceTask>
1  — 指定 Java 代理类。
2  — 定义任务类型。

实例化 Java 代理类

Java Delegate 类型的服务任务中使用的类 在部署期间不会实例化。 当流程引擎在执行期间首次到达这个任务时,会创建一个 JavaDelegate 类的实例。

对于一个使用 Java 代理的服务任务,只会实例化一个代理类的实例。 如果一个流程中的多个服务任务使用的是同一个 Java 代理类, 那么将为每个服务任务创建一个单独的代理类实例。 所有流程实例共享任务的对应代理类实例。

java delegate instantiating

也就是说,不同线程可能会同时执行同一个代理类,因此代理类中不能使用任何成员变量,且必须保证线程安全。 可能也会影响 字段注入

代理表达式

代理表达式(delegate expression) 是服务任务中的一个强大功能,可以在运行时动态确定 Java 对象。 例如,${myServiceBean} 表达式会解析为一个名为 myServiceBean 的 Spring bean。

在 Spring 上下文中,代理表达式可以直接引用 Spring bean,无缝集成 Spring 框架。 因此,可以在代理实现中使用依赖注入并使用 Spring 的功能。

为了能在代理表达式中使用代理类,与 Spring bean 类似,这些类需要带有 @Component 注解。 此时,两种类型功能都可使用 — Spring beanJava 代理类

@Component
public class MyDelegateExpression implements JavaDelegate {
    // Class fields and injections
    @Override
    public void execute(DelegateExecution execution) {
    // Required logic
    }
}

最终,我们可以从该类中访问 Spring 上下文和流程上下文。 调用时,请使用 Delegate expression 任务类型。例如:

delegate expression
XML 表示

如需指定服务任务在流程执行期间调用的类,可以使用解析为对象的表达式。 在 XML 中,可以使用 flowable:delegateExpression 属性:

<serviceTask id="delegate-expression"
    name="Delegate expression task"
    flowable:delegateExpression="${MyDelegateExpression}"
    jmix:taskType="delegateExpression">
</serviceTask>

表达式

表达式是调用 Java 逻辑最常见的方式。 可以在表达式中调用 Spring bean:

expression service task

解析表达式的值

此外,服务任务中也可以直接使用一个值表达式。 然后指定一个结果变量,表达式的值会赋值给这个变量。 示例:

${'Hello, World!'}

结果变量 greeting 的值为 "Hello, World!"

失败重试

关于 失败重试(fail retry) 的介绍,请参阅 失败重试

配置

设置失败重试参数时,首先在 BPMN Inspector 中找到对应的属性:

fail retry property

设置值的格式必须是符合 ISO 8601 格式的时间周期表达式。 上图示例中的 R5/PT7M 表示作业执行程序会重试 5 次,并在每次重试之间等待 7 分钟。

XML 表示

失败重试参数使用 flowable:failedJobRetryTimeCycle 元素表示。 示例:

<serviceTask id="failingServiceTask"
    flowable:async="true"
    flowable:class="org.flowable.engine.test.jobexecutor.RetryFailingDelegate">
    <extensionElements>
        <flowable:failedJobRetryTimeCycle>R5/PT7M</flowable:failedJobRetryTimeCycle>
    </extensionElements>
</serviceTask>

在默认配置下,流程引擎会对出现异常的作业重复执行 3 次。

字段注入

字段注入(field injections) 是 Flowable 中的一种机制,用于将固定字符串值或字符串解析的表达式传递给 Java 代理 类中的参数。

注入的字段只能是 org.flowable.common.engine.api.delegate.Expression 类型。 当注入的表达式解析完成后,可以转换为合适的目标类型。

不能通过 字段注入 传递实体或其他对象。实际上,表达式是以 String 类型解析。 如果字符串值可以强制转换为指定的类型,则没问题。 否则,这将会报错。

如何注入字段:

  1. 在代理类中创建字段定义:

    private Expression messageField;
    private Expression quantityField;
  2. 选择服务任务并使用与代码中一样的名称创建字段。字段值可以是表达式或字符串。

    field injection properties

    如果需要传递数值,请使用上图所示的表达式,例如 ${3}。 如果只写 3,将被解析为 String 对象 “3”,并且不能转换为 Integer 类型。

  3. 在运行时,流程引擎会解析表达式并将结果字符串传递给代理类。

  4. 在 Java 代理类中,必须有从字段中获取值并转换为所需类型的代码:

    String message = (String) messageField.getValue(execution);
    Integer quantity = (Integer) quantityField.getValue(execution);

字段注入和线程安全

一般来是,在服务任务中使用 Java 代理和字段注入是线程安全的。但是,在极少情况下,由于 Flowable 运行的配置或环境因素会导致线程安全无法得到保证。

Java 代理类任务类型

这种类型使用字段注入始终是线程安全的。对于多个服务任务使用同一个代理类,仅实例化一个新实例,并在实例创建完成后立即执行字段注入。在不同的任务或流程定义中多次重用同一个类是没有问题的。

需要记住的是,不同的流程实例使用的 Java 代理类是同一个实例。 因此一个流程实例理论上有可能影响另一个流程实例,但实际上不会发生。

Spring bean 服务和表达式任务类型

从技术上讲,对于 Flowable,Spring bean 服务任务由 flowable:expression 属性表示。

使用 flowable:expression 属性时,不需要使用字段注入。参数通过方法调用传递,并且这些调用始终是线程安全的。

严格意义上讲,字段注入可以用,但不应该用。

代理表达式服务任务

当使用 flowable:delegateExpression 属性时,代理实例的线程安全性将取决于表达式的解析方式。如果代理表达式在多个任务或流程定义中重复使用,并且表达式始终返回相同的实例,则使用字段注入不是线程安全的。

两个服务任务可以使用相同的代理表达式,但注入不同的 Expression 字段。如果表达式解析为同一实例,则在流程执行的并发场景中注入字段时,可能存在先到先得的情况。

解决此问题的最简单方法是:

  • 重写 Java 代理,使用表达式或 Spring Bean,并通过方法参数传递所需的数据。

  • 每次解析代理表达式时,返回代理类的新实例。例如,当使用 Spring 时,bean 的 scope 必须设置为 prototype(例如,可以为代理类添加 @Scope(SCOPE_PROTOTYPE) 注解)。

Spring 中的 @Scope(SCOPE_PROTOTYPE) 注解用于定义 bean 的作用域,表示每次从 Spring 容器请求 bean 时,都应创建一个新的 bean 实例。 这与单例作用域相反,在单例作用域中,仅创建 bean 的一个实例,并在整个应用程序中共享。Prototype 作用域非常适合维护状态或非线程安全的 bean。

示例

public class UpperCaseJavaDelegate implements JavaDelegate {

    private Expression messageField;
    private Expression quantityField;
    @Override
    public void execute(DelegateExecution execution) {

        String message = (String) messageField.getValue(execution);
        Integer quantity = (Integer) quantityField.getValue(execution);

        String upperCaseMessage = message.toUpperCase();
        for (int i = 0; i < quantity; i++) {
            System.out.println(upperCaseMessage);
        }
    }
}