-
Notifications
You must be signed in to change notification settings - Fork 8.8k
expression decoupling survey
SpEL 固然是应用场景广泛、功能强大的表达式语言,但其特定于 Java 语言,不适宜于多语言扩展。所以通过解耦表达式的使用,以支持其它表达式语言势在必行。此外,Seata Saga 在状态机定义时多处地方使用表达式语言,除 SpEL 外还定制了其它种类的表达式如用于异常判断的计算式类型EVALUATOR_TYPE_EXCEPTION,用于生成 UUID 的表达式类型EXPRESSION_TYPE_SEQUENCE。这些表达式相互独立,而构建自定义的计算微内核也可以帮助统一表达式的使用。 针对于表达式具体的使用场景来说,同样是使用 SpEL 表达式,有些地方需要添加 $. 前缀,而有些地方则不需要。这对初学者容易造成混淆。
在 Seata Saga 中构建自定义的计算微内核:
- [KR1] 向下兼容:构建自定义的计算微内核应该对旧版本的状态机 JSON 定义保证兼容性。
- [KR2] 解析效率:解析效率更高是定制化表达式语言取代 SpEL 的一项优点,所以在实现解析器时需注重效率。
- [KR3] 功能覆盖:梳理现有状态机中 SpEL 表达式用法,定制化表达式语言需尽可能覆盖现有用法。
- [KR4] 单测覆盖:计算微内核的单测需覆盖到所有实现的功能。
目前状态机定义表达式解析可以分为两种方式,一种是通过EvaluatorFactoryManager获取 evaluator 来进行评估 (返回的一定是 true / false),另一种是通过 ExpressionFactoryManager创建 expression 来进行解析。
使用 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}"
使用 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"
通过观察 seata-test 中测试用的状态机定义 statelang/*.json 文件,发现EVALUATOR_TYPE_EXCEPTION
和EXPRESSION_TYPE_SEQUENCE
类型表达式由于功能单一所有用法比较固定。但 SpEL 表达式功能较多,且应用的场景广泛,根据上一小节的梳理,SpEL 用法有:Choice 的选项表达式评估、任务 Loop 条件的完成条件评估、任务的输入输出参数绑定、任务 Loop 条件的集合名称。经梳理,SpEL 在状态机定义的这些用法可归为两类:
- 评估 (Choice 的选项表达式评估、任务 Loop 条件的完成条件评估)
- 取值 (任务的输入输出参数绑定、任务 Loop 条件的集合名称)
具体的实现可以继续沿用Expression
和ExpressionFactory
接口,除此之外新建解析器类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。