Flowable getting started series Article 44 - Java service tasks

1. Description

The Java service task is used to call external Java classes.

2. Graphical representation

The service task is visualized as a rounded rectangle with a pinion icon in the upper left corner.

3. XML representation

There are four ways to declare how to invoke Java logic:

  • Specifies the class that implements JavaDelegate or ActivityBehavior
  • Evaluate expressions that resolve to delegate objects
  • Call method expression
  • Evaluate a value expression

To specify the class to call during process execution, you need to provide a fully qualified class name using the flowable: class attribute.

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

You can also use expressions that resolve to objects. This purpose must follow the same rules. When the created object is flowable:class, use the attribute.

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

Here, delegateExpressionBean is a bean that implements the interface defined by Java delegate in the Spring container.

To specify the UEL method expression that should be evaluated, use the property flowable: expression.

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

printMessage will call the method (without parameters) printer on the named object named.
You can also pass parameters using the methods used in expressions.

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

The printMessage method calls printer on the object named. The first parameter passed is DelegateExecution, which is available in the expression context. By default, execution is available. The second parameter passed is the value of the name variable in the current execution of myVar.

To specify the UEL value expression that should be evaluated, use the property flowable: expression.

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

Property's getter methods, ready, getReady (without parameters), will be called split in the calling bean. Named objects in the execution process variables and Spring context (if applicable)
Parsing in.

4. Perform

To implement a class that can be called during process execution, this class needs to implement the org.flowable.engine.delegate.JavaDelegate interface and provide the required logic in the execute method. When the process execution reaches this particular step, it will execute the logic defined in the method and leave the activity in the default BPMN 2.0 mode.

For example, let's create a Java class that can be used to change the process variable String to uppercase. This class needs to implement the org.flowable.engine.delegate.JavaDelegate interface, which requires us to implement the execute (DelegateExecution) method. This operation will be called by the engine and needs to include business logic. Process instance information, such as process variables, can be accessed and operated through the DelegateExecution interface.

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

Note: there will be only one Java class instance created for the serviceTask defined for it. All process instances share the same class instance that will be used to call execute (DelegateExecution). This means that a class cannot use any member variables and must be thread safe because it can be executed simultaneously from different threads. This also affects the treatment of field injection.

Classes referenced in the process definition (by using flowable:class) will not be instantiated during deployment. An instance of a class is created only when the process execution first reaches a point in the process of using the class. If the class is not found, a FlowableException is thrown. The reason is that the deployment time environment (more specifically, the classpath) is usually different from the actual runtime environment. For example, when you deploy a process using ant or business archive upload in a Flowable application, the classpath will not automatically contain the referenced classes.

[International: Non-public implementation class] you can also provide a class that implements the org.flowable.engine.impl.delegate.ActivityBehavior interface. Then, the implementation can access more powerful engine functions, such as control processes that affect processes. But please note that this is not a good practice and should be avoided as much as possible. Therefore, it is recommended to only use the ActivityBehavior interface for high-level use cases if you know exactly what you are doing.

5. Field injection

You can inject values into fields of a delegate class. The following types of injection are supported:

  • Fixed string value
  • expression

If available, inject the bean through the public setter method in your delegate class according to the Java Bean naming convention (for example, the field firstName is setter setFirstName(...))
Value. If no setter is available for this field, the value of the private member will be set in the delegate. SecurityManagers in some environments do not allow private fields to be modified, so it is safer to expose setter methods for fields to be injected.

Regardless of the type of value declared in the procedure definition, the type of setter / private field on the injection target should always be org.flowable.engine.delegate.Expression. After the expression is parsed, it can be converted to the appropriate type.

Field injection is supported when using 'flowable: class' attribute. Field injection is also possible when using the portable: delegateExpression attribute, but special rules on thread safety also apply.

The following code snippet shows how to inject a constant value into a field declared in a class. Note that we need to declare the extensionElements XML element before the actual field injection declaration, which is a requirement of 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>

This class ToUpperCaseFieldInjected has a field org.flowable.engine.delegate.Expression of type text. When calling ext.getValue(execution),
Hello World will return the configured string value:

public class ToUpperCaseFieldInjected implements JavaDelegate {
	private Expression text;
	public void execute(DelegateExecution execution) {
		execution.setVariable("var", 	((String)text.getValue(execution)).toUpperCase());
	}
}

Alternatively, for long text (for example, inline e-mail), you can use the 'flowable: string' sub element:

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

To inject values that are dynamically parsed at run time, you can use expressions. These expressions can use process variables or Spring defined bean s (if Spring is used). As mentioned in the service task implementation, when the flowable: class attribute is used, an instance of the Java class will be shared among all process instances in the service task. To dynamically inject values into fields, you can inject values and method expressions into expressions that can be evaluated / called using the execute method passed in by elegateExecution.

