Flowable入门系列文章44 - Java服务任务
flowable入门、flowable现状、flowable开源产品、flwoable入门系列、flowable课程、flowable与activiti区别Flowable是用Java编写的轻量级业务流程引擎。Flowable流程引擎允许您部署BPMN 2.0流程定义(用于定义流程的行业XML标准),创建流程定义的流程实例,运行查询,访问活动或历史流程实例以及相关数据等等。本节将逐步介绍各种概念和
1、描述
Java服务任务用于调用外部Java类。
2、图形表示法
服务任务可视化为圆角矩形,左上角有一个小齿轮图标。
3、XML表示
有四种方式来声明如何调用Java逻辑:
- 指定实现JavaDelegate或ActivityBehavior的类
- 评估解析为委托对象的表达式
- 调用方法表达式
- 评估一个值表达式
要指定在流程执行期间调用的类,需要使用flowable:class属性提供完全限定的类名。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:class="org.flowable.MyJavaDelegate" />
也可以使用解析为对象的表达式。这个目的必须遵循相同的规则,当被创建的对象flowable:class,使用属性。
<serviceTask id="serviceTask" flowable:delegateExpression="${delegateExpressionBean}" />
这里delegateExpressionBean是一个bean,它实现了JavaDelegate在Spring容器中定义的接口。
要指定应评估的UEL方法表达式,请使用属性flowable:expression。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage()}" />
printMessage将在名为的命名对象上调用方法(不带参数)printer。
也可以使用表达式中使用的方法传递参数。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage(execution, myVar)}" />
方法printMessage将在名为的对象上调用printer。传递的第一个参数是DelegateExecution,在表达式上下文中可用,默认情况下,可用execution。传递的第二个参数是myVar当前执行中名称变量的值。
要指定应评估的UEL值表达式,请使用属性flowable:expression。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{split.ready}" />
财产的getter方法ready,getReady(不带参数),将在叫的bean被调用split。命名的对象在执行的流程变量和Spring上下文(如果适用)
中解析。
4、履行
要实现一个可以在流程执行期间调用的类,这个类需要实现org.flowable.engine.delegate.JavaDelegate接口,并在execute方法中提供所需的逻辑。当流程执行到达此特定步骤时,它将执行该方法中定义的逻辑,并将该活动保留为默认的BPMN 2.0方式。
例如,我们来创建一个可以用来将流程变量String更改为大写的Java类。这个类需要实现org.flowable.engine.delegate.JavaDelegate接口,它要求我们实现execute(DelegateExecution)方法。这个操作将被引擎调用,并且需要包含业务逻辑。流程实例信息,比如流程变量,可以通过DelegateExecution接口进行访问和操作。
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)的相同类实例。这意味着类不能使用任何成员变量,并且必须是线程安全的,因为它可以从不同的线程同时执行。这也影响了田间注入的处理方式。
流程定义中引用的类(通过使用flowable:class)在部署期间不会被实例化。只有当流程执行第一次到达使用类的过程中的某个点时,才会创建该类的一个实例。如果找不到类,FlowableException就会抛出一个。其原因是,部署时的环境(更具体地说是类路径)通常与实际运行时环境有所不同。例如,在Flowable应用程序中使用ant或业务归档上载来部署流程时,类路径将不会自动包含引用的类。
[INTERNAL:非公共实现类]也可以提供一个实现org.flowable.engine.impl.delegate.ActivityBehavior接口的类。然后,实现可以访问更强大的引擎功能,例如,影响流程的控制流程。但请注意,这不是一个很好的做法,应该尽可能避免。所以,建议只将ActivityBehavior接口用于高级用例,如果你确切地知道你在做什么。
5、现场注入
可以将值注入到委托类的字段中。支持以下类型的注入:
- 固定字符串值
- 表达式
如果可用,则按照Java Bean命名约定(例如,字段firstName为setter setFirstName(…)),通过您的委托类中的公共setter方法注入该
值。如果没有setter可用于该字段,私人成员的值将被设置在委托。某些环境中的SecurityManagers不允许修改私有字段,因此为要注入的字段公开setter方法更为安全。
无论在过程定义中声明的值的类型如何,注入目标上的setter / private字段的类型应始终为org.flowable.engine.delegate.Expression。表达式解析后,可以将其转换为适当的类型。
使用’flowable:class’属性时支持字段注入。当使用可移植的:delegateExpression属性时,字段注入也是可能的,但是关于线程安全的特殊规则也适用。
下面的代码片段显示了如何将一个常量值注入到类中声明的字段中。请注意,我们需要在实际的字段注入声明之前声明extensionElements XML元素,这是BPMN 2.0 XML Schema的一个要求。
<serviceTask id="javaService"
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。调用时ext.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 id="javaService"
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定义的bean(如果使用Spring的话)。正如服务任务实现中提到的那样,当使用flowable:class属性时,Java类的一个实例将在服务任务中的所有进程实例之间共享。要在字段中动态注入值,可以将值和方法表达式注入org.flowable.engine.delegate.Expression到可以使用elegateExecution传入的execute方法评估/调用的表达式中。
下面的示例类使用注入的表达式并使用当前的解决方案DelegateExecution。甲genderBean同时使该方法调用用于性别变量。完整的代码和测试可以在中找到org.flowable.examples.bpmn.servicetask.JavaServiceTaskTest.testExpressionFieldInjection
<serviceTask id="javaService" 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}" />
6、现场注射和螺纹安全
一般来说,对Java委托和字段注入使用服务任务是线程安全的。但是,有一些情况下,不能保证线程安全,这取决于Flowable正在运行的设置或环境。
有了flowable:class属性,使用字段注入总是线程安全的。对于引用特定类的每个服务任务,新实例将被实例化,并且在创建实例时将会注入一次字段。在不同的任务或流程定义中多次重复使用同一个类是没有问题的。
使用flowable:expression属性时,不能使用字段注入。参数是通过方法调用传递的,而且这些参数总是线程安全的。
使用flowable:delegateExpression属性时,委托实例的线程安全性将取决于表达式的解析方式。如果委托表达式在各种任务或过程定义中被重用,并且表达式总是返回相同的实例,则使用字段注入不是线程安全的。我们来看几个例子来澄清。
假设表达式是$ {factory.createDelegate(someVariable)},其中factory是引擎已知的Java bean(例如,使用Spring集成的Springbean),每次解析表达式时都会创建一个新实例。在这种情况下使用字段注入时,关于线程安全性没有问题:每次解析表达式时,这些字段都会注入到这个新实例中。
然而,假设表达式是$ {someJavaDelegateBean},它解析为JavaDelegate类的实现,并且我们运行在创建每个bean的singleton实例的环境中(比如Spring,但也有很多其他的)。在不同的任务或流程定义中使用此表达式时,表达式将始终解析为相同的实例。在这种情况下,使用字段注入不是线程安全的。例如:
<serviceTask id="serviceTask1" flowable:delegateExpression="${someJavaDelegateBean}">
<extensionElements>
<flowable:field name="someField" expression="${input * 2}"/>
</extensionElements>
</serviceTask>
<!-- other process definition elements -->
<serviceTask id="serviceTask2" flowable:delegateExpression="${someJavaDelegateBean}">
<extensionElements>
<flowable:field name="someField" expression="${input * 2000}"/>
</extensionElements>
</serviceTask>
此示例代码片段有两个使用相同委托表达式的服务任务,但为“ 表达式”字段注入不同的值。如果表达式解析为相同的实例,则在执行进程时,在注入字段someField时,并发场景中可能会出现争用条件。
解决这个问题的最简单的方法是:
- 重写Java委托以使用表达式并通过方法参数将所需的数据传递给委托。
- 每次解析委托表达式时,都会返回委托类的新实例。例如,使用Spring时,这意味着必须将bean的范围设置为原型(例如,通过将@Scope(SCOPE_PROTOTYPE)注释添加到委托类)。
从Flowable v5.22开始,可以通过设置delegateExpressionFieldInjectionMode属性的值(采用org.flowable.engine中的一个值)来设置流程引擎配置,从而禁止在委托表达式中使用字段注入.imp.cfg.DelegateExpressionFieldInjectionMode enum)。
以下设置是可能的:
- DISABLED:在使用委托表达式时完全禁用字段注入。将不会尝试现场注射。当涉及到线程安全时,这是最安全的模式。
- 兼容性:在这种模式下,行为将与v5.21之前的行为完全相同:使用委托表达式时可以进行字段注入,并且在委托类中未定义字段时将引发异常。当然,这对于线程安全来说是最不安全的模式,但是它可能需要向后兼容,或者可以安全地使用委托表达式仅用于一组进程定义中的一个任务(因此不使用并发的竞赛条件可能会发生)。
- MIXED:在使用delegateExpressions时允许注入,但当代理上没有定义字段时不会抛出异常。这允许混合的行为,其中一些代表有注入(例如,因为他们不是单身人士),有些则不是。
- Flowable 5.x版的默认模式是COMPATIBILITY。
- Flowable 6.x版的默认模式是MIXED。
举一个例子,假设我们正在使用MIXED模式,并且正在使用Spring集成。假设我们在Spring配置中有以下bean:
<bean id="singletonDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.SingletonDelegateExpressionBean" />
<bean id="prototypeDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.PrototypeDelegateExpressionBean"
scope="prototype" />
第一个bean是一个普通的Spring bean,因此是一个单例。第二个有作为范围的原型,并且Spring容器将在每次请求bean时返回一个新的实例。
给定以下流程定义:
<serviceTask id="serviceTask1" 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 id="serviceTask2" 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 id="serviceTask3" 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 id="serviceTask4" 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时,我们会返回两个,因为每次解析$
{prototypeDelegateExpressionBean}时都会创建一个新实例。字段可以在这里没有任何问题注入,我们可以在这里看到三个Expression成员字段。
但是,看起来稍有不同:
import java.util.concurrent.atomic.AtomicInteger;
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);
}
}
该INSTANCE_COUNT将永远是一个在这里,因为它是一个单例。在这个委托中,没有表达式成员字段。这是可能的,因为我们在混合模式下运行。在COMPATIBILITY模式下,它会抛出一个异常,因为它期望成员字段在那里。DISABLED模式也适用于这个bean,但是它不允许使用上面使用field注入的原型bean。
在这个委托代码中,使用org.flowable.engine.delegate.DelegateHelper类,它具有一些有用的实用方法来执行相同的逻辑,但是在代理是单例时以线程安全的方式。而不是注入表达式,它通过getFieldExpression方法获取。这意味着当涉及到服务任务XML时,这些字段的定义与单例bean完全一样。如果您查看上面的XML片段,您可以看到它们在定义上是相同的,只有实现逻辑不同。
技术说明:getFieldExpression会反省BpmnModel,并在执行该方法时动态创建Expression,从而使其线程安全。
- 对于Flowable v5.x,DelegateHelper不能用于ExecutionListener或TaskListener(由于架构上的缺陷)。要为这些侦听器创建线程安全的实例,请使用表达式或确保在每次解析委托表达式时都创建一个新实例。
- 对于Flowable V6.x,DelegateHelper在ExecutionListener和TaskListener实现中起作用。例如,在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);
}
}
7、服务任务结果
通过将流程变量名称指定为服务任务定义的“flowable:resultVariable”属性的文字值,可以将服务执行的返回值(仅用于使用表达式的服务任务)分配给现有流程变量或新流程变量。任何特定过程变量的现有值都将被服务执行的结果值覆盖。如果未指定结果变量名称,则服务执行结果值将被忽略。
<serviceTask id="aMethodExpressionServiceTask"
flowable:expression="#{myService.doSomething()}"
flowable:resultVariable="myVar" />
在上面的例子中,服务执行的结果(在一个对象上的’doSomething()'方法调用的返回值在名称’myService’下可用,无论是在流程变量还是作为Spring bean)到服务执行完成后,名为“myVar”的流程变量。
8、处理异常
当定制逻辑被执行时,经常需要捕获某些业务异常并在周围进程中处理它们。流动性提供了不同的选择来做到这一点。
9、投掷BPMN错误
可以从服务任务或脚本任务中的用户代码中抛出BPMN错误。为此,可以在JavaDelegates,脚本,表达式和委托表达式中引发一个名为BpmnError的特殊FlowableException。引擎将捕获此异常并将其转发给适当的错误处理程序,例如边界错误事件或错误事件子进程。
public class ThrowBpmnErrorDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
try {
executeBusinessLogic();
} catch (BusinessException e) {
throw new BpmnError("BusinessExceptionOccurred");
}
}
}
构造函数的参数是一个错误代码,将被用来确定负责错误的错误处理程序。有关如何捕获BPMN错误的信息,请参阅边界错误事件。
这种机制只应用于业务故障,这些业务故障将由流程定义中建模的边界错误事件或错误事件子流程来处理。技术错误应该由其他异常类型来表示,通常不在流程内部处理。
10、异常映射
也可以使用mapException扩展将Java异常直接映射到业务异常。单一映射是最简单的形式:
<serviceTask id="servicetask1" 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 id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException errorCode="myErrorCode1"
includeChildExceptions="true">org.flowable.SomeException</flowable:mapException>
</extensionElements>
</serviceTask>
上面的代码将导致Flowable SomeException将给定的错误代码转换为BPMN错误的任何直接或间接的后代。 includeChildExceptions在没有给出时将被认为是“假”。
最通用的映射是默认映射,它是没有类的映射。它将匹配任何Java异常:
<serviceTask id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException errorCode="myErrorCode1"/>
</extensionElements>
</serviceTask>
按照从上到下的顺序检查映射,除了默认映射之外,将会跟踪找到的第一个匹配。仅在所有地图检查失败后才选择默认地图。只有没有班级的第一张地图才会被视为默认地图。includeChildExceptions被默认的地图忽略。
11、异常序列流
另一种选择是在发生某种异常时通过不同的路径将流程执行路由。以下示例显示了如何完成此操作。
<serviceTask id="javaService"
name="Java service invocation"
flowable:class="org.flowable.ThrowsExceptionBehavior">
</serviceTask>
<sequenceFlow id="no-exception" sourceRef="javaService" targetRef="theEnd" />
<sequenceFlow id="exception" sourceRef="javaService" targetRef="fixException" />
在这里,服务任务有两个输出序列流,分别是named exception和no-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);
}
}
12、在JavaDelegate中使用Flowable服务
对于某些用例,可能需要从Java服务任务中使用Flowable服务(例如,如果callActivity不适合您的需要,则通过RuntimeService启动流程实例)。
import android.content.Context;
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的数据源。例如,下面的代码片段与上面的代码片段相同,但现在RuntimeService被注入而不是通过org.flowable.engine.EngineServices接口获取。
@Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {
@Autowired
private RuntimeService runtimeService;
public void startProcess() {
runtimeService.startProcessInstanceByKey("oneTaskProcess");
}
}
重要的技术说明:由于服务调用是作为当前事务的一部分完成的,所以在执行服务任务之前生成或更改的任何数据尚未刷新到数据库。所有API调用都在数据库数据上工作,这意味着这些未提交的更改在服务任务的API调用中不可见。
上面文章来自盘古BPM研究院:http://vue.pangubpm.com/
文章翻译提交:https://github.com/qiudaoke/flowable-userguide
了解更多文章可以关注微信公众号:
更多推荐
所有评论(0)