ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Activiti 6.0 工作流实战避坑指南:BPMN部署、任务配置与定时器调试

Activiti 6.0 工作流实战避坑指南:BPMN部署、任务配置与定时器调试

简介:本资源是面向Java开发工程师与流程自动化实践者的Activiti 6.0工作流引擎入门与实操指南,聚焦业务流程建模、任务类型配置与流程控制逻辑落地。文档系统梳理模型设计器核心能力,涵盖用户任务、服务任务、脚本任务、邮件任务等10类任务的适用场景与配置要点,并详解子流程、并行/排他分支、事件子流程等关键构造机制,助力开发者快速掌握流程定义、部署与调试全流程。资源为单文件PDF格式,共1个1.72MB文档,内容结构清晰,含37页完整目录与分章节技术说明,便于按需查阅与实践对照。目前已有1168人学习下载,适合初接触Activiti的工作流开发人员、企业OA/审批系统改造工程师及Spring Boot集成流程引擎的学习者系统入门与快速上手。

1. 这不是一份“翻完就扔”的 PDF:Activiti 6.0 工作流使用说明文档 v1.0 是你落地 BPMN 流程的实操黑匣子

如果你正在用 Spring Boot 集成工作流,却卡在「画完流程图,一部署就报错Invalid BPMN model」;或者调试时发现「定时器没触发、信号收不到、邮件发不出」,翻遍官网文档和 Stack Overflow 还是找不到对应配置项在哪——那你手头这份《Activiti6.0工作流使用说明文档v1.0.pdf》不是过时资料,而是当年某高校实验室在真实产线项目中沉淀下来的「避坑型操作手册」。它不讲抽象理论,37 页里有 28 页是设计器界面截图+属性面板填空式说明,连「log4j.logger.org.activiti.engine.impl.persistence.entity=DEBUG」这种打开 SQL 日志的开关都标在第 4 页脚注里。它专为两类人存在:一是刚接手遗留 Activiti 6 项目的维护工程师,需要快速定位Camel 任务怎么配 Spring Bean、决策表变量为空导致后续${riskCheck==false}报错的根因;二是想绕过官方 User Guide 的冗长描述,直接抄参数、粘配置、复现 zip 示例包的实战派。它不承诺兼容 7.x,但能让你在 JDK8 + Spring 4.x + Tomcat 8 的老环境中,把「用户审批 → 调用风控服务 → 自动发邮件 → 超时自动升级」这条链路真正跑通。


2. 模型设计器不是画布,是流程逻辑的编译器:从拖拽元素到生成可执行 BPMN 文件的完整闭环

Activiti 6 的模型设计器(qqdznyyglpt-workflow-designer)本质是一个 BPMN 2.0 编译前端:你拖进去的每个图标,最终都会被翻译成<userTask>、<serviceTask>等 XML 元素,并注入具体属性值。它的输出不是图片,而是.bpmn20.xml文件——这才是引擎真正加载的对象。理解这一点,才能避开「界面看着对,运行就报错」的玄学现场。

2.1 任务节点:10 类任务的本质差异与选型铁律

任务是流程的原子单元,但不同任务的执行机制天差地别。选错类型,轻则功能失效,重则流程卡死。

2.1.1 用户任务:必须人工介入的强制关卡

用户任务(<userTask>)是唯一要求「人」参与的节点。关键配置项不是名称或描述,而是assignee(处理人)和candidateGroups(候选组):

  • assignee填admin表示固定分配给 admin 用户;
  • candidateGroups填hr,finance表示 HR 和财务组所有成员都能拾取该任务;
  • 若两者都为空,任务将处于unclaimed状态,需调用taskService.claim(taskId, userId)手动认领。

提示:生产环境严禁硬编码assignee="admin"。应使用表达式${initiator}(发起人)或${ldapService.getApproverByDept(dept)}(动态查LDAP),否则流程无法流转。

2.1.2 服务任务:三种实现方式的边界与血泪经验

服务任务(<serviceTask>)用于执行 Java 逻辑,但类、表达式、委托表达式三者不可混用,且依赖注入方式完全不同:

