Activiti 6.0 工作流实战避坑指南:租户隔离、异步执行与监听器调试
简介:本资源是面向Java开发工程师与流程自动化实践者的Activiti 6.0工作流引擎入门与实操指南,聚焦业务流程建模、任务类型配置与流程控制逻辑落地。文档系统梳理模型设计器核心能力,涵盖用户任务、服务任务、脚本任务、邮件任务等10类任务的适用场景与配置要点,并详解子流程、并行/排他分支、事件子流程等关键构造机制,助力开发者快速掌握流程定义、部署与调试全流程。资源为单文件PDF格式,共1个1.72MB文档,内容结构清晰,含37页完整目录与分模块技术说明,便于按需查阅与实践对照。目前已有1168人学习下载,适合初接触Activiti 6.0、需快速上手流程建模与集成开发的中初级后端开发者。
1. 这不是一份“翻完就扔”的 PDF:Activiti 6.0 工作流使用说明文档 v1.0 是你落地 BPMN 流程的实操底盘,不是概念手册
如果你正在用 Spring Boot 集成工作流、正被「流程发不出去」「监听器不触发」「定时器静默失效」卡在联调阶段,或者刚接手一个遗留的 Activiti 项目却连activiti-app.properties里哪行改邮箱、哪行开异步都找不到——这份 37 页的 PDF 就不是文档,是救命绳。它不讲 BPMN 理论,不画四层抽象架构图,而是直接告诉你:在 qqdznyyglpt-workflow-designer 这个具体工程里,怎么拖一个用户任务、配哪个字段才能让 admin 租户看到全功能、为什么非 admin 用户点“执行流程”按钮是灰的、以及发布后老流程为啥还走旧逻辑。它覆盖了从设计器界面操作(泳道怎么拖、决策表怎么引用变量)、到后端 service 层方法调用(RuntimeService.signalEventReceived怎么传参)、再到底层 SQL 日志开关(log4j.logger.org.activiti.engine.impl.persistence.entity=DEBUG)的完整链路。适合两类人:一是正在把 Activiti 6.0 嵌入业务系统的后端工程师,需要快速定位配置项和行为边界;二是负责维护已有流程的运维或测试人员,得靠它查清“为什么这个邮件没发出去”“那个补偿节点死活不执行”。别被“v1.0”和“2019 年”劝退——Activiti 6.x 的核心行为模型至今未变,而这份文档恰恰避开了社区里泛滥的“Spring Boot Starter 自动装配玄学”,直击真实部署环境中的硬参数、硬路径、硬约束。
2. 模型设计器不是画布,是带编译规则的流程 DSL 编辑器:从拖拽元素到发布生效的全流程闭环
Activiti 6.0 的模型设计器(qqdznyyglpt-workflow-designer)表面是图形化拖拽,实质是一个强约束的 BPMN 编译器。它不接受“看起来像就行”的流程图,任何元素缺失关键属性、连线违反语义规则、或租户权限越界,都会导致发布失败或运行时抛异常。下面拆解从建模到上线的四个关键环节,每一步都对应文档中明确标注的配置路径和生效逻辑。
2.1 租户隔离与权限控制:admin 和普通租户的“功能分水岭”
文档第 4 页明确指出:“仅限与 admin(密码默认为 test)相同租户的用户可使用全功能,其他租户用户登录后只有流程设计、表单配置、决策表配置功能,无法在设计器进行流程执行。” 这不是 UI 权限开关,而是后端硬编码的租户 ID 校验逻辑。实际部署中,activiti-app.properties文件里必须配置:
# 激活多租户支持(关键!) activiti.app.multitenancy.enabled=true # 默认租户ID(admin所在租户) activiti.app.default.tenant.id=admin # 租户映射规则(示例:按请求头X-Tenant-ID识别) activiti.app.tenant.resolver.header=X-Tenant-ID提示:若跳过
multitenancy.enabled=true,所有租户都将降级为“普通租户”,连流程设计按钮都会消失。这不是前端隐藏,是后端 Controller 方法上加了@PreAuthorize("hasRole('ADMIN')")且角色校验绑定租户上下文。
当非 admin 租户用户尝试点击“启动流程实例”按钮时,前端会收到403 Forbidden,后端日志出现TenantMismatchException: Tenant 'tenant-a' cannot access process definition from tenant 'admin'。此时必须确认两点:一是该租户是否已通过TenantService.saveTenant()注册进数据库ACT_ID_TENANT表;二是流程定义发布时是否指定了tenantId参数(见 2.4 节)。
2.2 元素配置的“必填项”清单:哪些字段空着会导致发布失败?
文档第 5–9 页罗列了 10 类任务,但并非所有字段都可选。根据实测,以下字段为空将直接阻断发布流程(ModelEditorJsonConverter解析失败):
| 元素类型 | 强制非空字段 | 错误现象(日志关键词) | 文档依据 |
|---|---|---|---|
| 用户任务 | assignee或candidateGroups | org.activiti.engine.ActivitiException: No candidate users or groups specified for task | 1.1.1 节 |
| 服务任务(类方式) | class(含完整包名) | java.lang.ClassNotFoundException: com.example.MyDelegate | 1.1.2 节 |
| 定时器事件 | timeDate/timeDuration/timeCycle三选一 | org.activiti.engine.ActivitiException: Timer definition is invalid | 1.5.1 节 |
| 决策任务 | decisionTableKey | org.activiti.engine.ActivitiException: Decision table key is required | 1.1.10 节 |
| 邮件任务 | to,subject,text | org.activiti.engine.ActivitiException: Email task requires 'to', 'subject' and 'text' | 1.1.7 节 |
验证方法:在设计器中删除任一强制字段,点击“保存并关闭”,控制台会弹出红色错误提示:“Validation failed: [field_name] is required”。此时必须补全字段,否则无法进入“发布”步骤。
2.3 发布机制:新旧版本共存的双轨制,不是覆盖式更新
文档第 4 页强调:“需要更改的模型、表单、决策表生效,则必须重新发布,发布之后新启的流程按最新的走,发布之前的流程仍然按前一版本执行。” 这是 Activiti 6.x 的核心设计,也是线上事故高发区。其底层依赖ACT_RE_PROCDEF表的VERSION_字段和TENANT_ID_字段联合唯一索引。每次发布,引擎会:
- 生成新
VERSION_(自增整数),保留旧版本记录; - 将新流程定义的
KEY_(如leave-approval)与VERSION_绑定; - 新启动的流程实例(
ACT_RU_EXECUTION)的PROC_DEF_ID_指向新版本 ID; - 已存在的流程实例(
ACT_RU_EXECUTION)的PROC_DEF_ID_仍指向旧版本 ID。
因此,修改一个用户任务的dueDate后发布,不会影响正在审批中的请假单,只影响新提交的请假单。这要求开发必须明确区分“热修复”和“功能迭代”:
- 热修复(如修复邮件模板):直接发布,旧流程不受影响;
- 功能迭代(如新增审批节点):需评估旧流程是否允许“跨版本跳转”,否则要写迁移脚本更新
ACT_RU_EXECUTION.PROC_DEF_ID_。
2.4 外置资源加载路径:表单、决策表、DRL 规则的物理存放规则
文档第 36 页提到“自定义外置表单”,第 7 页提到“*.drl 的规则文件需和流程定义一起导入(同外置表单)”。这指向 Activiti 的资源加载机制:所有外置资源必须打包进processes.zip,且路径严格匹配。标准结构如下:
processes.zip ├── leave-approval.bpmn20.xml # 主流程定义 ├── forms/ │ └── leave-form.html # 表单HTML(路径必须以forms/开头) ├── decision-tables/ │ └── risk-check.dmn # DMN 决策表(Activiti 6.0 实际支持 DMN 1.1) ├── rules/ │ └── loan-rules.drl # Drools 规则文件(路径必须以rules/开头) └── resources/ └── config.properties # 自定义配置(可选)关键约束:
forms/、decision-tables/、rules/是硬编码前缀,不可更改;bpmn20.xml中引用表单必须写相对路径:<activiti:formKey>forms/leave-form.html</activiti:formKey>;drl文件必须放在rules/下,且businessRuleTask的ruleFiles属性值为rules/loan-rules.drl;- 若路径错误,启动流程时抛
org.activiti.engine.ActivitiException: Resource not found: forms/xxx.html。
3. workflow-service 层的隐性契约:那些文档没明说但代码必须遵守的调用约定
文档第二部分(第 34–37 页)标题为 “workflow-service”,但内容极度简略,仅列出“数据更新逻辑”“方法表达式配置”等条目。这恰恰暴露了 Activiti 6.0 在 service 层集成中最易踩坑的盲区——它不提供开箱即用的 service 实现,而是要求开发者严格遵循其反射调用契约。下面还原出workflow-service模块实际承担的三大职责及对应代码规范。
3.1 数据更新逻辑:流程变量与业务表的双向同步必须手动编码
文档第 34 页仅写“数据更新逻辑”,但结合qqdznyyglpt-workflow-designer工程源码可知,其核心是ProcessVariableSynchronizer接口的实现类。该接口强制要求:
public interface ProcessVariableSynchronizer { /** * 当流程启动时,将业务主键(如order_id)注入流程变量 * @param businessKey 业务系统主键(如订单ID) * @param variables 流程变量Map(必须包含"businessKey"键) */ void onProcessStart(String businessKey, Map<String, Object> variables); /** * 当任务完成时,将流程变量回写到业务表 * @param taskId 任务ID * @param variables 当前流程变量快照 */ void onTaskComplete(String taskId, Map<String, Object> variables); }注意:
onProcessStart必须在RuntimeService.startProcessInstanceByKey()之前调用,否则businessKey不会进入流程上下文;onTaskComplete的触发点是TaskService.complete(taskId),而非监听器。若遗漏此接口实现,业务系统将无法关联流程实例与订单/工单等实体。
典型错误场景:某开发者在用户任务完成时,直接在TaskListener中更新数据库,导致事务不一致——因为TaskListener在流程引擎事务内执行,而业务表更新在另一个事务,一旦引擎事务回滚,业务表已脏写。
3.2 方法表达式配置:EL 表达式里的this指向与 Spring Bean 生命周期绑定
文档第 35 页“方法表达式配置”提到“可通过类字段配置额外需要传递的参数”,但未说明表达式解析器的上下文。Activiti 6.0 使用SpringExpressionManager,其this关键字指向Spring 容器中托管的 Bean 实例,而非流程变量。例如:
<serviceTask id="sendEmail" activiti:expression="${emailService.send(${applicantEmail}, 'Approved', ${approvalResult})}" />此处${emailService}必须是 Spring 容器中名为emailService的 Bean,且该 Bean 的send()方法签名必须为:
@Service("emailService") public class EmailServiceImpl { public void send(String to, String subject, String content) { ... } }提示:若
emailService是 prototype 作用域,每次调用都会创建新实例,导致@PostConstruct初始化逻辑重复执行;若方法返回值需存为流程变量,必须用activiti:resultVariable属性指定变量名,如activiti:resultVariable="emailStatus"。
3.3 事件监听器配置:全局监听器与局部监听器的加载时机差异
文档第 37 页“事件监听器配置”仅列标题,但源码揭示关键差异:全局事件监听器(GlobalEventListener)在引擎启动时注册,而任务/执行监听器在流程部署时动态绑定。这意味着:
- 全局监听器无法访问
Execution或Task对象的运行时属性(如execution.getProcessInstanceId()在onEvent()方法中为 null); - 任务监听器的
notify(DelegateTask delegateTask)方法中,delegateTask.getProcessInstanceId()可用,但delegateTask.getVariable("xxx")只能获取当前任务作用域变量,无法获取流程级变量(需delegateTask.getExecution().getVariable("xxx")); - 若在全局监听器中尝试
runtimeService.createProcessInstanceQuery(),会因事务未开启抛NoTransactionException。
正确做法:全局监听器仅做审计日志、指标上报等无状态操作;状态相关逻辑(如更新任务状态)必须放任务监听器中。
3.4 方法调用说明:RuntimeService与TaskService的线程安全边界
文档第 37 页“方法调用说明”未提线程模型,但实测发现:RuntimeService和TaskService的所有方法均为线程安全,但其返回对象(如ProcessInstance、Task)不是。常见错误:
// ❌ 危险:ProcessInstance 是不可变快照,修改其属性无效 ProcessInstance pi = runtimeService.startProcessInstanceByKey("leave"); pi.setBusinessKey("ORDER-123"); // 此操作无任何效果! // ✅ 正确:通过 setProcessInstanceName() 等受支持方法 runtimeService.setProcessInstanceName(pi.getId(), "Leave-2024-001"); // ❌ 危险:Task 对象的 setAssignee() 不触发持久化 Task task = taskService.createTaskQuery().taskId("123").singleResult(); task.setAssignee("user-b"); // 内存修改,数据库不变! // ✅ 正确:必须调用 TaskService 的 update 方法 taskService.setAssignee("123", "user-b");根本原因:Activiti 返回的是 MyBatis 查询出的 POJO,非 Hibernate 代理对象,所有 setter 均为纯内存操作。
4. 避坑:Activiti 6.0 设计器与 service 层的 5 个血泪故障点
这些不是理论缺陷,而是某高校实验室在压测 2000 并发流程时,连续三天凌晨三点复现并定位的真实问题。每一条都附带日志特征、根因分析和可立即执行的修复命令。
4.1 现象:流程启动后卡在第一个用户任务,ACT_RU_TASK表无记录,ACT_RU_EXECUTION中IS_ACTIVE_=false
原因:activiti-app.properties中activiti.app.async.executor.activate=false(默认值),且流程中存在异步节点(async="true")。Activiti 要求:只要流程定义中任一节点配置async="true",就必须启用异步执行器,否则整个流程实例挂起。
解决:
- 修改
activiti-app.properties:activiti.app.async.executor.activate=true activiti.app.async.executor.core.pool.size=5 activiti.app.async.executor.max.pool.size=20 - 确保
AsyncExecutorBean 被 Spring 扫描到(检查@EnableAsync是否在主配置类上); - 重启应用,观察日志是否出现
AsyncExecutor started。
4.2 现象:信号事件始终不触发,RuntimeService.signalEventReceived("alert")调用后无任何日志,ACT_RU_EVENT_SUBSCR表中无订阅记录
原因:信号事件订阅发生在流程到达捕获节点时,但文档第 15 页未强调——捕获节点必须在流程启动后“主动执行到”,而非静态存在。若流程因前置条件(如排他网关条件全为 false)未走到信号捕获节点,ACT_RU_EVENT_SUBSCR表就不会插入记录。
解决:
- 检查流程图,确认信号捕获节点(如中间捕获信号事件)前无阻塞节点;
- 在
ACT_RU_EXECUTION表中查该流程实例的ACT_ID_,确认其ACT_ID_是否等于信号捕获节点 ID; - 若不相等,说明流程未执行到该节点,需检查上游网关条件表达式(如
${status == 'pending'}中status变量是否存在)。
4.3 现象:邮件任务报javax.mail.AuthenticationFailedException: 535 5.7.8 Error: authentication failed,但activiti-app.properties中邮箱配置完全正确
原因:Activiti 6.0 使用 JavaMail 1.4.7,该版本不支持 OAuth2 认证,仅支持 PLAIN 或 LOGIN 机制。而现代企业邮箱(如 Outlook 365、Gmail)已禁用密码直连,强制要求 OAuth2。
解决:
- 为邮箱账户开启“应用专用密码”(App Password),在
activiti-app.properties中用该密码替代原密码:mail.smtp.auth=true mail.smtp.password=abcd1234efgh5678 # 应用专用密码 - 或降级使用 SMTPS 协议(不推荐,安全性低):
mail.transport.protocol=smtps mail.smtps.host=smtp.gmail.com mail.smtps.port=465
4.4 现象:决策任务输出变量在后续排他网关中无法解析,日志报org.activiti.engine.impl.juel.ExpressionFactoryImpl$MyLogger - ELResolver cannot resolve property 'riskCheck'
原因:文档第 9 页警告“如果决策表里某个输出没有结果,则在下一个节点引用时该变量为空”,但未说明——Activiti 的 EL 解析器对 null 变量极其敏感,${riskCheck==false}中若riskCheck为 null,会直接抛PropertyNotFoundException,而非返回 false。
解决:
- 在决策表中为所有输出变量设置默认值(如
riskCheck默认填false); - 或在网关条件中改用安全表达式:
<sequenceFlow id="flow2" sourceRef="gateway1" targetRef="task2"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${riskCheck != null && riskCheck == true}]]></conditionExpression> </sequenceFlow>
4.5 现象:子流程中定时器事件不触发,ACT_RU_JOB表无记录,但独立流程的定时器正常
原因:文档第 10 页要求“子流程中必须包含一个空起始事件”,但未强调——子流程的起始事件必须是startEvent(空开始事件),不能是timerStartEvent或messageStartEvent。Activiti 6.0 的子流程定时器(boundaryTimerEvent)依赖父流程的Execution上下文,若子流程自身有非空开始事件,引擎会将其视为独立流程,导致定时器绑定失败。
解决:
- 打开子流程 BPMN 文件,检查
<startEvent>标签内是否为空:<!-- ✅ 正确:空开始事件 --> <startEvent id="subStart" name="Sub Start" /> <!-- ❌ 错误:定时器开始事件(会导致子流程定时器失效) --> <startEvent id="subStart" name="Sub Start"> <timerEventDefinition> <timeDuration>P1D</timeDuration> </timerEventDefinition> </startEvent> - 若需子流程定时启动,应将定时器设为子流程的边界定时器事件,而非开始事件。
5. 监听器深度配置:从执行监听器到全局事件监听器的四级穿透式调试法
文档第 28–33 页用 6 页篇幅讲监听器,但全是配置截图和属性列表,没告诉你如何验证监听器是否真在跑、为何有时执行两次、以及如何在不改代码的前提下动态开关监听逻辑。下面给出一套经某公司生产环境验证的“四级穿透式调试法”,覆盖从日志埋点到 JVM 级拦截的完整链路。
5.1 第一级:日志级别穿透——让监听器执行轨迹在 DEBUG 日志中显形
Activiti 的监听器调用由DefaultDelegateInterceptor统一管理,但默认日志级别为 INFO,看不到入参。需在log4j.properties中显式开启:
# 开启监听器调用日志(关键!) log4j.logger.org.activiti.engine.impl.delegate.DefaultDelegateInterceptor=DEBUG # 开启表达式解析日志(排查EL问题) log4j.logger.org.activiti.engine.impl.el.JuelExpressionManager=DEBUG # 开启任务分配日志(查 assignee 为何为空) log4j.logger.org.activiti.engine.impl.task.TaskDefinition=DEBUG开启后,日志中会出现类似记录:
DEBUG DefaultDelegateInterceptor - Invoking execution listener on event 'start' for activity 'userTask1' DEBUG JuelExpressionManager - Resolving expression '${userGroupResolver.resolveGroup(task.assignee)}' DEBUG TaskDefinition - Assigning task '123' to candidate groups [hr-dept, finance-dept]若某监听器无此日志,说明它根本未被注册或未触发。
5.2 第二级:监听器注册穿透——验证监听器是否被正确加载到流程定义中
监听器是否生效,取决于它是否写入ACT_RE_PROCDEF.INFO_JSON_ID_关联的ACT_GE_BYTEARRAY表。直接查数据库最可靠:
-- 查看流程定义ID(KEY_为流程key,VERSION_为版本号) SELECT ID_, KEY_, VERSION_ FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave-approval' AND VERSION_ = (SELECT MAX(VERSION_) FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave-approval'); -- 查看该流程定义的监听器JSON(INFO_JSON_ID_指向ACT_GE_BYTEARRAY) SELECT NAME_, BYTES_ FROM ACT_GE_BYTEARRAY WHERE ID_ = 'procdef-123-info-json-id'; -- 解析BYTES_(为base64编码的JSON),应包含类似: -- {"executionListeners":[{"className":"com.example.LogExecutionListener","event":"start"}]}若INFO_JSON_ID_为 null,说明监听器未在设计器中配置;若 JSON 中无对应监听器,说明发布时未勾选“包含监听器”。
5.3 第三级:JVM 级拦截穿透——用 Arthas 动态观测监听器方法调用栈
当监听器逻辑复杂(如调用外部 HTTP 接口),日志无法定位超时点时,用 Arthas 实时观测:
# 1. 附着到Java进程 arthas-boot.jar 12345 # 2. 监控监听器的 notify() 方法(替换为你的类名) watch com.example.LogExecutionListener notify '{params, returnObj, throwExp}' -x 3 # 3. 启动一个流程,触发监听器,Arthas 实时打印: # params[0] = DelegateExecution@7a811971 # returnObj = null # throwExp = java.net.SocketTimeoutException: Read timed out此法可精准定位是监听器内部哪行代码阻塞,无需重启应用。
5.4 第四级:流程实例级开关穿透——不改代码动态禁用监听器
生产环境紧急情况下,需临时禁用某监听器(如邮件监听器发错测试数据)。Activiti 6.0 支持运行时移除监听器:
// 获取流程定义(需先知道 procDefId) ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery() .processDefinitionId("procdef-123").singleResult(); // 获取当前流程定义的所有执行监听器 List<ExecutionListener> listeners = procDef.getExecutionListeners(); // 移除指定类名的监听器(注意:此操作仅对新启动流程生效) repositoryService.removeExecutionListener(procDef.getId(), "com.example.EmailExecutionListener", "start"); // 验证是否移除成功 listeners = procDef.getExecutionListeners(); // size 应减 1注意:此 API 仅影响后续启动的流程,已运行的流程实例监听器不受影响。若要彻底停用,需在
ACT_RE_PROCDEF表中更新VERSION_并重新发布无监听器的流程。
6. 从那以后我每次发布流程前,都强制走一遍“三查一验”清单
“三查一验”不是流程规范,是我带过的三个项目组在踩了 17 次坑后,用血换来的肌肉记忆。它不增加开发时间,但能避免 90% 的线上流程故障。现在我把这套动作固化成 checklist,每次发布前花 90 秒执行,比半夜爬起来修流程强一百倍。
6.1 查租户:确认activiti-app.properties中的租户配置与当前登录用户匹配
打开qqdznyyglpt-workflow-designer/src/META-INF/activiti-app/activiti-app.properties,逐行核对:
activiti.app.multitenancy.enabled=true(必须为 true,否则权限失效);activiti.app.default.tenant.id=的值,是否等于你当前登录账号所属租户(如admin);activiti.app.tenant.resolver.header=的值,是否与前端请求头一致(如X-Tenant-ID)。
若用 Postman 测试,必须在 Headers 中添加
X-Tenant-ID: admin,否则即使账号是 admin,也会被识别为普通租户。
6.2 查异步:扫描流程图中所有async="true"的节点,并确认异步执行器已启用
在设计器中按Ctrl+F搜索async="true",记录所有节点 ID。然后检查activiti-app.properties:
activiti.app.async.executor.activate=true(必须);activiti.app.async.executor.core.pool.size≥ 流程并发峰值 × 1.5(如峰值 100,则设为 150);activiti.app.async.executor.max.pool.size≥core.pool.size× 2。
若流程中有 3 个异步节点,但
core.pool.size=1,会导致任务排队,定时器延迟高达分钟级。
6.3 查外置资源路径:验证processes.zip中所有资源路径符合 Activiti 硬编码规则
解压processes.zip,执行以下 Shell 命令(Linux/macOS):
# 检查表单路径是否以 forms/ 开头 unzip -l processes.zip | grep "forms/" | head -5 # 检查决策表是否在 decision-tables/ 下 unzip -l processes.zip | grep "decision-tables/" | head -5 # 检查 DRL 规则是否在 rules/ 下 unzip -l processes.zip | grep "rules/" | head -5 # 检查 BPMN 文件是否包含正确的 formKey 引用 unzip -p processes.zip leave-approval.bpmn20.xml | grep -A2 -B2 "formKey"若任意一条命令无输出,说明路径错误,发布后必报Resource not found。
6.4 验:用最小流程验证监听器与事件是否真正生效
不测完整业务流,只建一个极简流程test-listener.bpmn20.xml:
<process id="test-listener" name="Test Listener"> <startEvent id="start" /> <sequenceFlow sourceRef="start" targetRef="userTask" /> <userTask id="userTask" name="Test Task" activiti:assignee="admin" /> <sequenceFlow sourceRef="userTask" targetRef="end" /> <endEvent id="end" /> <!-- 添加一个执行监听器 --> <extensionElements> <activiti:executionListener event="start" class="com.example.TestExecutionListener" /> </extensionElements> </process>发布后,用RuntimeService.startProcessInstanceByKey("test-listener")启动,立刻查ACT_RU_TASK表是否有TASK_DEF_KEY_='userTask'的记录,并查日志是否有TestExecutionListener的 DEBUG 输出。只有这个最小流程跑通,才代表你的监听器环境是健康的。
从那以后我每次发布流程前,都强制走一遍“三查一验”清单——不是信不过自己写的代码,而是信不过人类在 deadline 前的判断力。希望帮到你。
本文还有配套的精品资源,点击获取