任务编排框架LiteFlow

742 阅读10分钟

特别声明:仅用于学习,如对LiteFlow感兴趣,可以前往官网查看更多知识点。LiteFlow

任务编排框架LiteFlow

快速开始

此示例使用的是SpringBoot,如用的是Spring或者其他,可以前往官网上查看用法。

  1. 新建一个 maven 项目,在pom.xml中导入相关依赖

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
        <!--核心是 liteflow-spring-boot-starter -->
        <dependency>
            <groupId>com.yomahub</groupId>
            <artifactId>liteflow-spring-boot-starter</artifactId>
            <version>2.10.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
  2. 编写主启动类

    package com.gjy.liteflow;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class LiteflowApp {
        public static void main(String[] args) {
            SpringApplication.run(LiteflowApp.class, args);
        }
    }
    
  3. 编写三个普通组件,参考Acmp编写Bcmp,Ccmp。@Component的value需要修改为对应的值为b,c。日志打印也要修改。

    package com.gjy.liteflow.flow;
    
    import com.yomahub.liteflow.core.NodeComponent;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    
    @Component(value = "a")
    @Slf4j
    public class Acmp extends NodeComponent {
    
        @Override
        public void process() throws Exception {
            log.info("process a");
        }
    
    }
    
  4. 编写 application.yaml

    liteflow:
      rule-source: config/flow.el.xml
    
  5. 编写规则定义文件 flow.el.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <flow>
        <chain name="chain">
            THEN(a,b,c);
        </chain>
    </flow>
    
  6. 编写测试代码

    package com.gjy.liteflow.flow;
    
    import com.gjy.liteflow.LiteflowApp;
    import com.yomahub.liteflow.core.FlowExecutor;
    import com.yomahub.liteflow.flow.LiteflowResponse;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import javax.annotation.Resource;
    
    @SpringBootTest(classes = LiteflowApp.class)
    public class FlowTest {
    
        @Resource
        private FlowExecutor flowExecutor;
    
        @Test
        public void test1() {
            LiteflowResponse resp = flowExecutor.execute2Resp("chain");
        }
    }
    
  7. 观察运行结果,会发现a,b,c依次打印

常规组件

任务的执行顺序依赖于EL的编写,任务的执行功能依赖于组件的编写。

本节不仅介绍了组件的编写,也介绍了对应的EL规则的写法。

普通组件

普通组件节点需要继承 NodeComponent ,可用于 EL 的 THEN 和 WHEN 关键字中,必须实现抽象方法 process,下面是一个简单示例:

@Component(value = "a")
public class Acmp extends NodeComponent {
    @Override
    public void process() throws Exception {
    }
}

其中,@Component也可以换成 @LiteflowComponent,编写时最好指定value的值,以方便后续编写EL规则,value的值就是EL规则中的组件ID。