实现方式配置位置依赖注入要求典型错误场景
类(Class)属性面板Class字段类必须实现JavaDelegate或ActivityBehavior,且含@Override execute(DelegateExecution)方法忘记在activiti.cfg.xml中注册该类为 bean,导致ClassNotFoundException
表达式(Expression)Field→Name: resultVar,String value: ${myService.doSomething()}方法必须返回值,且resultVar名称需全局唯一在resultVar中用了${}表达式,导致解析失败
委托表达式(Delegate Expression)Delegate expression字段,填#{myServiceBean}myServiceBean必须在 Spring 容器中声明为@Bean,且实现JavaDelegateSpring 上下文未加载,#{myServiceBean}解析为空

实际项目中,我一般会强制走委托表达式路线:

  • 在SpringConfig.java中声明:
@Bean public MyOrderService myOrderService() { return new MyOrderService(); }
  • 设计器中Delegate expression填#{myOrderService},Field中添加orderNo=${execution.getVariable("orderNo")}传参。
    这样既保证 Spring 管理生命周期,又避免类路径硬编码。
2.1.3 脚本任务:JavaScript 引擎的隐形依赖与变量陷阱

脚本任务(<scriptTask>)默认用 Nashorn(JDK8 内置 JS 引擎),但必须显式声明脚本格式:

  • 属性面板Script format填javascript(不能写js或留空);
  • 脚本内容中禁止使用out,context,elcontext等保留字作为变量名(文档第 6 页明确警告);
  • 设置流程变量必须用execution.setVariable("key", value),不能用var key = value。

一个典型翻车案例:

// ❌ 错误:用 var 声明,变量仅在脚本作用域内 var amount = execution.getVariable("loanAmount"); if (amount > 10000) { execution.setVariable("riskLevel", "high"); // 此处设值有效 } else { execution.setVariable("riskLevel", "low"); // 此处设值有效 } // 但下面这行无效!因为 amount 是局部变量,无法被后续节点引用 execution.setVariable("amount", amount); // ✅ 必须显式 set
2.1.4 业务规则任务:Drools 规则文件的打包与加载规范

业务规则任务(<businessRuleTask>)依赖 Drools 引擎,但规则文件(.drl)必须和 BPMN 文件同级打包:

  • 正确结构:process.bpmn20.xml+rules.drl(同一 ZIP 包内);
  • 设计器中Rule files字段填rules.drl(若多个用逗号分隔);
  • 规则中input变量名必须与流程变量名完全一致(大小写敏感),output只能有一个变量名(如decisionResult)。

若规则中定义rule "CheckLoan" when $l: Loan(amount > 10000) then $l.setRisk("high"); end,则流程中必须存在名为Loan的变量对象,且含amount属性。否则规则不触发,且无任何日志提示——这是最隐蔽的坑。

2.2 分支与网关:排他、并行、包容、事件四类网关的执行逻辑拆解

网关控制流程走向,但每种网关的「条件判断时机」和「实例生成策略」完全不同,直接决定流程是否按预期并发或串行。

2.2.1 排他网关(Exclusive Gateway):单选题,但默认选第一个

排他网关(<exclusiveGateway>)像一道单选题:

  • 每条出线可配置conditionExpression(如${loanAmount > 5000});
  • 引擎从上到下扫描,遇到第一个为true的条件即执行,其余忽略;
  • 若所有条件为false,抛出ActivitiException: No outgoing sequence flow of the exclusive gateway ...。

注意:条件表达式必须返回布尔值。${status == 'APPROVED'}合法,${status}不合法(返回字符串,非布尔)。

2.2.2 并行网关(Parallel Gateway):强制并发,条件失效

并行网关(<parallelGateway>)的分叉(Fork)和聚合(Join)是强绑定的:

  • 分叉后,所有出线任务必定同时启动,无论是否配置条件;
  • 聚合时,必须等待所有入线任务完成才继续;
  • 若某条线任务失败,整个流程卡在聚合点,除非配置async或异常处理。

典型误用:想让「审核A」和「审核B」并行,但希望「审核B」只在dept == 'finance'时执行。此时不能在并行网关出线上写条件,而应改用包容网关。

2.2.3 包容网关(Inclusive Gateway):带条件的并行,但聚合逻辑更严苛

包容网关(<inclusiveGateway>)是排他与并行的混合体:

  • 分叉时,只启动条件为true的出线任务;
  • 聚合时,必须等待所有「已启动」的任务完成才继续(不是所有出线,而是所有满足条件的出线);
  • 若无一条出线条件为true,流程直接卡死(同排他网关)。

