From 68dc35ee92a4fafcd851435e5ae18b33d8621847 Mon Sep 17 00:00:00 2001 From: "everywhere.z" Date: Sat, 27 Jun 2026 13:51:22 +0800 Subject: [PATCH] =?UTF-8?q?bug=20#IJX8AE=20=E8=A1=A8=E8=BE=BE=E5=BC=8F?= =?UTF-8?q?=E7=BA=A7=20bind=20=E7=9A=84=E6=95=B0=E6=8D=AE=EF=BC=8C?= =?UTF-8?q?=E5=9C=A8=E6=B5=81=E7=A8=8B=E6=89=A7=E8=A1=8C=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E5=90=8E=E6=97=A0=E6=B3=95=E9=80=9A=E8=BF=87=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E4=BA=8B=E5=90=8E=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builder/el/operator/BindOperator.java | 36 +++++++++++++++++++ .../bindData/BindDataSpringbootTest1.java | 35 ++++++++++++++++++ .../src/test/resources/bindData/flow1.xml | 13 +++++++ 3 files changed, 84 insertions(+) diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/builder/el/operator/BindOperator.java b/liteflow-core/src/main/java/com/yomahub/liteflow/builder/el/operator/BindOperator.java index b02797474..6c0060c96 100644 --- a/liteflow-core/src/main/java/com/yomahub/liteflow/builder/el/operator/BindOperator.java +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/builder/el/operator/BindOperator.java @@ -48,6 +48,12 @@ public class BindOperator extends BaseOperator { if (override) { clearNodeBindData(condition, key); } + // 将 bind 数据下放到该 Condition 直属(不跨子 chain 引用)的节点上, + // 使流程执行结束后仍能通过 Node#getBindData(如 CmpStep.getRefNode().getBindData(...))读取到, + // 与 2.15.1 的表现保持一致。 + // 注意:这里只下放到属于当前 chain 的节点克隆,不会递归进被引用的子 chain, + // 从而避免污染被多个 chain 共享的子 chain(见 #ID7OTO / #IDCBQ2)。 + putBindDataToLocalNodes(condition, key, value, override); return condition; } @@ -75,4 +81,34 @@ public class BindOperator extends BaseOperator { } }); } + + /** + * 将 bind 数据下放到该 Condition 直属的节点上(递归进嵌套 Condition,但不跨子 chain 引用)。 + *

+ * 这些节点是当前 chain 构建时 clone 出来的、专属于本 chain 的实例(每个 clone 持有独立的 + * bindDataMap),因此写入 bind 数据不会污染被多个 chain 共享的子 chain。 + * 下放之后,流程执行结束仍可通过 {@code CmpStep.getRefNode().getBindData(key)} 读取到表达式级 bind 的值。 + *

+ * 对于条件中以 chainId 形式引用的子 chain({@link Chain} 类型,对象在 FlowBus 中被多个 chain 共享), + * 这里不会写入其内部节点,其 bind 数据仍由运行时通过 Condition 调用栈解析,从而避免数据污染(见 #ID7OTO / #IDCBQ2)。 + * + * @param condition 目标 Condition + * @param key bind 数据的 key + * @param value bind 数据的 value + * @param override 为 true 时强制覆盖节点级已有的同 key 数据,否则保留节点级 bind(节点级优先) + */ + private void putBindDataToLocalNodes(Condition condition, String key, String value, boolean override) { + condition.getExecutableGroup().values().forEach(executableList -> executableList.forEach(executable -> { + if (executable instanceof Node) { + Node node = (Node) executable; + // 节点级 bind 优先:非 override 情况下,节点已有同 key 数据时不覆盖 + if (override || !node.hasBindData(key)) { + node.putBindData(key, value); + } + } else if (executable instanceof Condition) { + // 递归进嵌套 Condition(同属当前 chain);Chain 类型不进入此分支,避免污染共享子 chain + putBindDataToLocalNodes((Condition) executable, key, value, override); + } + })); + } } diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/bindData/BindDataSpringbootTest1.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/bindData/BindDataSpringbootTest1.java index d5aeea52c..7de6062fc 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/bindData/BindDataSpringbootTest1.java +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/bindData/BindDataSpringbootTest1.java @@ -2,6 +2,7 @@ package com.yomahub.liteflow.test.bindData; import com.yomahub.liteflow.core.FlowExecutor; import com.yomahub.liteflow.flow.LiteflowResponse; +import com.yomahub.liteflow.flow.entity.CmpStep; import com.yomahub.liteflow.slot.DefaultContext; import com.yomahub.liteflow.test.BaseTest; import com.yomahub.liteflow.util.JsonUtil; @@ -13,6 +14,8 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.test.context.TestPropertySource; import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; /** @@ -105,4 +108,36 @@ public class BindDataSpringbootTest1 extends BaseTest { Assertions.assertTrue(response.isSuccess()); } + // 复现 issue #IJT2YA:表达式级 bind 的数据,执行结束后应能通过 Node#getBindData 读取到, + // 且节点级 bind 不会被表达式级 bind 覆盖(a=v2, b=v1, c=v2) + @Test + public void testBind8() throws Exception { + String el = "THEN(a, b.bind(\"k\", \"v1\"), c).bind(\"k\", \"v2\");"; + LiteflowResponse response = flowExecutor.execute2RespWithEL(el); + Assertions.assertTrue(response.isSuccess()); + Map resultMap = new HashMap<>(); + for (CmpStep cmpStep : response.getExecuteStepQueue()) { + resultMap.put(cmpStep.getNodeId(), cmpStep.getRefNode().getBindData("k")); + } + Assertions.assertEquals("v2", resultMap.get("a")); + Assertions.assertEquals("v1", resultMap.get("b")); + Assertions.assertEquals("v2", resultMap.get("c")); + } + + // 回归保护:表达式级 bind 作用于含子 chain 引用的条件时,绝不能污染被共享的子 chain(#ID7OTO) + @Test + public void testBind9() throws Exception { + // 带 bind 的 chain:运行时子 chain(psub)内的节点 a 应解析到 outer + LiteflowResponse responseC = flowExecutor.execute2Resp("pchainC", "arg"); + DefaultContext ctxC = responseC.getFirstContextBean(); + Assertions.assertTrue(responseC.isSuccess()); + Assertions.assertEquals("outer", ctxC.getData("a")); + + // 未 bind 的 chain 单独引用同一子 chain,绝不能被上面的 bind 污染 + LiteflowResponse responseB = flowExecutor.execute2Resp("pchainB", "arg"); + DefaultContext ctxB = responseB.getFirstContextBean(); + Assertions.assertTrue(responseB.isSuccess()); + Assertions.assertNull(ctxB.getData("a")); + } + } diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/bindData/flow1.xml b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/bindData/flow1.xml index a6a3bd9b3..2abf7014d 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/bindData/flow1.xml +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/bindData/flow1.xml @@ -33,4 +33,17 @@ THEN(a, b.bind("k1", "test_b"), c).bind("k1", "test", true); + + + + THEN(a); + + + + THEN(psub); + + + + THEN(d, psub).bind("k1", "outer"); + \ No newline at end of file