Skip to content

expression decoupling survey

wt_better edited this page Dec 21, 2023 · 1 revision

SpEL 固然是应用场景广泛、功能强大的表达式语言,但其特定于 Java 语言,不适宜于多语言扩展。所以通过解耦表达式的使用,以支持其它表达式语言势在必行。此外,Seata Saga 在状态机定义时多处地方使用表达式语言,除 SpEL 外还定制了其它种类的表达式如用于异常判断的计算式类型EVALUATOR_TYPE_EXCEPTION,用于生成 UUID 的表达式类型EXPRESSION_TYPE_SEQUENCE。这些表达式相互独立,而构建自定义的计算微内核也可以帮助统一表达式的使用。 针对于表达式具体的使用场景来说,同样是使用 SpEL 表达式,有些地方需要添加 $. 前缀,而有些地方则不需要。这对初学者容易造成混淆。

设计目标

在 Seata Saga 中构建自定义的计算微内核:

  1. [KR1] 向下兼容:构建自定义的计算微内核应该对旧版本的状态机 JSON 定义保证兼容性。
  2. [KR2] 解析效率:解析效率更高是定制化表达式语言取代 SpEL 的一项优点,所以在实现解析器时需注重效率。
  3. [KR3] 功能覆盖:梳理现有状态机中 SpEL 表达式用法,定制化表达式语言需尽可能覆盖现有用法。
  4. [KR4] 单测覆盖:计算微内核的单测需覆盖到所有实现的功能。

表达式现状

目前状态机定义表达式解析可以分为两种方式,一种是通过EvaluatorFactoryManager获取 evaluator 来进行评估 (返回的一定是 true / false),另一种是通过 ExpressionFactoryManager创建 expression 来进行解析。

使用 evaluator 进行解析

使用 evaluator 解析方式的场景有:

  • ServiceTask 执行状态评估 ServiceTask.Status
  • Choice 的选项表达式评估 Choice.Choices.Expression
  • 任务 Loop 条件的完成条件评估 Task.Loop.CompletionCondition

其中,Choice 的选项表达式评估和任务 Loop 条件的完成条件评估都使用的是 SpEL 表达式评估。 而执行状态评估的实现在postProcess中,通过调用decideExecutionStatus来决定服务任务实例的最终执行状态,其EvaluatorFactoryManager组件将状态定义 (不是状态实例) 的statusMatchList也就是 JSON 文件中的Status字段,转换为对应的Evaluator。具体使用的是createEvaluator方法:

if (expressionStr.startsWith("$")) {
    int expTypeStart = expressionStr.indexOf("$");
    int expTypeEnd = expressionStr.indexOf("{", expTypeStart);

    if (expTypeStart >= 0 && expTypeEnd > expTypeStart) {
        expressionType = expressionStr.substring(expTypeStart + 1, expTypeEnd);
    }

    int expEnd = expressionStr.lastIndexOf("}");
    if (expTypeEnd > 0 && expEnd > expTypeEnd) {
        expressionContent = expressionStr.substring(expTypeEnd + 1, expEnd);
    }
} else {
    expressionContent = expressionStr;
}

执行状态评估目前存在两种表达式结构: 其中EVALUATOR_TYPE_DEFAULT结构使用ExpressionEvaluatorFactory实现,其提供的 evaluator 旨在使用表达式语言进行计算,而其依赖的表达式工厂ExpressionFactory使用的是SpringELExpressionFactory版本实现,是对 SpEL 表达式进行评估。例如, "#root == true" 而EVALUATOR_TYPE_EXCEPTION结构使用ExceptionMatchEvaluatorFactory实现,其提供的 evaluator 旨在判别业务抛出的异常是否匹配。例如, "$Exception{java.lang.Throwable}" image-20230524174055714.png

使用 expression 进行解析

使用 expression 解析方式的场景有:

  • 任务 (ServiceTask, ScriptTask) 的输入输出参数 Task.Input 和 Task.Output
  • 任务 Loop 条件的集合名称 Task.Loop.Collection

使用 expression 解析目前也存在两种表达式结构: 其中DEFAULT_EXPRESSION_TYPE结构使用SpringELExpressionFactory实现,其使用 SpEL 解析器进行解析。例如, "$.[a]" 而EXPRESSION_TYPE_SEQUENCE结构使用SequenceExpressionFactory实现,其用于 UUID 生成。例如, "$Sequence.BUSINESS_KEY|SIMPLE" image-20230525205346248.png

初步方案

通过观察 seata-test 中测试用的状态机定义 statelang/*.json 文件,发现EVALUATOR_TYPE_EXCEPTIONEXPRESSION_TYPE_SEQUENCE类型表达式由于功能单一所有用法比较固定。但 SpEL 表达式功能较多,且应用的场景广泛,根据上一小节的梳理,SpEL 用法有:Choice 的选项表达式评估、任务 Loop 条件的完成条件评估、任务的输入输出参数绑定、任务 Loop 条件的集合名称。经梳理,SpEL 在状态机定义的这些用法可归为两类:

  • 评估 (Choice 的选项表达式评估、任务 Loop 条件的完成条件评估)
  • 取值 (任务的输入输出参数绑定、任务 Loop 条件的集合名称)

具体的实现可以继续沿用ExpressionExpressionFactory接口,除此之外新建解析器类ExpressionResolver用于解析自定义的表达式语言。

扩展:评估策略

目前是执行状态评估是顺序执行的,如果有条件同时满足的话则会按照第一个命中规则赋值:

for (Object evaluatorObj : statusEvaluators.keySet()) {
    Evaluator evaluator = (Evaluator)evaluatorObj;
    String statusVal = statusEvaluators.get(evaluator);
    if (evaluator.evaluate(context.getVariables())) {
        stateInstance.setStatus(ExecutionStatus.valueOf(statusVal));
        break;
    }
}

同理,对于 Choice 状态的选项评估,也是按照第一个命中选项赋值:

for (Map.Entry<Object, String> entry : choiceEvaluators.entrySet()) {
    evaluator = (Evaluator)entry.getKey();
    if (evaluator.evaluate(context.getVariables())) {
        context.setVariable(DomainConstants.VAR_NAME_CURRENT_CHOICE, entry.getValue());
        return;
    }
}

这里也可以是一个后续扩展点,参考 Camunda DMN Hit Policy[^2] 可以添加唯一、任意、首个 (当前方案)、优先级 (比如规定FA要比SU规则优先级更高),状态机涉及到规则评估时也可以添加 Hit Policy

Clone this wiki locally