可以覆盖的方法

  1. isAccess

    推荐实现,表示是否进入该节点,可用于业务参数的预先判断。建议搭配 isEnd 来使用,因为 isAccess 的结果不会影响后面任务的执行。下面是一个示例,**注意:**使用的组件和EL规则还是快速开始中的,除了Acmp和测试用例有改动。

    通过改变测试用例中的流程入参,查看不同的效果。

    @Component(value = "a")
    @Slf4j
    public class Acmp extends NodeComponent {
    
        @Override
        public void process() throws Exception {
            log.info("process a");
        }
    
        @Override
        public boolean isAccess() { // 判断是否进入该节点,比如判断订单号是否存在,再进行操作
            String s = this.getRequestData(); // 获取流程入参
            return Lists.newArrayList("1000111001", "1000111002").contains(s);
        }
    
        @Override
        public boolean isEnd() { // 搭配 isAccess,如果该节点不执行的时候,且不需要后续节点执行了,返回值为isAccess取反
            return !isAccess();
        }
    }
    

    测试用例:

    @SpringBootTest(classes = LiteflowApp.class)
    public class FlowTest {
    
        @Resource
        private FlowExecutor flowExecutor;
    
        @Test
        public void test1() {
            LiteflowResponse resp = flowExecutor.execute2Resp(
                    "chain",
                    "1000111005" // 流程入参, 在任务中可以使用 this.getRequestData() 接受该参数,类型任意
            );
        }
    }
    
  2. isContinueOnError

    表示出错是否执行下一个节点,默认为false。根据需要选择合适的返回结果。下面是一个示例,**注意:**使用的组件和EL规则还是快速开始中的,除了Bcmp有改动。Acmp不是上面的,是快速开始里面的,这个需要注意,不然效果可能不一样。

    @Component(value = "b")
    @Slf4j
    public class Bcmp extends NodeComponent {
    
        @Override
        public void process() throws Exception {
            log.info("process b");
            throw new RuntimeException();
        }
    
        @Override
        public boolean isContinueOnError() {
            return false;
        }
    }
    

    测试用例

    @SpringBootTest(classes = LiteflowApp.class)
    public class FlowTest {
    
        @Resource
        private FlowExecutor flowExecutor;
    
        @Test
        public void test2() {
            LiteflowResponse resp = flowExecutor.execute2Resp("chain");
        }
    }
    

    在Bcmp的process中抛出了一个异常,通过改变Bcmp中isContinueOnError的返回值,查看任务执行的结果。为true时会忽略process中的错误,继续执行下面的任务节点,为false时会抛出异常,执行中止。

  3. isEnd

    在isAccess中已经介绍了一部分。为true时表示整个流程终止,但是返回的isSuccess为true,因为是用户主动结束的流程。

    注意:

    在isContinueOnError为true的情况下,给isEnd设置为true,流程依然会终止,response中的isSuccess也是true。

    在isContinueOnError为true的情况下,给isEnd设置为true,但是process抛异常了,流程不会终止,response中的isSuccess也是true。

    issues:gitee.com/dromara/lit…

  4. beforeProcess和afterProcess

    任务的前置和后置处理器,前置处理器在isAccess之后。可以在组件中重写或者继承ICmpAroundAspect 做全局处理

    @Component
    public class LfCmpAspect implements ICmpAroundAspect {
        @Override
        public void beforeProcess(NodeComponent cmp) {
            System.out.println("lf before");
        }
    
        @Override
        public void afterProcess(NodeComponent cmp) {
            System.out.println("lf after");
        }
    }
    
    public void beforeProcess() {
        log.info("a before");
    }
    
    @Override
    public void afterProcess() {
        log.info("a after");
    }
    
  5. onSuccess和onError

    组件的成功失败回调。

    @Override
    public void onSuccess() throws Exception {
    }
    
    @Override
    public void onError(Exception e) throws Exception {
    }
    

    (1)onError执行后,afterProcess依然会执行

  6. rollback

    v2.11.0开始支持,本文章采用的是2.10.6,没有此项功能,建议去官网查看。

this关键字可以调用的方法

  1. this.getContextBean(clazz):获取创建流程时定义的上下文,clazz为类型。
@Test
public void test1() {
    // 第三个参数为上下文对象,可以为对象或者为类名,支持多个。
    // 需要注意的是,要么都穿bean,要么都穿class
    LiteflowResponse resp = flowExecutor.execute2Resp(
        "chain",
        "1000111005",
        new AcmpDomain("1", LocalDateTime.now())
    );
}
// 任务中获取的方式:AcmpDomain bean = this.getContextBean(AcmpDomain.class);
  1. getNodeId():获取组件ID

  2. getName:获取组件别名

  3. getChainName:获取当前流程名

  4. getRequestData:获取流程参数

    可以参考上文的可以覆盖的方法-isAccess

  5. setIsEnd:是否立即终止路程

  6. getTag:获取组件的tag标签

  7. invoke2Resp:隐式调用子流程

选择组件

