完成流程任务时,如果日志里出现下面的异常,第一反应很容易是监听器没有初始化,或者引擎里的服务对象为空:
ActivitiException: Exception while invoking TaskListener:
Exception while invoking TaskListener: null
但这里的 null 只是异常消息的一部分,不是空对象的名称。真正的定位信息在更深一层的 Caused by 中。
沿异常链找到业务代码
这次问题发生在 Activiti 7.1.0.M6、Spring 5.1.13、Java 8 的项目中。把重复的框架调用帧收起来后,异常层次是:
ActivitiException: Exception while invoking TaskListener
caused by: ActivitiException: Exception while invoking TaskListener
caused by: java.lang.NullPointerException
at example.project.listener.BusinessListener.notify(BusinessListener.java:52)
包名是脱敏示例,关键是最内层指向了业务监听器的 notify,而不是给出了一个“Activiti 的 getVariable 实现必然有错”的结论。先在对应源码行设断点,再拆开这一行的表达式,通常比搜索最外层错误更有效。
服务和 ID 不为空,仍然可能空指针
当时关注的是读取审批变量:
Object value = runtimeService.getVariable(executionId, "pass");
即使 runtimeService 和 executionId 非空,也不代表变量存在、返回类型符合预期,或同一行后面的访问一定安全。例如下面的独立示例,会在返回 null 时因为自动拆箱而报空指针:
boolean pass = (Boolean) runtimeService.getVariable(executionId, "pass");
这说明一种可能的机制,不是对当年那一行缺失上下文的根因认定。更容易诊断的写法是先检查值和类型:
Object value = runtimeService.getVariable(executionId, "pass");
if (!(value instanceof Boolean)) {
throw new IllegalStateException("审批变量 pass 缺失或类型不是 Boolean");
}
boolean pass = (Boolean) value;
这里选择缺失时拒绝继续执行,避免把“不知道是否通过”悄悄当成 false 或 true。实际默认值应由业务规则决定。
TaskService 和 RuntimeService 的参数不能混用
同一个变量名,可以从不同作用域读取:
| 调用 | 第一个参数 | 查找范围 |
|---|---|---|
runtimeService.getVariable | executionId | 指定执行及可见的父作用域 |
taskService.getVariable | taskId | 任务及可见的执行作用域 |
taskService.getVariableLocal | taskId | 任务局部作用域 |
当时改用 TaskService.getVariable 后,故障现象消失了;这条经验值得记下,但不能因此推导“监听器里只能用 TaskService”。如果使用任务入口,应传任务 ID:
Object value = taskService.getVariable(delegateTask.getId(), "pass");
不能只把服务对象的名字替换掉,却继续传 executionId。不同入口得到不同结果时,要检查变量写在任务局部还是执行作用域,以及 create、assignment、complete 等事件触发时变量是否已设置。接口语义可对照 TaskService 与 RuntimeService 的对应版本说明。
推荐的定位顺序
先保存完整异常链,找到最内层业务行;再确认监听事件、任务 ID、执行 ID、变量的设置时机和作用域。将复合表达式拆成“读取—判空—检查类型—使用”几个步骤,逐项观察。
如果仍不能解释差异,用一个只有必要节点和监听器的测试流程比较两个读取入口。不要在尚未确定变量语义时,给审批判断随意补默认值;那可能只是让异常消失,却改变了审批结果。