配置要点:

  • 分叉前必须有inclusiveGateway标签;
  • 每条出线conditionExpression必须明确(不能留空);
  • 聚合点也必须是inclusiveGateway,且default属性无效。
2.2.4 事件网关(Event Gateway):挂起等待,非抢占式分支

事件网关(<eventGateway>)不基于条件,而是等待事件发生:

  • 出线只能连接中间捕获事件(如messageEvent,signalEvent,timerEvent);
  • 流程执行到此即挂起,订阅所有出线事件;
  • 任一事件触发,其他事件订阅自动取消,流程沿该事件线继续。

关键限制:

  • 至少连接2 个中间捕获事件(文档第 13 页强调,否则部署失败);
  • 每个事件只能有一个输入流(不能从其他节点连入);
  • 定时器事件需配置timeDuration(如PT10M表示 10 分钟),否则不生效。

2.3 事件:捕获与抛出的生命周期管理

事件是流程的「神经末梢」,但捕获(Catch)和抛出(Throw)行为严格分离,且生命周期由引擎托管。

2.3.1 定时器事件:三种配置格式的语法细节

定时器事件(<timerEventDefinition>)必须配置timeDate、timeDuration或timeCycle之一,否则发布失败。常见错误:

配置类型正确示例常见错误原因
timeDate2023-12-25T09:00:00+08:002023/12/25 09:00:00必须 ISO-8601 格式,T和时区+08:00不可省略
timeDurationPT30M(30分钟)P30M(30个月)P后跟T表示时间,不跟T表示周期(年月日)
timeCycleR3/PT1H(循环3次,间隔1小时)R/PT1H(无限循环)R后必须跟数字,否则引擎无法解析

提示:timeCycle的 cron 表达式需用0 0/5 * * * ?格式(秒 分 时 日 月 周 年),且jobExecutorActivate=true必须在activiti.cfg.xml中开启。

2.3.2 信号事件:全局与实例作用域的隔离机制

信号事件(<signalEventDefinition>)分两种作用域:

  • 全局信号:RuntimeService.signalEventReceived("alert"),所有订阅该信号的流程实例均接收;
  • 实例信号:RuntimeService.signalEventReceived("alert", executionId),仅指定实例接收。

关键约束:

  • 捕获信号只能捕获「信号抛出之后」的事件(文档第 14 页),即流程必须先到达信号捕获节点,再抛出信号;
  • 若流程尚未到达捕获节点就抛出信号,该信号丢失,无日志;
  • 查询订阅:runtimeService.createExecutionQuery().signalEventSubscriptionName("alert").list()返回当前所有等待该信号的执行实例。
2.3.3 消息事件:点对点通信的协议约定

消息事件(<messageEventDefinition>)与信号核心区别是一对一:

  • 每个消息名(messageName)唯一对应一个接收方;
  • 抛出消息必须指定executionId(不能全局);
  • 消息内容通过HashMap<String, Object>传递,如:
Map<String, Object> vars = new HashMap<>(); vars.put("orderId", "ORD-2023-001"); runtimeService.messageEventReceived("orderConfirmed", executionId, vars);
  • 接收方在messageEvent节点中通过${orderId}获取变量。

3. workflow-service 模块:流程服务层的配置与方法调用契约

workflow-service是连接设计器与业务系统的胶水层,其核心是ProcessEngineConfiguration的定制化。它不提供新功能,但决定了「流程如何与你的代码对话」。

3.1 数据更新逻辑:流程变量与业务数据的双向同步

流程变量(execution.setVariable())默认存储在 Activiti 自身数据库中,但业务系统通常有自己的数据表。workflow-service通过监听器实现同步:

3.1.1 执行监听器(Execution Listener):流程节点级别的钩子

在 BPMN 文件中为节点添加:

<userTask id="approveTask" name="审批"> <extensionElements> <activiti:executionListener event="start" class="com.example.listener.ApproveStartListener"/> <activiti:executionListener event="end" delegateExpression="${approveEndListener}"/> </extensionElements> </userTask>
  • event="start":节点开始执行前触发;
  • event="end":节点执行完成后触发(无论成功或失败);
  • class方式需实现ExecutionListener接口;
  • delegateExpression方式需 Spring Bean 实现ExecutionListener。

