mirror of
https://gitee.com/dromara/liteFlow.git
synced 2026-05-14 20:22:07 +08:00
feature #I4HGOW 支持链路的前置和后置节点
This commit is contained in:
@@ -16,6 +16,7 @@ import com.yomahub.liteflow.entity.data.Slot;
|
||||
import com.yomahub.liteflow.entity.flow.parallel.CompletableFutureTimeout;
|
||||
import com.yomahub.liteflow.entity.flow.parallel.ParallelSupplier;
|
||||
import com.yomahub.liteflow.entity.flow.parallel.WhenFutureObj;
|
||||
import com.yomahub.liteflow.enums.ConditionTypeEnum;
|
||||
import com.yomahub.liteflow.enums.ExecuteTypeEnum;
|
||||
import com.yomahub.liteflow.exception.FlowSystemException;
|
||||
import com.yomahub.liteflow.exception.WhenExecuteException;
|
||||
@@ -72,15 +73,31 @@ public class Chain implements Executable {
|
||||
|
||||
Slot slot = DataBus.getSlot(slotIndex);
|
||||
|
||||
//循环chain里包含的condition,每一个condition有可能是then,也有可能是when
|
||||
//when的话为异步,用闭锁进行等待,所有when结束后才能进入下一个condition
|
||||
for (Condition condition : conditionList) {
|
||||
if (condition instanceof ThenCondition) {
|
||||
for (Executable executableItem : condition.getNodeList()) {
|
||||
//先把finally的节点过滤出来
|
||||
List<Condition> finallyConditionList = conditionList.stream().filter(condition ->
|
||||
condition.getConditionType().equals(ConditionTypeEnum.TYPE_FINALLY.getType())).collect(Collectors.toList());
|
||||
|
||||
//循环chain里包含的condition,每一个condition分四种类型:pre,then,when,finally
|
||||
//这里conditionList其实已经是有序的,pre一定在最前面,finally一定在最后面
|
||||
try{
|
||||
for (Condition condition : conditionList) {
|
||||
if (condition instanceof PreCondition){
|
||||
for (Executable executableItem : condition.getNodeList()) {
|
||||
executableItem.execute(slotIndex);
|
||||
}
|
||||
} else if (condition instanceof ThenCondition) {
|
||||
for (Executable executableItem : condition.getNodeList()) {
|
||||
executableItem.execute(slotIndex);
|
||||
}
|
||||
} else if (condition instanceof WhenCondition) {
|
||||
executeAsyncCondition((WhenCondition) condition, slotIndex, slot.getRequestId());
|
||||
}
|
||||
}
|
||||
}finally {
|
||||
for (Condition finallyCondition : finallyConditionList){
|
||||
for(Executable executableItem : finallyCondition.getNodeList()){
|
||||
executableItem.execute(slotIndex);
|
||||
}
|
||||
} else if (condition instanceof WhenCondition) {
|
||||
executeAsyncCondition((WhenCondition) condition, slotIndex, slot.getRequestId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.entity.flow;
|
||||
|
||||
/**
|
||||
* 前置Condition
|
||||
* @author Bryan.Zhang
|
||||
* @since 2.6.4
|
||||
*/
|
||||
public class FinallyCondition extends Condition {
|
||||
|
||||
public FinallyCondition(Condition condition){
|
||||
super(condition.getNodeList());
|
||||
super.setConditionType(condition.getConditionType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.entity.flow;
|
||||
|
||||
/**
|
||||
* 前置Condition
|
||||
* @author Bryan.Zhang
|
||||
* @since 2.6.4
|
||||
*/
|
||||
public class PreCondition extends Condition {
|
||||
|
||||
public PreCondition(Condition condition){
|
||||
super(condition.getNodeList());
|
||||
super.setConditionType(condition.getConditionType());
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,12 @@
|
||||
*/
|
||||
package com.yomahub.liteflow.entity.flow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 串行器
|
||||
* @author Bryan.Zhang
|
||||
*/
|
||||
public class ThenCondition extends Condition {
|
||||
|
||||
public ThenCondition(List<Executable> nodeList) {
|
||||
super(nodeList);
|
||||
}
|
||||
|
||||
public ThenCondition(Condition condition){
|
||||
super(condition.getNodeList());
|
||||
super.setConditionType(condition.getConditionType());
|
||||
|
||||
@@ -7,25 +7,12 @@
|
||||
*/
|
||||
package com.yomahub.liteflow.entity.flow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 并行器
|
||||
* @author Bryan.Zhang
|
||||
*/
|
||||
public class WhenCondition extends Condition{
|
||||
|
||||
|
||||
public WhenCondition(List<Executable> nodeList) {
|
||||
super(nodeList);
|
||||
super.setErrorResume(true);
|
||||
}
|
||||
|
||||
public WhenCondition(List<Executable> nodeList, boolean errorResume) {
|
||||
super(nodeList);
|
||||
super.setErrorResume(errorResume);
|
||||
}
|
||||
|
||||
public WhenCondition(Condition condition) {
|
||||
super(condition.getNodeList());
|
||||
super.setConditionType(condition.getConditionType());
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.yomahub.liteflow.enums;
|
||||
|
||||
public enum ConditionTypeEnum {
|
||||
TYPE_THEN("then","then"),
|
||||
TYPE_WHEN("when","when")
|
||||
TYPE_WHEN("when","when"),
|
||||
TYPE_PRE("pre","pre"),
|
||||
TYPE_FINALLY("finally","finally")
|
||||
;
|
||||
private String type;
|
||||
private String name;
|
||||
|
||||
@@ -4,9 +4,8 @@ import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.yomahub.liteflow.entity.flow.Condition;
|
||||
import com.yomahub.liteflow.entity.flow.ThenCondition;
|
||||
import com.yomahub.liteflow.entity.flow.WhenCondition;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.yomahub.liteflow.entity.flow.*;
|
||||
import com.yomahub.liteflow.enums.ConditionTypeEnum;
|
||||
import com.yomahub.liteflow.exception.ConfigErrorException;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -15,12 +14,10 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 虽则Parser的抽象类,所有的parser需要继承这个抽象类
|
||||
* @author guodongqing
|
||||
* @since 2.5.0
|
||||
*/
|
||||
@@ -28,10 +25,15 @@ public abstract class FlowParser {
|
||||
|
||||
public abstract void parseMain(List<String> pathList) throws Exception;
|
||||
|
||||
public abstract void parse(List<String> contentList) throws Exception ;
|
||||
public abstract void parse(List<String> contentList) throws Exception;
|
||||
|
||||
protected void buildBaseFlowConditions(List<Condition> conditionList,Condition condition){
|
||||
if (condition.getConditionType().equals(ConditionTypeEnum.TYPE_THEN.getType())) {
|
||||
protected void buildConditions(List<Condition> conditionList, Condition condition) {
|
||||
//这里进行合并逻辑
|
||||
//对于then来说,相邻的2个then会合并成一个condition
|
||||
//对于when来说,相同组的when会合并成一个condition,不同组的when还是会拆开
|
||||
if (condition.getConditionType().equals(ConditionTypeEnum.TYPE_PRE.getType())) {
|
||||
conditionList.add(new PreCondition(condition));
|
||||
} else if (condition.getConditionType().equals(ConditionTypeEnum.TYPE_THEN.getType())) {
|
||||
if (conditionList.size() >= 1 &&
|
||||
CollectionUtil.getLast(conditionList) instanceof ThenCondition) {
|
||||
CollectionUtil.getLast(conditionList).getNodeList().addAll(condition.getNodeList());
|
||||
@@ -46,24 +48,37 @@ public abstract class FlowParser {
|
||||
} else {
|
||||
conditionList.add(new WhenCondition(condition));
|
||||
}
|
||||
} else if (condition.getConditionType().equals(ConditionTypeEnum.TYPE_FINALLY.getType())) {
|
||||
conditionList.add(new FinallyCondition(condition));
|
||||
}
|
||||
|
||||
//每一次build之后,对conditionList进行排序,pre最前面,finally最后
|
||||
//这里为什么要排序,因为在声明的时候,哪怕有人不把pre放最前,finally放最后,但最终也要确保是正确的顺序
|
||||
CollectionUtil.sort(conditionList, (o1, o2) -> {
|
||||
if (o1.getConditionType().equals(ConditionTypeEnum.TYPE_PRE.getType()) || o2.getConditionType().equals(ConditionTypeEnum.TYPE_FINALLY.getType())){
|
||||
return -1;
|
||||
} else if (o2.getConditionType().equals(ConditionTypeEnum.TYPE_PRE.getType()) || o1.getConditionType().equals(ConditionTypeEnum.TYPE_FINALLY.getType())){
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据配置的ruleSource查找匹配的资源
|
||||
* 根据配置的ruleSource查找匹配的资源
|
||||
*/
|
||||
protected Resource[] matchRuleResources(final List<String> pathList) throws IOException {
|
||||
protected Resource[] matchRuleResources(final List<String> pathList) throws IOException {
|
||||
Assert.notEmpty(pathList, "rule source must not be null");
|
||||
|
||||
List<Resource> allResource = new ArrayList<>();
|
||||
for (String path : pathList){
|
||||
for (String path : pathList) {
|
||||
String locationPattern = path;
|
||||
if (!locationPattern.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
|
||||
locationPattern = ResourceUtils.CLASSPATH_URL_PREFIX + locationPattern;
|
||||
}
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
Resource[] resources = resolver.getResources(locationPattern);
|
||||
if(ArrayUtil.isEmpty(resources)) {
|
||||
if (ArrayUtil.isEmpty(resources)) {
|
||||
throw new ConfigErrorException("config error,please check rule source property");
|
||||
}
|
||||
allResource.addAll(ListUtil.toList(resources));
|
||||
@@ -72,7 +87,7 @@ public abstract class FlowParser {
|
||||
//如果有多个资源,检查资源都是同一个类型,如果出现不同类型的配置,则抛出错误提示
|
||||
Set<String> fileTypeSet = new HashSet<>();
|
||||
allResource.forEach(resource -> fileTypeSet.add(FileUtil.extName(resource.getFilename())));
|
||||
if (fileTypeSet.size() != 1){
|
||||
if (fileTypeSet.size() != 1) {
|
||||
throw new ConfigErrorException("config error,please use the same type of configuration");
|
||||
}
|
||||
|
||||
|
||||
@@ -131,14 +131,13 @@ public abstract class JsonFlowParser extends FlowParser {
|
||||
String condArrayStr;
|
||||
String[] condArray;
|
||||
List<Executable> chainNodeList;
|
||||
List<Condition> conditionList;
|
||||
List<Condition> conditionList = new ArrayList<>();
|
||||
String group;
|
||||
String errorResume;
|
||||
Condition condition;
|
||||
String any;
|
||||
String chainName = chainObject.getString("name");
|
||||
JSONArray conditionArray = chainObject.getJSONArray("condition");
|
||||
conditionList = new ArrayList<>();
|
||||
for (Object o : conditionArray) {
|
||||
JSONObject condObject = (JSONObject) o;
|
||||
String condType = condObject.getString("type");
|
||||
@@ -199,7 +198,7 @@ public abstract class JsonFlowParser extends FlowParser {
|
||||
condition.setAny(any.equals(Boolean.TRUE.toString()));
|
||||
condition.setConditionType(condType);
|
||||
condition.setNodeList(chainNodeList);
|
||||
super.buildBaseFlowConditions(conditionList, condition);
|
||||
super.buildConditions(conditionList, condition);
|
||||
}
|
||||
FlowBus.addChain(chainName, new Chain(chainName, conditionList));
|
||||
}
|
||||
|
||||
@@ -135,10 +135,9 @@ public abstract class XmlFlowParser extends FlowParser {
|
||||
Condition condition;
|
||||
Element condE;
|
||||
List<Executable> chainNodeList;
|
||||
List<Condition> conditionList;
|
||||
List<Condition> conditionList = new ArrayList<>();
|
||||
|
||||
String chainName = e.attributeValue("name");
|
||||
conditionList = new ArrayList<>();
|
||||
for (Iterator<Element> it = e.elementIterator(); it.hasNext(); ) {
|
||||
condE = it.next();
|
||||
condArrayStr = condE.attributeValue("value");
|
||||
@@ -198,7 +197,7 @@ public abstract class XmlFlowParser extends FlowParser {
|
||||
condition.setAny(any.equals(Boolean.TRUE.toString()));
|
||||
condition.setConditionType(condE.getName());
|
||||
condition.setNodeList(chainNodeList);
|
||||
super.buildBaseFlowConditions(conditionList, condition);
|
||||
super.buildConditions(conditionList, condition);
|
||||
}
|
||||
FlowBus.addChain(chainName, new Chain(chainName, conditionList));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.yomahub.liteflow.test.preAndFinally;
|
||||
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.entity.data.DefaultSlot;
|
||||
import com.yomahub.liteflow.entity.data.LiteflowResponse;
|
||||
import com.yomahub.liteflow.test.BaseTest;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
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 org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* springboot环境下pre节点和finally节点的测试
|
||||
* @author Bryan.Zhang
|
||||
* @since 2.6.4
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@TestPropertySource(value = "classpath:/preAndFinally/application.properties")
|
||||
@SpringBootTest(classes = PreAndFinallySpringbootTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({"com.yomahub.liteflow.test.preAndFinally.cmp"})
|
||||
public class PreAndFinallySpringbootTest extends BaseTest {
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
//测试普通的pre和finally节点
|
||||
@Test
|
||||
public void testPreAndFinally1() throws Exception{
|
||||
LiteflowResponse<DefaultSlot> response = flowExecutor.execute2Resp("chain1", "arg");
|
||||
Assert.assertTrue(response.isSuccess());
|
||||
Assert.assertEquals("p1==>p2==>a==>b==>c==>f1==>f2",response.getSlot().printStep());
|
||||
}
|
||||
|
||||
//测试pre和finally节点不放在开头和结尾的情况
|
||||
@Test
|
||||
public void testPreAndFinally2() throws Exception{
|
||||
LiteflowResponse<DefaultSlot> response = flowExecutor.execute2Resp("chain2", "arg");
|
||||
Assert.assertTrue(response.isSuccess());
|
||||
Assert.assertEquals("p1==>p2==>a==>b==>c==>f1==>f2",response.getSlot().printStep());
|
||||
}
|
||||
|
||||
//测试有节点报错是否还执行finally节点的情况,其中d节点会报错,但依旧执行f1,f2节点
|
||||
@Test
|
||||
public void testPreAndFinally3() throws Exception{
|
||||
LiteflowResponse<DefaultSlot> response = flowExecutor.execute2Resp("chain3", "arg");
|
||||
Assert.assertFalse(response.isSuccess());
|
||||
Assert.assertEquals("p1==>p2==>a==>d==>f1==>f2", response.getSlot().printStep());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("a")
|
||||
public class ACmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("ACmp executed!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("b")
|
||||
public class BCmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("BCmp executed!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("c")
|
||||
public class CCmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("CCmp executed!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("d")
|
||||
public class DCmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("CCmp executed!");
|
||||
int i = 1/0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("f1")
|
||||
public class Finally1Cmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("Finally1Cmp executed!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("f2")
|
||||
public class Finally2Cmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("Finally2Cmp executed!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("p1")
|
||||
public class Pre1Cmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("Pre1Cmp executed!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* <p>Title: liteflow</p>
|
||||
* <p>Description: 轻量级的组件式流程框架</p>
|
||||
* @author Bryan.Zhang
|
||||
* @email weenyc31@163.com
|
||||
* @Date 2020/4/1
|
||||
*/
|
||||
package com.yomahub.liteflow.test.preAndFinally.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("p2")
|
||||
public class Pre2Cmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
System.out.println("Pre2Cmp executed!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
liteflow.rule-source=preAndFinally/flow.xml
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<flow>
|
||||
<chain name="chain1">
|
||||
<pre value="p1,p2"/>
|
||||
<then value="a,b,c"/>
|
||||
<finally value="f1,f2"/>
|
||||
</chain>
|
||||
|
||||
<chain name="chain2">
|
||||
<then value="a"/>
|
||||
<pre value="p1,p2"/>
|
||||
<finally value="f1,f2"/>
|
||||
<then value="b,c"/>
|
||||
</chain>
|
||||
|
||||
<chain name="chain3">
|
||||
<pre value="p1,p2"/>
|
||||
<then value="a,d,c"/>
|
||||
<finally value="f1,f2"/>
|
||||
</chain>
|
||||
</flow>
|
||||
Reference in New Issue
Block a user