解决 Activiti UEL 表达式解析异常 TreeBuilderException

问题现象

在基于 Activiti 的工作流引擎开发与运维过程中,当流程实例流转至排他网关、包容网关或带有条件判断的序列流节点时,引擎底层会对配置的条件表达式进行实时解析。若表达式语法不符合规范,控制台通常会抛出如下异常堆栈:

text 复制代码
de.odysseus.el.tree.TreeBuilderException: Error parsing '${...}': lexical error at position X, encountered invalid character '{', expected expression token
    at de.odysseus.el.tree.impl.Builder.build(Builder.java:XXX)
    at de.odysseus.el.tree.TreeStore.get(TreeStore.java:XXX)
    at de.odysseus.el.TreeValueExpression.<init>(TreeValueExpression.java:XXX)
    at de.odysseus.el.ExpressionFactoryImpl.createValueExpression(ExpressionFactoryImpl.java:XXX)
    at org.activiti.engine.impl.el.ExpressionManager.createExpression(ExpressionManager.java:XXX)
    at org.activiti.engine.impl.util.condition.ConditionUtil.hasTrueCondition(ConditionUtil.java:XXX)
    at org.activiti.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation.leaveFlowNode(TakeOutgoingSequenceFlowsOperation.java:XXX)
    ...

该异常的核心特征为 TreeBuilderException,且错误信息中明确提示词法分析阶段遇到非法字符 { 或期望表达式令牌(Token)。这表明 Activiti 底层的 UEL(Unified Expression Language)解析器在构建表达式抽象语法树时,遇到了无法识别的词法符号,导致解析流程中断,流程实例无法继续流转。

原因分析

Activiti 引擎底层依赖 JUEL(Java Unified Expression Language)实现 UEL 表达式的解析与执行。UEL 作为 Java EE 标准规范的一部分,其语法有严格定义:整个表达式必须且只能被一对 ${}#{} 包裹,内部不允许再次出现表达式定界符

核心根因:表达式定界符嵌套

导致该报错最常见、最直接的原因是在同一个条件表达式中嵌套使用了 ${}

部分开发者在组合多个条件变量时,习惯性地将每个变量都用 ${} 包裹,写出如下错误语法:

xml 复制代码
<!-- ❌ 错误写法:内部嵌套了 ${} -->
${(varA <= 4) && ${varB}}

解析器内部行为拆解:

  1. 解析器扫描到第一个 ${,进入表达式解析模式,开始构建语法树;
  2. 逐字符读取 (varA <= 4) && ,该片段符合 UEL 逻辑运算语法,解析正常;
  3. 继续读取到第二个 ${ 时,解析器当前处于表达式内部上下文,期望接收操作数(变量名、字面量)或运算符,但遇到了字符 {
  4. 词法分析器(Lexer)无法将 { 映射为任何合法 Token,判定为词法错误,立即抛出 lexical error,终止语法树构建,最终向上层抛出 TreeBuilderException

其他潜在诱因

除嵌套定界符外,以下情况也可能触发同类或相似异常:

  • XML 特殊字符未转义 :表达式中的 <>&& 等符号是 XML 保留字符,直接书写会导致 XML 解析器先于 UEL 解析器报错,或产生截断后的残缺表达式传入 UEL 解析器;
  • 字符串字面量引号错误:使用双引号包裹字符串可能与 XML 属性引号冲突,导致表达式被截断;
  • 表达式内容为空或格式残缺 :复制粘贴过程中丢失闭合的 },导致解析器读到末尾仍未找到结束标记。

解决方案

1. 移除内部嵌套的定界符(核心修复)

UEL 表达式只要最外层存在 ${},内部所有变量引用、方法调用、属性访问均会自动解析,无需也不允许再次使用 ${}

xml 复制代码
<!-- ✅ 正确写法:仅保留最外层 ${} -->
${varA <= 4 && varB}

2. 使用 CDATA 规避 XML 特殊字符问题

在 BPMN 2.0 XML 文件中,强烈建议使用 CDATA 块包裹条件表达式,彻底避免 XML 转义带来的隐性风险:

xml 复制代码
<sequenceFlow id="flow1" sourceRef="gateway1" targetRef="task1">
    <conditionExpression xsi:type="tFormalExpression">
        <![CDATA[${varA <= 4 && varB}]]>
    </conditionExpression>
</sequenceFlow>

若不使用 CDATA,则必须手动转义:${varA &lt;= 4 &amp;&amp; varB}

3. 确保表达式返回布尔类型

修复语法后,需验证表达式最终计算结果为 Boolean 类型(true / false)。若返回 null、数值或字符串,Activiti 会抛出 condition expression returns non-Boolean 异常,虽不是本文讨论的解析异常,但属于同一环节的关联故障。

排查与预防规范

为从根源上杜绝此类问题,建议在开发、Code Review 及测试阶段建立以下标准化规范:

静态检查

  • 正则全局扫描 :在项目 BPMN 资源目录中执行正则搜索 \$\{.*\$\{,可一键定位所有嵌套定界符的错误写法;
  • IDE 插件辅助:启用 Activiti / Flowable 官方 IDE 插件,多数插件可在编辑时对 UEL 表达式进行实时语法校验,提前标红错误。

编码规范

  • 字符串比较统一使用单引号${status == 'approved'},禁止使用双引号;
  • 防御性变量访问 :对可能未赋值的流程变量,使用 empty 运算符或 execution.hasVariable() 前置校验,例如 ${!empty varA && varA <= 4},避免空指针导致的运行时异常;
  • 复杂逻辑外置到 Service Bean :当条件判断涉及多字段组合、日期计算、外部数据查询等复杂逻辑时,禁止在 UEL 中硬编码。应封装为 Spring Bean 方法,通过方法表达式调用:${approvalService.checkCondition(execution)}。此举既规避语法风险,又提升可测试性与可维护性。

测试验证

  • 单元测试覆盖:对关键网关条件编写独立单元测试,模拟不同变量组合验证表达式返回值;
  • 流程定义部署预检:在 CI/CD 流水线中增加流程定义合法性校验步骤,部署前自动解析所有 BPMN 文件,拦截包含语法错误的流程定义进入生产环境。
相关推荐
大龄码农有梦想1 个月前
Camunda 7 与 Camunda 8 不是升级关系:架构差异与选型边界
activiti·flowable·流程引擎·camunda·开源工作流·开源工作流引擎
xrl20124 个月前
JeecgBoot集成Activiti工作流实现定时器案例
activiti·定时器·flowable·camunda·jeecgboot
spencer_tseng8 个月前
activiti-engine-7.0.0.Beta2.jar org/activiti/db/create/*.sql
activiti
hhzz9 个月前
Activiti7工作流(五)流程操作
java·activiti·工作流引擎·工作流
D_alyoo10 个月前
06 Activiti 与 Spring Boot 整合
java·activiti·activiti7源码
D_alyoo1 年前
Activiti 中各种 startProcessInstance 接口之间的区别
java·activiti
老马啸西风1 年前
工作流引擎-18-开源审批流项目之 plumdo-work 工作流,表单,报表结合的多模块系统
vue.js·开源·activiti·workflow·flowable·oa·bpm
老马啸西风1 年前
工作流引擎-16-开源审批流项目之 整合Flowable官方的Rest包
开源·activiti·workflow·flowable·erp·oa·bpm
老马啸西风1 年前
工作流引擎-11-开源 BPM 项目 jbpm
开源·activiti·workflow·oa·bpm