典型用途:

  • start时校验前置条件(如检查申请人是否有权限);
  • end时更新业务表状态(如UPDATE order SET status='APPROVED' WHERE id=#{orderId})。
3.1.2 任务监听器(Task Listener):任务生命周期的精细控制

任务监听器(<activiti:taskListener>)作用于任务状态变更:

  • event="create":任务创建时(taskService.newTask()后);
  • event="assign":任务被分配时(taskService.claim()后);
  • event="complete":任务完成时(taskService.complete()后)。

注意:assign事件在claim时触发,但create事件在流程启动时即触发(即使任务未被领取)。因此,发送待办通知应在create事件中,而非assign。

3.2 方法表达式配置:让流程调用你的 Service 方法

方法表达式是服务任务的简化版,但需严格遵循 SpEL 语法:

3.2.1 方法调用的参数传递规范

在服务任务属性中:

  • Expression字段填#{orderService.confirmOrder(execution.getVariable('orderId'), execution.getVariable('userId'))};
  • Result variable字段填confirmResult(接收返回值)。

关键约束:

  • 方法参数必须是流程变量,不能直接写字符串字面量(如#{orderService.confirmOrder('ORD-001', 'admin')}会报错);
  • 若方法返回对象,confirmResult将存入流程变量,后续节点可用${confirmResult.status}访问。
3.2.2 外置表单(External Form)的集成路径

外置表单(HTML/JS)需通过formKey关联:

  • BPMN 中用户任务设置formKey="orderForm";
  • workflow-service需提供/form/orderForm接口,返回表单 HTML;
  • 表单提交时,POST /runtime/tasks/{taskId}/variables提交变量。

提示:表单中字段名必须与流程变量名一致(如<input name="amount">对应${amount}),否则变量无法注入。

3.3 事件监听器配置:全局事件的捕获与响应

全局事件监听器(org.activiti.engine.delegate.event.ActivitiEventListener)可监听所有流程事件:

3.3.1 监听器注册方式

在activiti.cfg.xml中:

<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"> <property name="eventListeners"> <list> <bean class="com.example.listener.GlobalProcessListener"/> </list> </property> </bean>
3.3.2 关键事件类型与处理建议
事件类型触发时机推荐处理动作避坑点
EVENTTYPE_PROCESS_STARTED流程实例启动记录日志、初始化监控指标避免在此处调用耗时 DB 操作,阻塞流程启动
EVENTTYPE_TASK_ASSIGNED任务分配发送站内信、邮件提醒event.getTask().getAssignee()可能为 null(未分配)
EVENTTYPE_ACTIVITY_COMPLETED节点完成更新业务状态、触发下游流程event.getActivityId()返回节点 ID,非任务 ID

4. 避坑指南:Activiti 6.0 设计器与引擎的 5 个高频翻车现场

这些坑我在三个项目中反复踩过,每次排查都耗掉半天。列在这里,帮你省下 15 小时 debug 时间。

4.1 现象:流程部署成功,但启动时报Invalid BPMN model: no start event found

原因:BPMN 文件中确实没有<startEvent>节点,或开始事件未正确连接到第一个任务。设计器有时会因拖拽失误导致开始事件「悬浮」在画布外,XML 中<startEvent>存在但无outgoing连接。
解决:打开.bpmn20.xml文件,搜索<startEvent>,确认其id是否在<sequenceFlow sourceRef="xxx" targetRef="yyy"/>中作为sourceRef出现。若无,手动添加<sequenceFlow sourceRef="startEventId" targetRef="firstTaskId"/>。

4.2 现象:定时器事件(Timer Event)始终不触发,日志无任何报错

原因:jobExecutorActivate或asyncExecutorActivate未启用。Activiti 6 默认关闭异步执行器,定时器、邮件等异步任务全部失效。
解决:在activiti.cfg.xml中添加:

<property name="jobExecutorActivate" value="true"/> <!-- 或 --> <property name="asyncExecutorActivate" value="true"/>

并确保activiti-app.properties中mail.server.host等配置正确(影响邮件任务,但定时器依赖执行器本身)。

4.3 现象:信号事件(Signal Event)抛出后,流程无响应,createExecutionQuery().signalEventSubscriptionName(...).list()返回空列表

原因:流程未执行到信号捕获节点就抛出了信号。信号是「广播」,但捕获是「订阅」,必须先订阅后广播。
解决:在流程中插入一个User Task或Service Task,确保流程至少执行一步后再到达信号捕获节点。例如:

Start → Dummy Task(空服务任务,仅 sleep(100))→ Signal Catch → Next Node

然后在Dummy Task的end监听器中抛出信号。

4.4 现象:决策任务(Business Rule Task)中${riskCheck==false}报错Cannot resolve identifier 'riskCheck'

原因:决策表(.drl)中未为riskCheck输出变量赋值,或规则未匹配导致变量未创建。Activiti 不会为未命中的规则创建输出变量。
解决:在.drl文件末尾添加兜底规则:

rule "Default Risk Check" when // 无条件 then $decision.setRiskCheck(false); end

确保所有路径都有输出。

4.5 现象:邮件任务(Mail Task)配置了activiti-app.properties,但启动时抛javax.mail.AuthenticationFailedException

原因:activiti-app.properties中mail.server.password是明文,但某些邮件服务器(如 QQ 邮箱)要求使用「授权码」而非登录密码,且授权码需 URL 编码。
解决:将授权码进行 URL 编码(如abc123!@#编码为abc123%21%40%23),再填入配置:

mail.server.password=abc123%21%40%23

并在log4j.properties中开启DEBUG级日志,确认org.activiti.engine.impl.bpmn.behavior.MailActivityBehavior是否正常加载。


5. 验证与调试:用三步法把「流程跑起来」变成「流程稳得住」

部署一个流程不难,难的是确保它在高并发、异常网络、数据库抖动下依然可靠。我总结了一套验证闭环,从单点测试到混沌压测,覆盖 95% 的线上问题。

5.1 第一步:SQL 日志 + 流程实例快照,定位执行断点

Activiti 的最大优势是全程可追溯。开启 SQL 日志是诊断的第一步:

  • 修改log4j.properties:
log4j.logger.org.activiti.engine.impl.persistence.entity=DEBUG log4j.logger.org.activiti.engine.impl.jobexecutor=DEBUG
  • 启动应用,执行一次流程;
  • 查看日志中INSERT INTO ACT_RU_EXECUTION和UPDATE ACT_RU_TASK的 SQL,确认:
    • 流程实例 ID(ID_)是否生成;
    • 当前执行节点(ACT_ID_)是否指向预期任务;
    • 任务表(ACT_RU_TASK)中ASSIGNEE_是否为空(未分配)或有值(已分配)。

提示:若ACT_RU_EXECUTION中IS_ACTIVE_=0且END_TIME_为空,说明流程卡在某个节点,需检查该节点的异步配置或监听器是否抛异常。

5.2 第二步:用 REST API 模拟全流程,绕过前端干扰

前端页面可能隐藏表单校验或 JS 错误,直接调用 Activiti REST API 可排除干扰:

  • 启动流程实例:
curl -X POST "http://localhost:8080/activiti-rest/service/runtime/process-instances" \ -H "Content-Type: application/json" \ -u "admin:test" \ -d '{"processDefinitionKey":"orderProcess","variables":[{"name":"orderNo","value":"ORD-001"},{"name":"amount","value":9999}]}'
  • 查询待办任务:
curl "http://localhost:8080/activiti-rest/service/runtime/tasks?assignee=admin" \ -u "admin:test"
  • 完成任务:
curl -X POST "http://localhost:8080/activiti-rest/service/runtime/tasks/ taskId/complete" \ -H "Content-Type: application/json" \ -u "admin:test" \ -d '{"variables":[{"name":"approved","value":true}]}'

若 API 能跑通而前端不行,问题一定在前端 JS 或表单渲染逻辑。

5.3 第三步:混沌测试:主动制造故障,验证补偿与重试

生产环境不会总是一帆风顺。我习惯在预发环境做三类主动故障测试:

  • 数据库连接中断:停掉 MySQL,观察流程是否卡在ACT_RU_EXECUTION,恢复后是否自动重试(需配置asyncExecutor);
  • 服务任务超时:在MyOrderService.confirmOrder()中加Thread.sleep(60000),确认async=true的节点是否挂起,而主线程继续;
  • 补偿事件触发:在事务子流程中,故意让服务任务抛BpmnError("PAYMENT_FAILED"),检查边界补偿事件是否执行refund()方法。

从那以后我每次上线新流程,都强制走一遍这三步:SQL 日志确认执行路径 → REST API 验证接口契约 → 混沌测试验证容错能力。少一次,就可能多一个凌晨三点的告警电话。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进