服务任务

概览

服务任务(Service task) 是流程中表示自动活动的一种任务类型。 服务任务可以调用 Java 代码、外部服务、API 或者自动化程序以完成相应的功能。

图形表示法

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

service task
XML 表示

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

属性

服务任务有下列属性:

service task properties

设置任务属性:

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

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

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

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

  • 确定是否需要 扩展属性

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

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

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

服务任务的类型

任务类型(Task type) 参数定义服务任务的实现方式。 Jmix BPM 中的服务任务有以下几种类型:

service taks types

我们简要了解一下:

Spring bean

当流程到达任务时,将执行所选 bean 的指定方法。 方法参数可使用流程变量或固定值,返回结果可以存储在结果变量中。

Java Delegate Class

服务任务作为 JavaDelegate 类实现,执行任务时调用 execute() 方法。 此外,还支持 字段注入 机制。

Expression

这是调用任何方法或计算内联表达式的通用方法。

Delegate expression

实际上,这不是表达式,而是一个实现 JavaDelegate 接口的 Spring bean。 因此,这种类型的任务具有两者的特点。

Spring Bean 服务任务

Jmix BPM 支持从服务任务中调用 Spring bean 方法,并为其提供参数值。 这种类型是最常见的服务任务类型。 因此作为默认类型选项。

可以点击 'Plus' 按钮从这里创建一个新的 bean:

bean create button

然后,输入 bean 的名称:

create new bean

完成后自动切换至代码编辑器,并编写需要使用的方法,示例:

@Component(value = "smpl_OrderStatusBean")
public class OrderStatusBean {

    public Integer setStatus(String orderId, String status) {
        // set status, returns quantity of items
        return quantity;
    }
}

Bean 的名称和方法也可以从下拉列表中选择:

select bean

选择方法后,将显示一个用于输入方法参数值的面板:

spring bean task properties

BPMN Inspector 会自动构建一个表达式,用于调用 bean 方法,这个表达式无法手动编辑。 对于上面截图中选择的方法,表达式为:

${smpl_OrderStatusBean.setStatus(OrderId,'Sent')}

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

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

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

结果变量

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

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

结果变量 有一个 本地变量(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.JavaDelegate 接口的类的 execute() 方法执行。 方法接收一个 execution 对象作为参数,因此可以在访问流程上下文,包括全部流程变量。

如果在 Task type 下拉框中选择 JavaDelegate class,可以通过点击 'Plus' 按钮新建一个类:

create java delegate

在弹窗内输入新 Java 代理类的名称:

new 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 的功能。

为了能在代理表达式中使用,JavaDelegate 的实现类必须带有 Spring 的 @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

这里可以创建一个新的代理表达式类:

new delegate expression

或从下拉列表选择一个已有的类:

select delegate expression
XML 表示

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

<serviceTask id="delegate-expression"
    name="Delegate expression task"
    flowable:delegateExpression="${smpl_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> (1)
    </extensionElements>
</serviceTask>
1  — 失败重试参数

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

字段注入

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

  • Java 代理类

如果调用对象是 Java 代理类,那么也可以用在:

  • 代理表达式

  • 表达式

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

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

如何注入字段:

  1. JavaDelegate 类中创建字段定义:

    private Expression messageField;
    private Expression quantityField;
  2. 在图中,选择服务任务并创建与代码中定义的同名字段:

    create field
  3. 然后输入字段值,例如,表达式或字符串:

    field injection properties

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

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

  5. 在 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);
        }
    }
}