服务任务(Service Task)
服务任务是 BPMN 2.0 规范中的核心元素之一,在 Flowable 工作流引擎中扮演着至关重要的角色。它代表了流程中一个由系统自动执行的步骤,用于与外部世界进行交互,而无需人工干预。
可以把它理解为流程中的“机器人”,专门负责执行后台代码、调用外部服务或执行任何自动化任务。
1. 核心概念与用途是什么?
服务任务是一个自动化的活动,当流程执行到该节点时,Flowable 引擎会调用预定义的逻辑。这个逻辑通常是一段 Java 代码。
为什么用?
服务任务是连接业务流程和技术实现的桥梁。它的主要用途包括:
调用外部系统: 通过 REST API、SOAP Web Service、RPC 等方式与其他微服务或传统应用交互。例如:查询客户信息、创建订单、调用支付接口。
执行业务逻辑: 执行复杂的内部业务规则计算。例如:根据用户积分和等级计算折扣。
数据处理与转换: 从数据库读取数据、对流程变量进行格式化或转换、将数据写入数据仓库。
发送通知: 自动发送电子邮件、短信或推送通知。
文件操作: 生成报告(PDF、Excel)、操作文件系统等。
总之,任何不需要人工决策和操作的后台任务,都应该是服务任务的候选者。
2. 三种主要的实现方式
在 Flowable 中,实现服务任务的逻辑主要有三种方式。在 BPMN XML 文件中,通过不同的 flowable: 属性来指定。
a) Java Delegate (Class)
这是最常用、最强大、最推荐的方式。你需要创建一个实现了 org.flowable.engine.delegate.JavaDelegate 接口的 Java 类。
工作原理:
你创建一个 Java 类,实现 JavaDelegate 接口。
这个接口只有一个方法:execute(DelegateExecution execution)。
所有的业务逻辑都写在 execute 方法中。DelegateExecution 对象是与引擎交互的入口,你可以通过它:
getVariable(String variableName): 获取流程变量。
setVariable(String variableName, Object value): 设置流程变量。
getProcessInstanceId(): 获取当前流程实例ID。
…等等。
在 BPMN 模型的服务任务节点上,使用 flowable:class 属性指向这个类的完全限定名。
<serviceTask id="sendEmailTask" name="发送欢迎邮件" flowable:class="com.example.flowable.delegates.SendWelcomeEmailDelegate" />
Java代码如下
package com.example.flowable.delegates;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SendWelcomeEmailDelegate implements JavaDelegate {
private static final Logger logger = LoggerFactory.getLogger(SendWelcomeEmailDelegate.class);
@Override
public void execute(DelegateExecution execution) {
// 1. 从流程变量中获取所需数据
String userId = (String) execution.getVariable("userId");
String email = (String) execution.getVariable("email");
logger.info("准备向用户 {} 发送欢迎邮件...", userId);
// 2. 执行核心业务逻辑 (这里是模拟调用邮件服务)
System.out.println("==============================================");
System.out.printf("正在向邮箱 %s 发送欢迎邮件...%n", email);
System.out.println("邮件内容:欢迎加入我们的平台!");
System.out.println("==============================================");
// 3. (可选) 将执行结果设置回流程变量
execution.setVariable("welcomeEmailSent", true);
execution.setVariable("emailSentTimestamp", new java.util.Date());
}
}
优点:
功能强大: 可以编写任意复杂的 Java 代码,注入其他服务(如 Spring Bean)。
高可测试性: 可以独立于 Flowable 引擎进行单元测试。
代码清晰、可重用: 业务逻辑和流程定义分离,同一个 Delegate 类可以被多个不同的流程定义使用。
b) Expression (表达式)
适用于非常简单的、单行的逻辑调用。引擎会执行一个 JUEL (Java Unified Expression Language) 表达式。
工作原理:
引擎会解析并执行 flowable:expression 属性中定义的表达式。这个表达式通常用于调用一个已经存在的 Bean (例如 Spring Bean) 的方法。
<!-- 假设你有一个名为 'emailService' 的Spring Bean -->
<serviceTask id="sendNotificationTask" name="发送通知"
flowable:expression="${emailService.send(execution, 'some_template')}" />
Java代码如下:
@Service("emailService")
public class EmailService {
public void send(DelegateExecution execution, String templateName) {
String recipient = (String) execution.getVariable("recipient");
// ... 发送邮件的逻辑 ...
System.out.printf("使用表达式调用:向 %s 发送模板为 %s 的邮件%n", recipient, templateName);
}
}
优点:
简洁: 对于简单的调用非常方便,无需创建单独的 Delegate 类。
集成方便: 与 Spring 等依赖注入框架结合得很好。
缺点:
功能有限: 不适合复杂的业务逻辑,代码可读性和可维护性会变差。
测试困难: 测试时需要依赖完整的上下文环境(如 Spring Context)。
c) Delegate Expression (委托表达式)
这是 Class 和 Expression 的一种结合。它也使用表达式,但表达式的计算结果必须是一个实现了 JavaDelegate 接口的对象。
工作原理:
引擎首先评估 flowable:delegateExpression 中的表达式,得到一个对象实例。然后,引擎会调用这个实例的 execute 方法。这通常用于从 Spring Context 中动态获取一个 JavaDelegate Bean。
<!-- 'sendWelcomeEmailDelegate' 是一个在Spring中注册的Bean ID -->
<serviceTask id="sendEmailTask" name="发送欢迎邮件"
flowable:delegateExpression="${sendWelcomeEmailDelegate}" />
Java代码如下:
// 注意,这个类实现了JavaDelegate接口,并被注册为一个Spring Bean
@Component("sendWelcomeEmailDelegate")
public class SendWelcomeEmailDelegate implements JavaDelegate {
@Autowired // 可以注入其他Spring Bean
private SomeOtherService otherService;
@Override
public void execute(DelegateExecution execution) {
// ... 逻辑和 'Java Delegate (Class)' 方式完全一样 ...
// 但这里可以使用注入的 otherService
otherService.doSomething();
}
}
3. 数据传递:输入/输出参数映射
通常,你的 JavaDelegate 类不应该知道具体的流程变量名(如 “userId”)。为了让 Delegate 更具通用性和可重用性,可以使用字段注入和输入/输出参数映射。
字段注入 (Field Injection):
可以直接将流程变量注入到 JavaDelegate 的字段中。
<serviceTask id="someTask" flowable:class="com.example.MyDelegate">
<extensionElements>
<flowable:field name="textToProcess">
<flowable:string><![CDATA[Hello ${name}]]></flowable:string>
</flowable:field>
<flowable:field name="userVar">
<flowable:expression>${userId}</flowable:expression>
</flowable:field>
</extensionElements>
</serviceTask>
Java代码如下:
public class MyDelegate implements JavaDelegate {
// 引擎会自动注入这些字段
private Expression textToProcess;
private Expression userVar;
@Override
public void execute(DelegateExecution execution) {
// 需要手动从Expression中获取值
String processedText = (String) textToProcess.getValue(execution);
String userIdValue = (String) userVar.getValue(execution);
System.out.println(processedText); // 输出 "Hello [name变量的值]"
System.out.println(userIdValue); // 输出 userId变量的值
}
}
输入/输出参数 (Input/Output Parameters):
这是一个更现代、更清晰的做法,用于将流程变量映射到局部变量,或将局部结果映射回流程变量。
<serviceTask id="calculateTask" name="计算" flowable:class="com.example.CalculatorDelegate">
<extensionElements>
<!-- 输入映射:将流程变量 'var1' 和 'var2' 映射到任务的局部变量 'inputA' 和 'inputB' -->
<flowable:in source="var1" target="inputA" />
<flowable:in source="var2" target="inputB" />
<!-- 输出映射:将任务的局部变量 'calculationResult' 映射到流程变量 'processResult' -->
<flowable:out source="calculationResult" target="processResult" />
</extensionElements>
</serviceTask>
Java代码如下:
public class CalculatorDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) {
// 直接获取映射后的局部变量
Integer a = (Integer) execution.getVariable("inputA");
Integer b = (Integer) execution.getVariable("inputB");
Integer result = a + b;
// 设置局部结果变量
execution.setVariable("calculationResult", result);
// 引擎会根据<flowable:out>的定义,在任务结束后自动将'calculationResult'的值赋给'processResult'
}
}
4. 异步执行与事务边界
默认情况下,服务任务是同步执行的。这意味着从上一个节点到服务任务再到下一个节点的整个过程都在同一个事务中。如果服务任务执行失败,整个事务都会回滚。
对于耗时的操作或调用外部系统(可能暂时不可用),同步执行会阻塞引擎线程,并可能导致事务超时。这时应使用异步执行。
<serviceTask id="longRunningTask" name="耗时任务"
flowable:class="com.example.LongRunningDelegate"
flowable:async="true" />
工作原理:
当流程到达该服务任务前,引擎会提交当前事务,并将一个“作业(Job)”保存到数据库中。
流程执行会暂停,请求立即返回。
后台的 Job Executor 线程池会扫描数据库,发现这个新作业。
Job Executor 在一个新的事务中执行这个服务任务的逻辑。
如果执行成功,事务提交,流程继续向下执行。
如果执行失败,新事务回滚,作业的重试次数会减少。引擎会根据配置进行重试,如果最终还是失败,会产生一个“事件(Incident)”。
好处:
健壮性: 外部系统故障不会导致主流程事务回滚。
响应性: 不会阻塞主流程线程,可以更快地响应用户。
解耦: 将长时间运行的工作与主流程分离开。
5. 错误处理
当服务任务中发生异常时,如何优雅地处理?
强一致性不要瞎搞!!!!!!!!
脚本任务(Script Task)
脚本任务和服务任务一样,也是一种自动化的活动。但它执行的不是预编译的 Java 类,而是一段动态的脚本语言(如 Groovy, JavaScript)。
可以把它想象成流程中的一个“便签贴”,上面写着一段小程序,流程执行到这里时,引擎就会拿出这个便签贴,现场解释并执行上面的代码。
1. 核心概念与用途
是什么?
脚本任务是一个自动化的流程节点,当流程执行到该节点时,Flowable 引擎会调用内置的脚本引擎(遵循 JSR-223 规范)来执行预定义的一段脚本。
为什么用?
脚本任务和服务任务的目标相似,都是为了实现自动化,但它们的侧重点和适用场景不同。脚本任务的主要用途包括:
简单的业务逻辑: 执行一些不值得创建一个完整 Java 类的简单计算或逻辑判断。例如,variableC = variableA + variableB。
快速原型开发: 在流程设计初期,可以用脚本快速实现某些逻辑,而无需编译和部署 Java 代码,从而加快开发和验证周期。
动态逻辑注入: 脚本是文本,理论上可以存储在数据库或配置文件中,在运行时动态加载,实现比 Java 类更灵活的逻辑变更。
简单的数据转换/操作: 对流程变量进行简单的格式化、拼接或预处理。例如,将一个用户的姓和名拼接成全名。
2. 实现方式与配置
在 BPMN 模型中,脚本任务的实现主要有两种方式:内联脚本和外部资源脚本。
a) 内联脚本 (Inline Script)
这是最直接的方式,脚本代码直接写在 BPMN 2.0 XML 文件中。
工作原理:
在服务任务节点内,使用
<scriptTask id="calculateSumTask" name="计算总和" scriptFormat="groovy">
<script>
<![CDATA[
// 从流程变量中获取 a 和 b
def a = execution.getVariable("varA")
def b = execution.getVariable("varB")
// 执行计算
def sum = a + b
println "Groovy script: a + b = " + sum
// 将结果设置回流程变量
execution.setVariable("sumResult", sum)
]]>
</script>
</scriptTask>
优点:
直观: 逻辑和流程定义在一起,一目了然。
简单: 对于非常简短的脚本,这是最快的方式。
缺点:
维护困难: 复杂的脚本会让 XML 文件变得臃肿,难以阅读和维护。
无 IDE 支持: 在 XML 文件中编写脚本没有语法高亮、自动补全等 IDE 功能。
可重用性差: 脚本被绑定在特定的流程定义中。
b) 外部资源脚本 (External Resource Script)
对于较长或需要重用的脚本,更好的方式是将其放在一个单独的文件中,然后在 BPMN 中引用它。
工作原理:
创建一个脚本文件(例如 my-script.groovy)。
将该文件与流程定义一起部署到 Flowable 引擎。
在 节点上,使用 flowable:resource 属性指向该脚本文件的路径。
<scriptTask id="externalScriptTask" name="执行外部脚本"
scriptFormat="groovy"
flowable:resource="scripts/my-script.groovy" />
具体脚本文件内容
// 脚本文件内容和内联脚本完全一样
def user = execution.getVariable("username")
def score = execution.getVariable("score")
println "Processing score for user: " + user
def level = "Beginner"
if (score > 90) {
level = "Expert"
} else if (score > 60) {
level = "Intermediate"
}
execution.setVariable("userLevel", level)