From bbb2d403a9dd2949a4a921d727591b7297008811 Mon Sep 17 00:00:00 2001 From: "everywhere.z" Date: Mon, 29 Jun 2026 00:29:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=96=B0=E5=A2=9E=20PostProcessN?= =?UTF-8?q?odeExecuteLifeCycle=20=E8=8A=82=E7=82=B9=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E6=89=A9=E5=B1=95=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../yomahub/liteflow/core/NodeComponent.java | 18 ++++++ .../liteflow/lifecycle/LifeCycleHolder.java | 9 +++ .../PostProcessNodeExecuteLifeCycle.java | 21 +++++++ .../nodeexecute/NodeExecuteCollector.java | 32 +++++++++++ .../NodeExecuteLifeCycleSpringbootTest.java | 55 +++++++++++++++++++ .../nodeexecute/TestNodeExecuteLifeCycle.java | 17 ++++++ .../liteflow/test/nodeexecute/cmp/NeACmp.java | 12 ++++ .../test/nodeexecute/cmp/NeBoomCmp.java | 12 ++++ .../nodeexecute/application.properties | 1 + .../test/resources/nodeexecute/flow.el.xml | 10 ++++ 10 files changed, 187 insertions(+) create mode 100644 liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/PostProcessNodeExecuteLifeCycle.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteCollector.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteLifeCycleSpringbootTest.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/TestNodeExecuteLifeCycle.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeACmp.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeBoomCmp.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/application.properties create mode 100644 liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/flow.el.xml diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/core/NodeComponent.java b/liteflow-core/src/main/java/com/yomahub/liteflow/core/NodeComponent.java index b4dbe973a..725bd87c0 100644 --- a/liteflow-core/src/main/java/com/yomahub/liteflow/core/NodeComponent.java +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/core/NodeComponent.java @@ -24,6 +24,8 @@ import com.yomahub.liteflow.flow.element.Node; import com.yomahub.liteflow.flow.entity.CmpStep; import com.yomahub.liteflow.flow.executor.DefaultNodeExecutor; import com.yomahub.liteflow.flow.executor.NodeExecutor; +import com.yomahub.liteflow.lifecycle.LifeCycleHolder; +import com.yomahub.liteflow.lifecycle.PostProcessNodeExecuteLifeCycle; import com.yomahub.liteflow.log.LFLog; import com.yomahub.liteflow.log.LFLoggerManager; import com.yomahub.liteflow.monitor.CompStatistics; @@ -109,6 +111,15 @@ public abstract class NodeComponent{ StopWatch stopWatch = new StopWatch(); stopWatch.start(); + // 节点执行异常引用,供 finally 中的生命周期钩子使用(成功为 null) + Exception nodeExecuteException = null; + + // 节点执行生命周期(前)——list 形,可叠加,与 monitorBus 独立 + List nodeExecuteLifeCycleList = LifeCycleHolder.getPostProcessNodeExecuteLifeCycleList(); + if (!nodeExecuteLifeCycleList.isEmpty()) { + nodeExecuteLifeCycleList.forEach(lc -> lc.postProcessBeforeNodeExecute(self)); + } + try { LOG.info("[O]start component[{}] execution", self.getDisplayName()); @@ -128,6 +139,7 @@ public abstract class NodeComponent{ // 步骤状态设为false,并加入异常 cmpStep.setSuccess(false); cmpStep.setException(e); + nodeExecuteException = e; // 执行失败后回调方法 // 这里要注意,失败方法本身抛出错误,只打出堆栈,往外抛出的还是主要的异常 @@ -162,6 +174,12 @@ public abstract class NodeComponent{ CompStatistics statistics = new CompStatistics(this.getClass().getSimpleName(), timeSpent); monitorBus.addStatistics(statistics); } + + // 节点执行生命周期(后)——带上耗时与异常 + if (!nodeExecuteLifeCycleList.isEmpty()) { + final Exception finalEx = nodeExecuteException; + nodeExecuteLifeCycleList.forEach(lc -> lc.postProcessAfterNodeExecute(self, timeSpent, finalEx)); + } } } diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/LifeCycleHolder.java b/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/LifeCycleHolder.java index 8b9ceab8c..472cf1624 100644 --- a/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/LifeCycleHolder.java +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/LifeCycleHolder.java @@ -22,6 +22,8 @@ public class LifeCycleHolder { private static final List POST_PROCESS_CHAIN_EXECUTE_LIFE_CYCLE_LIST = new ArrayList<>(); + private static final List POST_PROCESS_NODE_EXECUTE_LIFE_CYCLE_LIST = new ArrayList<>(); + public static void addLifeCycle(LifeCycle lifeCycle){ if (PostProcessScriptEngineInitLifeCycle.class.isAssignableFrom(lifeCycle.getClass())){ @@ -34,6 +36,8 @@ public class LifeCycleHolder { POST_PROCESS_FLOW_EXECUTE_LIFE_CYCLE_LIST.add((PostProcessFlowExecuteLifeCycle)lifeCycle); }else if(PostProcessChainExecuteLifeCycle.class.isAssignableFrom(lifeCycle.getClass())){ POST_PROCESS_CHAIN_EXECUTE_LIFE_CYCLE_LIST.add((PostProcessChainExecuteLifeCycle)lifeCycle); + }else if(PostProcessNodeExecuteLifeCycle.class.isAssignableFrom(lifeCycle.getClass())){ + POST_PROCESS_NODE_EXECUTE_LIFE_CYCLE_LIST.add((PostProcessNodeExecuteLifeCycle)lifeCycle); } } @@ -57,11 +61,16 @@ public class LifeCycleHolder { return POST_PROCESS_CHAIN_EXECUTE_LIFE_CYCLE_LIST; } + public static List getPostProcessNodeExecuteLifeCycleList() { + return POST_PROCESS_NODE_EXECUTE_LIFE_CYCLE_LIST; + } + public static void clean(){ POST_PROCESS_SCRIPT_ENGINE_INIT_LIFE_CYCLE_LIST.clear(); POST_PROCESS_CHAIN_BUILD_LIFE_CYCLE_LIST.clear(); POST_PROCESS_NODE_BUILD_LIFE_CYCLE_LIST.clear(); POST_PROCESS_FLOW_EXECUTE_LIFE_CYCLE_LIST.clear(); POST_PROCESS_CHAIN_EXECUTE_LIFE_CYCLE_LIST.clear(); + POST_PROCESS_NODE_EXECUTE_LIFE_CYCLE_LIST.clear(); } } diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/PostProcessNodeExecuteLifeCycle.java b/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/PostProcessNodeExecuteLifeCycle.java new file mode 100644 index 000000000..5a1b4c5bb --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/PostProcessNodeExecuteLifeCycle.java @@ -0,0 +1,21 @@ +package com.yomahub.liteflow.lifecycle; + +import com.yomahub.liteflow.core.NodeComponent; + +/** + * 生命周期接口 + * 执行单个组件(Node)的时候 + * + * @author Bryan.Zhang + */ +public interface PostProcessNodeExecuteLifeCycle extends LifeCycle { + + void postProcessBeforeNodeExecute(NodeComponent cmp); + + /** + * @param cmp 执行完成的组件(含 nodeId / chainId / type) + * @param timeSpent 本次执行耗时(毫秒) + * @param e 执行异常,成功时为 null + */ + void postProcessAfterNodeExecute(NodeComponent cmp, long timeSpent, Exception e); +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteCollector.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteCollector.java new file mode 100644 index 000000000..eeeff31bf --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteCollector.java @@ -0,0 +1,32 @@ +package com.yomahub.liteflow.test.nodeexecute; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** 测试用静态收集器,记录节点执行钩子的回调 */ +public class NodeExecuteCollector { + + public static class Record { + public final String nodeId; + public final long timeSpent; + public final Exception exception; + public Record(String nodeId, long timeSpent, Exception exception) { + this.nodeId = nodeId; + this.timeSpent = timeSpent; + this.exception = exception; + } + } + + public static final List BEFORE = new CopyOnWriteArrayList<>(); + public static final List AFTER = new CopyOnWriteArrayList<>(); + + public static void clear() { + BEFORE.clear(); + AFTER.clear(); + } + + public static List afters() { + return Collections.unmodifiableList(AFTER); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteLifeCycleSpringbootTest.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteLifeCycleSpringbootTest.java new file mode 100644 index 000000000..5a4779f44 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/NodeExecuteLifeCycleSpringbootTest.java @@ -0,0 +1,55 @@ +package com.yomahub.liteflow.test.nodeexecute; + +import com.yomahub.liteflow.core.FlowExecutor; +import com.yomahub.liteflow.flow.LiteflowResponse; +import com.yomahub.liteflow.test.BaseTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.test.context.TestPropertySource; + +import javax.annotation.Resource; + +@TestPropertySource(value = "classpath:/nodeexecute/application.properties") +@SpringBootTest(classes = NodeExecuteLifeCycleSpringbootTest.class) +@EnableAutoConfiguration +@ComponentScan({ "com.yomahub.liteflow.test.nodeexecute" }) +public class NodeExecuteLifeCycleSpringbootTest extends BaseTest { + + @Resource + private FlowExecutor flowExecutor; + + @BeforeEach + public void setUp() { + NodeExecuteCollector.clear(); + } + + @Test + public void testSuccessHooks() { + LiteflowResponse response = flowExecutor.execute2Resp("okChain", "arg"); + Assertions.assertTrue(response.isSuccess()); + // okChain = THEN(neA, neA) → 2 次节点执行 + Assertions.assertEquals(2, NodeExecuteCollector.BEFORE.size()); + Assertions.assertEquals(2, NodeExecuteCollector.afters().size()); + for (NodeExecuteCollector.Record r : NodeExecuteCollector.afters()) { + Assertions.assertEquals("neA", r.nodeId); + Assertions.assertNull(r.exception); + Assertions.assertTrue(r.timeSpent >= 0); + } + } + + @Test + public void testErrorHookCarriesException() { + LiteflowResponse response = flowExecutor.execute2Resp("boomChain", "arg"); + Assertions.assertFalse(response.isSuccess()); + // boomChain = THEN(neA, neBoom):neA 成功,neBoom 抛异常 + Assertions.assertEquals(2, NodeExecuteCollector.afters().size()); + NodeExecuteCollector.Record boom = NodeExecuteCollector.afters().get(1); + Assertions.assertEquals("neBoom", boom.nodeId); + Assertions.assertNotNull(boom.exception); + Assertions.assertEquals(IllegalStateException.class, boom.exception.getClass()); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/TestNodeExecuteLifeCycle.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/TestNodeExecuteLifeCycle.java new file mode 100644 index 000000000..5ab148de6 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/TestNodeExecuteLifeCycle.java @@ -0,0 +1,17 @@ +package com.yomahub.liteflow.test.nodeexecute; + +import com.yomahub.liteflow.core.NodeComponent; +import com.yomahub.liteflow.lifecycle.PostProcessNodeExecuteLifeCycle; +import org.springframework.stereotype.Component; + +@Component +public class TestNodeExecuteLifeCycle implements PostProcessNodeExecuteLifeCycle { + @Override + public void postProcessBeforeNodeExecute(NodeComponent cmp) { + NodeExecuteCollector.BEFORE.add(cmp.getNodeId()); + } + @Override + public void postProcessAfterNodeExecute(NodeComponent cmp, long timeSpent, Exception e) { + NodeExecuteCollector.AFTER.add(new NodeExecuteCollector.Record(cmp.getNodeId(), timeSpent, e)); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeACmp.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeACmp.java new file mode 100644 index 000000000..f04054a0a --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeACmp.java @@ -0,0 +1,12 @@ +package com.yomahub.liteflow.test.nodeexecute.cmp; + +import com.yomahub.liteflow.core.NodeComponent; +import org.springframework.stereotype.Component; + +@Component("neA") +public class NeACmp extends NodeComponent { + @Override + public void process() { + // 正常组件 + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeBoomCmp.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeBoomCmp.java new file mode 100644 index 000000000..b920ca52f --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/nodeexecute/cmp/NeBoomCmp.java @@ -0,0 +1,12 @@ +package com.yomahub.liteflow.test.nodeexecute.cmp; + +import com.yomahub.liteflow.core.NodeComponent; +import org.springframework.stereotype.Component; + +@Component("neBoom") +public class NeBoomCmp extends NodeComponent { + @Override + public void process() { + throw new IllegalStateException("boom"); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/application.properties b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/application.properties new file mode 100644 index 000000000..095e03baa --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/application.properties @@ -0,0 +1 @@ +liteflow.rule-source=nodeexecute/flow.el.xml diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/flow.el.xml new file mode 100644 index 000000000..cb40144db --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/nodeexecute/flow.el.xml @@ -0,0 +1,10 @@ + + + + + THEN(neA, neA); + + + THEN(neA, neBoom); + +