选择组件节点需要继承 NodeSwitchComponent,可用于 EL 的 SWITCH 关键字中,必须实现抽象方法 processSwitch。

相信经过上面的联系,已经掌握了普通组件的编写,后面就不一一列出普通组件的写法了。

根据nodeId选择

a,b是一个普通组件,下面是一个简单示例:

@Component(value = "d")
public class Dcmp extends NodeSwitchComponent {
    @Override
    public String processSwitch() throws Exception {
        int i = ThreadLocalRandom.current().nextInt(3);
        if (i == 0) {
            return "a";
        } else {
            return "b";
        }
    }
}

EL规则

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="chain1">
        SWITCH(d).TO(a,b);
    </chain>
</flow>

测试代码

@Test
public void test3() {
    LiteflowResponse resp = flowExecutor.execute2Resp("chain1");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

根据表达式的ID选择

a,b,c是一个普通组件,可以通过修改EL规则中的 id和else中的返回值查看相同的效果,此两值必须一致,下面是一个简单示例:

@Component(value = "d")
@Slf4j
public class Dcmp extends NodeSwitchComponent {

    @Override
    public String processSwitch() throws Exception {
        int i = ThreadLocalRandom.current().nextInt(3);
        log.info("process d {}", i);
        if (i == 0) {
            return "a";
        } else {
            return "ww";
        }
    }
}

EL规则

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="chain3">
        SWITCH(d).TO(a,WHEN(b,c).id("ww"));
    </chain>
</flow>

测试代码

@Test
public void test4() {
    LiteflowResponse resp = flowExecutor.execute2Resp("chain3");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

表达式tag选择

a,c是一个普通组件,下面是一个简单示例:

@Component(value = "d")
@Slf4j
public class Dcmp extends NodeSwitchComponent {

    @Override
    public String processSwitch() throws Exception {
        int i = ThreadLocalRandom.current().nextInt(3);
        log.info("process d {}", i);
        if (i == 0) {
            return "tag:cat";
        } else {
            return "tag:dog";
        }
    }
}

EL规则

<?xml version="1.0" encoding="UTF-8"?>
<flow>
   <chain name="chain4">
        SWITCH(d).TO(
        a.tag("cat"),
        c.tag("dog")
        );
    </chain>
</flow>

测试代码

@Test
public void test5() {
    LiteflowResponse resp = flowExecutor.execute2Resp("chain4");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

链路tag选择

此项就不给测试用例了,可以参考上文的 根据nodeId选择 改写代码并测试。

<chain name="chain5">
    SWITCH(d).to(a, sub.tag("w1"));
</chain>

<chain name="sub">
    THEN(c,d);
</chain>

条件组件

条件组件节点需要继承 NodeIfComponent,可用于 EL 的 IF ELIF ELSE 关键字中,必须实现抽象方法 processIf。

三元表达式

下面是一个简单示例:

@Component(value = "h")
@Slf4j
public class HCmp extends NodeIfComponent {
    @Override
    public boolean processIf() throws Exception {
        int i = ThreadLocalRandom.current().nextInt(5);
        log.info("process h {}", i);
        return i >= 3;
    }
}

EL规则

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="chain6">
        IF(h,a,b);
    </chain>
</flow>

测试代码

@Test
public void test6() {
    LiteflowResponse resp = flowExecutor.execute2Resp("chain6");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

如果条件组件返回false,执行后一个任务,返回true,执行前一个任务。

二元和ELSE

和三元表达式相比没有明显的优势,这里简单介绍下EL规则文件的编写,感兴趣可以自己做测试用例。

二元表达式

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="c8">
        THEN(IF(h,a),b);
    </chain>
</flow>

ELSE

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="c7">
        IF(h,a).ELIF(b)
    </chain>
</flow>

ELIF

ELIF和三元表达式一样重要,是条件组件中必须掌握的。

下面看一下测试用例,可以根据组件名自己编写条件组件和普通组件,这里不一一列出。

EL规则

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="i0">
        IF(h,a).ELIF(i,b).ELIF(j,c).ELSE(d)
    </chain>
</flow>

测试代码

@Test
public void test6() {
    LiteflowResponse resp = flowExecutor.execute2Resp("chain6");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

次数循环组件

次数组件节点需要继承 NodeForComponent,可用于 EL 的 FOR DO 关键字中,必须实现抽象方法 processFor。

下面是一个简单的测试。

@Component(value = "k")
@Slf4j
public class KCmp extends NodeForComponent {
    @Override
    public int processFor() throws Exception {
        log.info("process k");
        return 3;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="f1">
        FOR(k).DO(a);
    </chain>
</flow>
@Test
public void test8() {
    LiteflowResponse resp = flowExecutor.execute2Resp("f1");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

可以在组件中 this.getLoopIndex() 获取下标。**注意:**次数循环只会执行一次自身,后续都是DO中的组件。

条件循环组件

条件组件节点需要继承 NodeWhileComponent,可用于 EL 的 WHILE DO 关键字中,必须实现抽象方法 processWhile。

下面是一个简单的测试。

@LiteflowComponent(value = "l")
@Slf4j
public class LCmp extends NodeWhileComponent {
    @Override
    public boolean processWhile() throws Exception {
        int i = ThreadLocalRandom.current().nextInt(3);
        log.info("process l {}", i);
        return i < 2;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="w1">
        WHILE(l).DO(a);
    </chain>
</flow>
@Test
public void test9() {
    LiteflowResponse resp = flowExecutor.execute2Resp("w1");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

可以在组件中 this.getLoopIndex() 获取下标。**注意:**条件循环每次执行时都会执行一下自身,这点和次数循环组件不同。

迭代循环组件

迭代循环组件节点需要继承 NodeIteratorComponent,可用于 EL 的 ITERATOR DO 关键字中,必须实现抽象方法 processIterator。

下面是一个简单的测试。

@LiteflowComponent(value = "m")
@Slf4j
public class MCmp extends NodeIteratorComponent {
    @Override
    public Iterator<?> processIterator() throws Exception {
        Iterator<String> iterator = Lists.newArrayList("hello", "World").iterator();
        log.info("process m {}", iterator);
        return iterator;
    }
}
@Component(value = "g")
@Slf4j
public class Gcmp extends NodeComponent {
    @Override
    public void process() throws Exception {
        String currLoopObj = this.getCurrLoopObj();
        log.info("process g {}", currLoopObj);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="m1">
        ITERATOR(m).DO(g);
    </chain>
</flow>
@Test
public void test10() {
    LiteflowResponse resp = flowExecutor.execute2Resp("m1");
    System.out.println("resp.isSuccess() = " + resp.isSuccess());
}

可以在组件中 this.getCurrLoopObj() 获取下标。

退出循环组件

迭代循环组件节点需要继承 NodeBreakComponent,可用于 EL 的 BREAK 关键字中,必须实现抽象方法 processBreak。

下面是一个简单的测试。

@Component(value = "n")
@Slf4j
public class NCmp extends NodeBreakComponent {
    @Override
    public boolean processBreak() throws Exception {
        // 获取 迭代循环组件的当前值
        Object o = this.getCurrLoopObj();
        // 获取 次数循环组件 和 条件循环组件 的当前下标值
        Integer loopIndex = this.getLoopIndex();
        return true;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="m1">
        ITERATOR(m).DO(g).BREAK(n);
    </chain>
    <chain name="f1">
        FOR(k).DO(a).BREAK(n);
    </chain>
    <chain name="w1">
        WHILE(l).DO(a).BREAK(n);
    </chain>
</flow>

可以通过代码示例中的获取当前值当前下标判断是否终止循环。

EL规则的写法

常规组件中已经介绍了大量的组件编写,本章中不在编写新的组件了,只给出EL规则文件和流程图,如果确实有需要的话,可以留言,后面我补充一下。

串行编排

串行编排1.png

el1 和 el2 是等价的。

<?xml version="1.0" encoding="utf-8" ?>
<flow>
    <chain name="el1">
        THEN(a,b,c,d);
    </chain>
    <chain name="el2">
        THEN(a,b,THEN(c,d));
    </chain>
</flow>

并行编排

直接并行

这种情况下,执行顺序无法确认,任务执行顺序每次都可能不一样,但确定的是,每个任务节点都会执行。

并行编排1.png

<?xml version="1.0" encoding="utf-8" ?>
<flow>
    <chain name="mm2">
        WHEN(a,b,c);
    </chain>
</flow>

和串行嵌套

默认情况下,b,c,d都执行完后才会执行e。

并行编排2.png

<?xml version="1.0" encoding="utf-8" ?>
<flow>
    <chain name="el3">
        THEN(a,WHEN(b,c,d),e);
    </chain>
</flow>

如果想要任一节点执行完则忽略其他节点,可以参考下面的EL,在WHEN后面加上any设置为true。

<?xml version="1.0" encoding="utf-8" ?>
<flow>
    <chain name="el4">
        THEN(a,WHEN(b,c,d).any(true),e);
    </chain>
</flow>

如果想要忽略错误,可以参考下面的EL,在WHEN后面加上ignoreError设置为true。b,c,d任一节点发生错误,e最终依然会执行。

<?xml version="1.0" encoding="utf-8" ?>
<flow>
    <chain name="el5">
        THEN(a,WHEN(b,c,d).ignoreError(true),e);
    </chain>
</flow>

最后再看一个复杂的例子,由于确实挺复杂的,所以采用了子流程的方式,子流程也是一种EL规则的编写方式,这里不单独列出来介绍了:

<?xml version="1.0" encoding="utf-8" ?>
<flow>
    <chain name="sub1">
        THEN(b,c);
    </chain>
    <chain name="sub2">
        THEN(d,e);
    </chain>
    <chain name="sub4">
        WHEN(g,h,i);
    </chain>
    <chain name="el6">
        THEN(a,WHEN(sub1,sub2),f,sub4,j);
    </chain>
</flow>

并行编排3.png

选择编排

选择编排1.png

<chain name="el7">
    THEN(a,WHEN(b,SWITCH(d).TO(e,g).DEFAULT(c)),f);
</chain>

新的关键字 DEFAULT,配合SWITCH TO使用,表示结果非e,g到c。

条件编排

条件编排.png

<?xml version="1.0" encoding="UTF-8"?>
<flow>
    <chain name="el8">
        IF(h,a).ELIF(i,b).ELIF(j,c).ELSE(d)
    </chain>
</flow>

常规组件-条件组件中介绍的非常详细了。

循环编排

  1. FOR

    <chain name="f1">
        FOR(k).DO(a).BREAK(n);
    </chain>
    
  2. WHILE

    <chain name="w1">
        WHILE(l).DO(a).BREAK(n);
    </chain>
    
  3. ITERATOR

    <chain name="m1">
        ITERATOR(m).DO(g).BREAK(n);
    </chain>
    
  4. BREAK

    上面三个都带有BREAK,BREAK关键字是在每次循环的末尾进行判断的。

  5. 异步循环

    v2.11.0开始支持,高于本文章LiteFlow的版本,感兴趣可以到官网上看一下

其他编排

  1. 捕获异常

    <chain name="chain1">
        CATCH(
        THEN(a,b)
        ).DO(c)
    </chain>
    
  2. 与或非表达式

    在IF,WHILE中均可以使用。

    <chain name="chain1">
        IF(AND(x,y), a, b);
    </chain>
    <chain name="chain2">
        IF(OR(x,y), a, b);
    </chain>
    <chain name="chain3">
        IF(NOT(x), a, b);
    </chain>