The following example class uses the injected expression and uses the current solution DelegateExecution. A genderBean also makes the method call for gender variables. The complete code and tests can be found at 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());
	}
}

Alternatively, you can set the expression to an attribute instead of a child element, making the XML less verbose.

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

6. Field injection and thread safety

In general, using service tasks for Java delegates and field injection is thread safe. However, there are some cases where thread safety cannot be guaranteed, depending on the setting or environment in which Flowable is running.

With the flowable: class attribute, using field injection is always thread safe. For each service task that references a specific class, the new instance will be instantiated and the field will be injected once when the instance is created. There is no problem reusing the same class multiple times in different task or process definitions.

Field injection cannot be used when using the flowable: expression property. Parameters are passed through method calls, and they are always thread safe.

When using the flowable: delegateExpression property, the thread safety of the delegate instance depends on how the expression is parsed. If delegate expressions are reused in various task or process definitions and expressions always return the same instance, using field injection is not thread safe. Let's look at a few examples to clarify.

Suppose the expression is ${factory. Createdelete (someVariable)}, where factory is a Java bean known to the engine (for example, a Spring bean using Spring integration), and a new instance is created each time the expression is parsed. When using field injection in this case, there is no problem with thread safety: these fields are injected into the new instance every time the expression is parsed.

However, suppose the expression is ${someJavaDelegateBean}, which resolves to the implementation of the JavaDelegate class, and we run in an environment that creates a singleton instance of each bean (such as Spring, but there are many others). When this expression is used in different task or process definitions, the expression will always resolve to the same instance. In this case, using field injection is not thread safe. For example:

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

This sample code fragment has two service tasks that use the same delegate expression, but inject different values into the expression field. If the expression resolves to the same instance, contention conditions may occur in the concurrency scenario when the process is executed and the field someField is injected.

The simplest way to solve this problem is:

  • Rewrite the Java delegate to use expressions and pass the required data to the delegate through method parameters.
  • Each time a delegate expression is parsed, a new instance of the delegate class is returned. For example, when using Spring, this means that the Scope of the bean must be set to prototype (for example, by adding the @ Scope (SCOPE_PROTOTYPE) annotation to the delegate class).

Starting from Flowable v5.22, you can set the process engine configuration by setting the value of the delegateExpressionFieldInjectionMode property (using a value in org.flowable.engine), so as to prohibit the use of field injection. imp.cfg.DelegateExpressionFieldInjectionMode enum) in delegate expressions.

The following settings are possible:

  • DISABLED: completely disable field injection when using delegate expressions. Field injection will not be attempted. This is the safest mode when it comes to thread safety.
  • Compatibility: in this mode, the behavior will be exactly the same as that before v5.21: field injection can be performed when using delegate expressions, and an exception will be thrown when no field is defined in the delegate class. Of course, this is the most insecure mode for thread safety, but it may need to be backward compatible, or it may be safe to use delegate expressions for only one task in a set of process definitions (so it may happen not to use concurrent race conditions).
  • MIXED: injection is allowed when delegateExpressions are used, but no exception is thrown when no field is defined on the agent. This allows MIXED behaviors, some of which represent injection (for example, because they are not single) and some are not.
  • The default mode of flowable version 5. X is COMPATIBILITY.
  • The default mode of flowable version 6. X is MIXED.
    For example, suppose we are using MIXED pattern and Spring integration. Suppose we have the following bean s in the Spring configuration:
<bean id="singletonDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.SingletonDelegateExpressionBean" />
<bean id="prototypeDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.PrototypeDelegateExpressionBean"
scope="prototype" />

The first bean is an ordinary Spring bean, so it is a singleton. The second has a prototype as a scope, and the Spring container will return a new instance each time the bean is requested.
The following process definitions are given:

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

We have four service tasks. The first and second use ${prototypeDelegateExpressionBean} delegate expressions, and the third and fourth use$
{singletonDelegateExpressionBean} delegate expression.

Let's look at the prototype bean first:

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);
    }
}

After running the process instance defined above, we check the instance_ When count, we will return two because each time we parse$
A new instance is created whenever {prototypeDelegateExpressionBean}. Fields can be injected here without any problems. We can see three Expression member fields here.

However, it looks slightly different:

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);
    }
}

This INSTANCE_COUNT will always be one here because it is a singleton. In this delegate, there are no expression member fields. This is possible because we run in mixed mode. In COMPATIBILITY mode, it throws an exception because it expects the member field to be there. The DISABLED pattern also applies to this bean, but it does not allow the use of the prototype bean using field injection above.

In this delegate code, the org.flowable.engine.delegate.DelegateHelper class is used. It has some useful practical methods to perform the same logic, but in a thread safe manner when the agent is a singleton. Instead of injecting an expression, it is obtained through the getFieldExpression method. This means that when it comes to the service task XML, the definitions of these fields are exactly the same as those of the singleton bean. If you look at the above XML fragments, you can see that they are the same in definition, only with different implementation logic.

Technical note: getFieldExpression will reflect on BpmnModel and dynamically create Expression when executing this method, so as to make it thread safe.

  • For Flowable v5.x, DelegateHelper cannot be used with ExecutionListener or TaskListener (due to architectural defects). To create thread safe instances for these listeners, use expressions or ensure that a new instance is created each time the delegate expression is resolved.
  • For Flowable V6.x, the DelegateHelper plays a role in the ExecutionListener and TaskListener implementations. For example, in V6.x, you can use the DelegateHelper.

Write the following code:

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

testExecutionListener resolves to an instance that implements the ExecutionListener interface:

@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. Service task results

By specifying the process variable name as the text value of the "flow: resultVariable" attribute of the service task definition, you can assign the return value of the service execution (only for service tasks using expressions) to an existing process variable or a new process variable. The existing value of any particular process variable will be overwritten by the result value of the service execution. If the result variable name is not specified, the service execution result value is ignored.

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

In the above example, the result of the service execution (the return value of the 'doSomething()' method call on an object is available under the name 'myService', whether in the process variable or as a Spring bean) to the process variable named "myVar" after the service execution is completed.

8. Handling exceptions

When custom logic is executed, it is often necessary to catch some business exceptions and deal with them in the surrounding processes. Liquidity offers different options to do this.

9. Throw BPMN error

BPMN errors can be thrown from user code in service tasks or script tasks. For this purpose, a special FlowableException named BpmnError can be raised in JavaDelegates, scripts, expressions and delegate expressions. The engine will catch this exception and forward it to the appropriate error handler, such as a boundary error event or an error event subprocess.

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

The argument to the constructor is an error code that will be used to determine the error handler responsible for the error. For information on how to catch BPMN errors, see boundary error events.

This mechanism applies only to business faults, which will be handled by boundary error events or error event subprocesses modeled in the process definition. Technical errors should be represented by other exception types and are usually not handled within the process.

10. Anomaly mapping

You can also use the mapException extension to map Java exceptions directly to business exceptions. Single mapping is the simplest form:

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

In the above code, if org.flowable.SomeException throws an instance in the service task, it will be caught and converted into a BPMN exception with the given errorCode. From this point of view, it will be handled like a normal BPMN exception. Any other exceptions will be treated as not mapped. It will be propagated to API callers.

By using the includechildexception property, you can map all child exceptions of an exception to one row.

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

The above code will cause a Flowable SomeException to convert a given error code into any direct or indirect descendant of a BPMN error. Includechildrexceptions will be considered "false" if not given.

The most common mapping is the default mapping, which is a mapping without classes. It will match any Java exception:

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

The mapping is checked from top to bottom, and the first match found is tracked in addition to the default mapping. Select the default map only after all map checks fail. Only the first map without a class is considered the default map. Includechildexception is ignored by the default map.

11. Abnormal sequence flow

Another option is to route the process execution through different paths when some exception occurs. The following example shows how to do this.

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

Here, the service task has two output sequence streams, named exception and no exception. If an exception occurs, the sequence flow ID will be used to guide the process flow:

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. Using Flowable service in Java delegate

For some use cases, you may need to use the Flowable service from the Java service task (for example, if the callActivity is not suitable for your needs, start the process instance through the 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");
    }
}

All Flowable service API s can use this interface.

All data changes that occur as a result of using these API calls become part of the current transaction. This also applies to environments with dependency injection, such as Spring and CDI, data sources with or without JTA. For example, the following code snippet is the same as the above code snippet, but now the RuntimeService is injected rather than obtained through the org.flowable.engine.EngineServices interface.

@Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {
@Autowired
private RuntimeService runtimeService;
public void startProcess() {
runtimeService.startProcessInstanceByKey("oneTaskProcess");
}
}

Important technical note: since the service call is completed as part of the current transaction, any data generated or changed before executing the service task has not been refreshed to the database. All API calls work on database data, which means that these uncommitted changes are not visible in the API call of the service task.

The above article is from Pangu BPM Research Institute: http://vue.pangubpm.com/
Article translation submission: https://github.com/qiudaoke/flowable-userguide
For more articles, you can focus on WeChat official account:

Keywords: Java Flowable oa bpm

Added by tronicsmasta on Sat, 30 Oct 2021 19:58:26 +0300