主頁 >  其他 > 輕量級作業流引擎的設計與實作

輕量級作業流引擎的設計與實作

2022-09-27 08:42:49 其他

一、什么是作業流引擎

作業流引擎是驅動作業流執行的一套代碼,

至于什么是作業流、為什么要有作業流、作業流的應用景,同學們可以看一看網上的資料,在此處不在展開,

 

二、為什么要重復造輪子

開源的作業流引擎很多,比如 activiti、flowable、Camunda 等,那么,為什么沒有選它們呢?基于以下幾點考慮:

  • 最重要的,滿足不了業務需求,一些特殊的場景無法實作,
  • 有些需求實作起來比較繞,更有甚者,需要直接修改引擎資料庫,這對于引擎的穩定運行帶來了巨大的隱患,也對以后引擎的版本升級制造了一些困難,
  • 資料、代碼量、API繁多,學習成本較高,維護性較差,
  • 經過分析與評估,我們的業務場景需要的BPMN元素較少,開發實作的代價不大,

因此,重復造了輪子,其實,還有一個更深層次的戰略上的考慮,即:作為科技公司,我們一定要有我們自己的核心底層技術!這樣,才能不受制于人(參考最近的芯片問題),

 

三、怎么造的輪子

對于一次學習型分享來講,程序比結果更重要,那些只說結果,不細說程序甚至不說的分享,我認為是秀肌肉,而不是真正意義上的分享,因此,接下來,本文將重點描述造輪子的主要程序,

一個成熟的作業流引擎的構建是很復雜的,如何應對這種復雜性呢?一般來講,有以下三種方法:

  • 確定性交付:弄清楚需求是什么,驗收標準是什么,最好能夠寫出測驗用例,這一步是為了明確目標,
  • 迭代式開發:先從小的問題集的解決開始,逐步過渡到解決大的問題集上來,羅馬不是一天建成的,人也不是一天就能成熟的,是需要個程序的,
  • 分而治之:把大的問題拆成小的問題,小問題的解決會推動大問題的解決(這個思想適用場景比較多,同學們可以用心體會和理解哈),

如果按照上述方法,一步一步的詳細展開,那么可能需要一本書,為了縮減篇幅而又不失干貨,本文會描述重點幾個迭代,進而闡述輕量級作業流引擎的設計與主要實作,

那么,輕量級又是指什么呢?這里,主要是指以下幾點

  • 少依賴:代碼的java實作上,除了jdk8以外,不依賴與其他第三方jar包,從而可以更好的減少依賴帶來的問題,
  • 內核化:設計上,采用了微內核架構模式,內核小巧,實用,同時提供了一定的擴展性,從而可以更好地理解與應用本引擎,
  • 輕規范:并沒有完全實作BPMN規范,也沒有完全按照BPMN規范進行設計,而只是參考了該規范,且只實作以一小部分必須實作的元素,從而降低了學習成本,可以按照需求自由發揮,
  • 工具化:代碼上,只是一個工具(UTIL),不是一個應用程式,從而你可以簡單的運行它,擴展你自己的資料層、節點層,更加方便的集成到其他應用中去,

好,廢話說完了,開始第一個迭代......

 

四、Hello ProcessEngine

按照國際慣例,第一個迭代用來實作 hello world ,

1、需求

作為一個流程管理員,我希望流程引擎可以運行如下圖所示的流程,以便我能夠配置流程來列印不同的字串,

 

2、分析

  • 第一個流程,可以列印Hello ProcessEngine,第二個流程可以列印ProcessEngine Hello,這兩個流程的區別是只有順序不同,藍色的節點與紅色的節點的本身功能沒有發生變化
  • 藍色的節點與紅色的節點都是節點,它們的功能是不一樣的,即:紅色的節點列印Hello,藍色的節點列印ProcessEngine
  • 開始與結束節點是兩個特殊的節點,一個開始流程,一個結束流程
  • 節點與節點之間是通過線來連接的,一個節點執行完畢后,是通過箭頭來確定下一個要執行的節點
  • 需要一種表示流程的方式,或是XML、或是JSON、或是其他,而不是圖片

3、設計

(1)流程的表示

相較于JSON,XML的語意更豐富,可以表達更多的資訊,因此這里使用XML來對流程進行表示,如下所示

<definitions>
    <process id="process_1" name="hello">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="printHello_1" />
        <printHello id="printHello_1" name="hello">
            <incoming>flow_1</incoming>
            <outgoing>flow_2</outgoing>
        </printHello>
        <sequenceFlow id="flow_2" sourceRef="printHello_1" targetRef="printProcessEngine_1" />
        <printProcessEngine id="printProcessEngine_1" name="processEngine">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </printProcessEngine>
        <sequenceFlow id="flow_3" sourceRef="printProcessEngine_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_3</incoming>
        </endEvent>
    </process>
</definitions>

 

  • process表示一個流程
  • startEvent表示開始節點,endEvent表示結束節點
  • printHello表示列印hello節點,就是需求中的藍色節點
  • processEngine表示列印processEngine節點,就是需求中的紅色節點
  • sequenceFlow表示連線,從sourceRef開始,指向targetRef,例如:flow_3,表示一條從printProcessEngine_1到endEvent_1的連線,

(2)節點的表示

  • outgoing表示出邊,即節點執行完畢后,應該從那個邊出去,
  • incoming表示入邊,即從哪個邊進入到本節點,
  • 一個節點只有outgoing而沒有incoming,如:startEvent,也可以 只有入邊而沒有出邊,如:endEvent,也可以既有入邊也有出邊,如:printHello、processEngine,

(3)流程引擎的邏輯

基于上述XML,流程引擎的運行邏輯如下

  1. 找到開始節點(startEvent)
  2. 找到startEvent的outgoing邊(sequenceFlow)
  3. 找到該邊(sequenceFlow)指向的節點(targetRef)
  4. 執行節點自身的邏輯
  5. 找到該節點的outgoing邊(sequenceFlow)
  6. 重復3-5,直到遇到結束節點(endEvent),流程結束

4、實作

首先要進行資料結構的設計,即:要把問題域中的資訊映射到計算機中的資料,

可以看到,一個流程(PeProcess)由多個節點(PeNode)與邊(PeEdge)組成,節點有出邊(out)、入邊(in),邊有流入節點(from)、流出節點(to),

具體的定義如下:

public class PeProcess {
    public String id;
    public PeNode start;

    public PeProcess(String id, PeNode start) {
        this.id = id;
        this.start = start;
    }
}

public class PeEdge {
    private String id;
    public PeNode from;
    public PeNode to;

    public PeEdge(String id) {
        this.id = id;
    }
}

public class PeNode {
    private String id;

    public String type;
    public PeEdge in;
    public PeEdge out;

    public PeNode(String id) {
        this.id=id;
    }
}

PS : 為了表述主要思想,在代碼上比較“奔放自由”,生產中不可直接復制粘貼!

接下來,構建流程圖,代碼如下:

public class XmlPeProcessBuilder {
    private String xmlStr;
    private final Map<String, PeNode> id2PeNode = new HashMap<>();
    private final Map<String, PeEdge> id2PeEdge = new HashMap<>();

    public XmlPeProcessBuilder(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public PeProcess build() throws Exception {
        //strToNode : 把一段xml轉換為org.w3c.dom.Node
        Node definations = XmlUtil.strToNode(xmlStr);
        //childByName : 找到definations子節點中nodeName為process的那個Node
        Node process = XmlUtil.childByName(definations, "process");
        NodeList childNodes = process.getChildNodes();

        for (int j = 0; j < childNodes.getLength(); j++) {
            Node node = childNodes.item(j);
            //#text node should be skip
            if (node.getNodeType() == Node.TEXT_NODE) continue;

            if ("sequenceFlow".equals(node.getNodeName()))
                buildPeEdge(node);
            else
                buildPeNode(node);
        }
        Map.Entry<String, PeNode> startEventEntry = id2PeNode.entrySet().stream().filter(entry -> "startEvent".equals(entry.getValue().type)).findFirst().get();
        return new PeProcess(startEventEntry.getKey(), startEventEntry.getValue());
    }

    private void buildPeEdge(Node node) {
        //attributeValue : 找到node節點上屬性為id的值
        PeEdge peEdge = id2PeEdge.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeEdge(id));
        peEdge.from = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "sourceRef"), id -> new PeNode(id));
        peEdge.to = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "targetRef"), id -> new PeNode(id));
    }

    private void buildPeNode(Node node) {
        PeNode peNode = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeNode(id));
        peNode.type = node.getNodeName();

        Node inPeEdgeNode = XmlUtil.childByName(node, "incoming");
        if (inPeEdgeNode != null)
            //text : 得到inPeEdgeNode的nodeValue
            peNode.in = id2PeEdge.computeIfAbsent(XmlUtil.text(inPeEdgeNode), id -> new PeEdge(id));

        Node outPeEdgeNode = XmlUtil.childByName(node, "outgoing");
        if (outPeEdgeNode != null)
            peNode.out = id2PeEdge.computeIfAbsent(XmlUtil.text(outPeEdgeNode), id -> new PeEdge(id));
    }
}

 

接下來,實作流程引擎主邏輯,代碼如下:

public class ProcessEngine {
    private String xmlStr;

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public void run() throws Exception {
        PeProcess peProcess = new XmlPeProcessBuilder(xmlStr).build();

        PeNode node = peProcess.start;
        while (!node.type.equals("endEvent")) {
            if ("printHello".equals(node.type))
                System.out.print("Hello ");
            if ("printProcessEngine".equals(node.type))
                System.out.print("ProcessEngine ");

            node = node.out.to;
        }
    }
}

就這?作業流引擎就這?同學們可千萬不要這樣簡單理解啊,畢竟這還只是hello world而已,各種代碼量就已經不少了,

另外,這里面還有很多可以改進的空間,比如例外控制、泛化、設計模式等,但畢竟只是一個hello world而已,其目的是方便同學理解,讓同學入門,

那么,接下來呢,就要稍微貼近一些具體的實際應用場景了,我們繼續第二個迭代,

五、簡單審批

一般來講作業流引擎屬于底層技術,在它之上可以構建審批流、業務流、資料流等型別的應用,那么接下啦就以實際中的簡單審批場景為例,繼續深入作業流引擎的設計,好,我們開始,

1、需求

作為一個流程管理員,我希望流程引擎可以運行如下圖所示的流程,以便我能夠配置流程來實作簡單的審批流,

 

例如:小張提交了一個申請單,然后經過經理審批,審批結束后,不管通過還是不通過,都會經過第三步把結果發送給小張,

2、分析

  • 總體上來講,這個流程還是線性順序類的,基本上可以沿用上次迭代的部分設計
  • 審批節點的耗時可能會比較長,甚至會達到幾天時間,作業流引擎主動式的調取下一個節點的邏輯并不適合此場景
  • 隨著節點型別的增多,作業流引擎里寫死的那部分節點型別自由邏輯也不合適
  • 審批時需要申請單資訊、審批人,結果郵件通知還需要審批結果等資訊,這些資訊如何傳遞也是一個要考慮的問題

3、設計

  • 采用注冊機制,把節點型別及其自有邏輯注冊進作業流引擎,以便能夠擴展更多節點,使得作業流引擎與節點解耦
  • 作業流引擎增加被動式驅動邏輯,使得能夠通過外部來使作業流引擎執行下一個節點
  • 增加背景關系語意,作為全域變數來使用,使得資料能夠流經各個節點

4、實作

新的XML定義如下:

<definitions>
    <process id="process_2" name="簡單審批例子">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="approvalApply_1" />
        <approvalApply id="approvalApply_1" name="提交申請單">
            <incoming>flow_1</incoming>
            <outgoing>flow_2</outgoing>
        </approvalApply>
        <sequenceFlow id="flow_2" sourceRef="approvalApply_1" targetRef="approval_1" />
        <approval id="approval_1" name="審批">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </approval>
        <sequenceFlow id="flow_3" sourceRef="approval_1" targetRef="notify_1"/>
        <notify id="notify_1" name="結果郵件通知">
            <incoming>flow_3</incoming>
            <outgoing>flow_4</outgoing>
        </notify>
        <sequenceFlow id="flow_4" sourceRef="notify_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_4</incoming>
        </endEvent>
    </process>
</definitions>

首先要有一個背景關系物件類,用于傳遞變數的,定義如下:

public class PeContext {
    private Map<String, Object> info = new ConcurrentHashMap<>();

    public Object getValue(String key) {
        return info.get(key);
    }

    public void putValue(String key, Object value) {
        info.put(key, value);
    }
}

每個節點的處理邏輯是不一樣的,此處應該進行一定的抽象,為了強調流程中節點的作用是邏輯處理,引入了一種新的型別--算子(Operator),定義如下:

public interface IOperator {
    //引擎可以據此來找到本算子
    String getType();

    //引擎調度本算子
    void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext);
}

對于引擎來講,當遇到一個節點時,需要調度之,但怎么調度呢?首先需要各個節點算子注冊(registNodeProcessor())進來,這樣才能找到要調度的那個算子,

其次,引擎怎么知道節點算子自有邏輯處理完了呢?一般來講,引擎是不知道的,只能是由算子告訴引擎,所以引擎要提供一個功能(nodeFinished()),這個功能由算子呼叫,

最后,把算子任務的調度和引擎的驅動解耦開來,放入不同的執行緒中,

修改后的ProcessEngine代碼如下:

public class ProcessEngine {
    private String xmlStr;

    //存盤算子
    private Map<String, IOperator> type2Operator = new ConcurrentHashMap<>();
    private PeProcess peProcess = null;
    private PeContext peContext = null;

    //任務資料暫存
    public final BlockingQueue<PeNode> arrayBlockingQueue = new LinkedBlockingQueue();
    //任務調度執行緒
    public final Thread dispatchThread = new Thread(() -> {
        while (true) {
            try {
                PeNode node = arrayBlockingQueue.take();
                type2Operator.get(node.type).doTask(this, node, peContext);
            } catch (Exception e) {
            }
        }
    });

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    //算子注冊到引擎中,便于引擎呼叫之
    public void registNodeProcessor(IOperator operator) {
        type2Operator.put(operator.getType(), operator);
    }

    public void start() throws Exception {
        peProcess = new XmlPeProcessBuilder(xmlStr).build();
        peContext = new PeContext();

        dispatchThread.setDaemon(true);
        dispatchThread.start();

        executeNode(peProcess.start.out.to);
    }

    private void executeNode(PeNode node) {
        if (!node.type.equals("endEvent"))
            arrayBlockingQueue.add(node);
        else
            System.out.println("process finished!");
    }

    public void nodeFinished(String peNodeID) {
        PeNode node = peProcess.peNodeWithID(peNodeID);
        executeNode(node.out.to);
    }
}

接下來,簡單(簡陋)實作本示例所需的三個算子,代碼如下:

/**
 * 提交申請單
 */
public class OperatorOfApprovalApply implements IOperator {
    @Override
    public String getType() {
        return "approvalApply";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("form", "formInfo");
        peContext.putValue("applicant", "小張");

        processEngine.nodeFinished(node.id);
    }
}

/**
 * 審批
 */
public class OperatorOfApproval implements IOperator {
    @Override
    public String getType() {
        return "approval";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("approver", "經理");
        peContext.putValue("message", "審批通過");

        processEngine.nodeFinished(node.id);
    }
}

/**
 * 結果郵件通知
 */
public class OperatorOfNotify implements IOperator {
    @Override
    public String getType() {
        return "notify";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {

        System.out.println(String.format("%s 提交的申請單 %s 被 %s 審批,結果為 %s",
                peContext.getValue("applicant"),
                peContext.getValue("form"),
                peContext.getValue("approver"),
                peContext.getValue("message")));

        processEngine.nodeFinished(node.id);
    }
}

運行一下,看看結果如何,代碼如下:

public class ProcessEngineTest {

    @Test
    public void testRun() throws Exception {
        //讀取檔案內容到字串
        String modelStr = Tools.readResoucesFile("model/two/hello.xml");
        ProcessEngine processEngine = new ProcessEngine(modelStr);

        processEngine.registNodeProcessor(new OperatorOfApproval());
        processEngine.registNodeProcessor(new OperatorOfApprovalApply());
        processEngine.registNodeProcessor(new OperatorOfNotify());

        processEngine.start();

        Thread.sleep(1000 * 1);

    }

}

 

小張 提交的申請單 formInfo 被 經理 審批,結果為 審批通過
process finished!

 

到此,輕量級作業流引擎的核心邏輯介紹的差不多了,然而,只支持順序結構是太單薄的,我們知道,程式流程的三種基本結構為順序、分支、回圈,有了這三種結構,基本上就可以表示絕大多數流程邏輯,回圈可以看做一種組合結構,即:回圈可以由順序與分支推匯出來,我們已經實作了順序,那么接下來只要實作分支即可,而分支有很多型別,如:二選一、N選一、N選M(1<=M<=N),其中N選一可以由二選一的組合推匯出來,N選M也可以由二選一的組合推匯出來,只是比較啰嗦,不那么直觀,所以,我們只要實作二選一分支,即可滿足絕大多數流程邏輯場景,好,第三個迭代開始,

 

六、一般審批

作為一個流程管理員,我希望流程引擎可以運行如下圖所示的流程,以便我能夠配置流程來實作一般的審批流,

 

例如:小張提交了一個申請單,然后經過經理審批,審批結束后,如果通過,發郵件通知,不通過,則打回重寫填寫申請單,直到通過為止,

1、分析

  • 需要引入一種分支節點,可以進行簡單的二選一流轉
  • 節點的入邊、出邊不只一條
  • 需要一種邏輯運算式語意,可以配置分支節點

2、設計

  • 節點要支持多入邊、多出邊
  • 節點算子來決定從哪個出邊出
  • 使用一種簡單的規則引擎,支持簡單的邏輯運算式的決議
  • 簡單分支節點的XML定義

3、實作

新的XML定義如下:

<definitions>
    <process id="process_2" name="簡單審批例子">
        <startEvent id="startEvent_1">
            <outgoing>flow_1</outgoing>
        </startEvent>
        <sequenceFlow id="flow_1" sourceRef="startEvent_1" targetRef="approvalApply_1"/>
        <approvalApply id="approvalApply_1" name="提交申請單">
            <incoming>flow_1</incoming>
            <incoming>flow_5</incoming>
            <outgoing>flow_2</outgoing>
        </approvalApply>
        <sequenceFlow id="flow_2" sourceRef="approvalApply_1" targetRef="approval_1"/>
        <approval id="approval_1" name="審批">
            <incoming>flow_2</incoming>
            <outgoing>flow_3</outgoing>
        </approval>
        <sequenceFlow id="flow_3" sourceRef="approval_1" targetRef="simpleGateway_1"/>
        <simpleGateway id="simpleGateway_1" name="簡單是非判斷">
            <trueOutGoing>flow_4</trueOutGoing>
            <expr>approvalResult</expr>
            <incoming>flow_3</incoming>
            <outgoing>flow_4</outgoing>
            <outgoing>flow_5</outgoing>
        </simpleGateway>
        <sequenceFlow id="flow_5" sourceRef="simpleGateway_1" targetRef="approvalApply_1"/>
        <sequenceFlow id="flow_4" sourceRef="simpleGateway_1" targetRef="notify_1"/>
        <notify id="notify_1" name="結果郵件通知">
            <incoming>flow_4</incoming>
            <outgoing>flow_6</outgoing>
        </notify>
        <sequenceFlow id="flow_6" sourceRef="notify_1" targetRef="endEvent_1"/>
        <endEvent id="endEvent_1">
            <incoming>flow_6</incoming>
        </endEvent>
    </process>
</definitions>

 

其中,加入了simpleGateway這個簡單分支節點,用于表示簡單的二選一分支,當expr中的運算式為真時,走trueOutGoing中的出邊,否則走另一個出邊,

節點支持多入邊、多出邊,修改后的PeNode如下:

public class PeNode {
    public String id;

    public String type;
    public List<PeEdge> in = new ArrayList<>();
    public List<PeEdge> out = new ArrayList<>();
    public Node xmlNode;

    public PeNode(String id) {
        this.id = id;
    }

    public PeEdge onlyOneOut() {
        return out.get(0);
    }

    public PeEdge outWithID(String nextPeEdgeID) {
        return out.stream().filter(e -> e.id.equals(nextPeEdgeID)).findFirst().get();
    }

    public PeEdge outWithOutID(String nextPeEdgeID) {
        return out.stream().filter(e -> !e.id.equals(nextPeEdgeID)).findFirst().get();
    }

}

以前只有一個出邊時,是由當前節點來決定下一節點的,現在多出邊了,該由邊來決定下一個節點是什么,修改后的流程引擎代碼如下:

public class ProcessEngine {
    private String xmlStr;

    //存盤算子
    private Map<String, IOperator> type2Operator = new ConcurrentHashMap<>();
    private PeProcess peProcess = null;
    private PeContext peContext = null;

    //任務資料暫存
    public final BlockingQueue<PeNode> arrayBlockingQueue = new LinkedBlockingQueue();
    //任務調度執行緒
    public final Thread dispatchThread = new Thread(() -> {
        while (true) {
            try {
                PeNode node = arrayBlockingQueue.take();
                type2Operator.get(node.type).doTask(this, node, peContext);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

    public ProcessEngine(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    //算子注冊到引擎中,便于引擎呼叫之
    public void registNodeProcessor(IOperator operator) {
        type2Operator.put(operator.getType(), operator);
    }

    public void start() throws Exception {
        peProcess = new XmlPeProcessBuilder(xmlStr).build();
        peContext = new PeContext();

        dispatchThread.setDaemon(true);
        dispatchThread.start();

        executeNode(peProcess.start.onlyOneOut().to);
    }

    private void executeNode(PeNode node) {
        if (!node.type.equals("endEvent"))
            arrayBlockingQueue.add(node);
        else
            System.out.println("process finished!");
    }

    public void nodeFinished(PeEdge nextPeEdgeID) {
        executeNode(nextPeEdgeID.to);
    }
}

新加入的simpleGateway節點算子如下:

/**
 * 簡單是非判斷
 */
public class OperatorOfSimpleGateway implements IOperator {
    @Override
    public String getType() {
        return "simpleGateway";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("js");
        engine.put("approvalResult", peContext.getValue("approvalResult"));

        String expression = XmlUtil.childTextByName(node.xmlNode, "expr");
        String trueOutGoingEdgeID = XmlUtil.childTextByName(node.xmlNode, "trueOutGoing");

        PeEdge outPeEdge = null;
        try {
            outPeEdge = (Boolean) engine.eval(expression) ?
                    node.outWithID(trueOutGoingEdgeID) : node.outWithOutID(trueOutGoingEdgeID);
        } catch (ScriptException e) {
            e.printStackTrace();
        }

        processEngine.nodeFinished(outPeEdge);
    }
}

其中簡單使用了js腳本作為運算式,當然其中的弊端這里就不展開了,

為了方便同學們CC+CV,其他發生相應變化的代碼如下:

/**
 * 審批
 */
public class OperatorOfApproval implements IOperator {
    @Override
    public String getType() {
        return "approval";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        peContext.putValue("approver", "經理");

        Integer price = (Integer) peContext.getValue("price");
        //價格<=200審批才通過,即:approvalResult=true
        boolean approvalResult = price <= 200;
        peContext.putValue("approvalResult", approvalResult);

        System.out.println("approvalResult :" + approvalResult + ",price : " + price);

        processEngine.nodeFinished(node.onlyOneOut());
    }
}

/**
 * 提交申請單
 */
public class OperatorOfApprovalApply implements IOperator {

    public static int price = 500;

    @Override
    public String getType() {
        return "approvalApply";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {
        //price每次減100
        peContext.putValue("price", price -= 100);
        peContext.putValue("applicant", "小張");

        processEngine.nodeFinished(node.onlyOneOut());
    }
}


/**
 * 結果郵件通知
 */
public class OperatorOfNotify implements IOperator {
    @Override
    public String getType() {
        return "notify";
    }

    @Override
    public void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {

        System.out.println(String.format("%s 提交的申請單 %s 被 %s 審批,結果為 %s",
                peContext.getValue("applicant"),
                peContext.getValue("price"),
                peContext.getValue("approver"),
                peContext.getValue("approvalResult")));

        processEngine.nodeFinished(node.onlyOneOut());
    }
}


public class XmlPeProcessBuilder {
    private String xmlStr;
    private final Map<String, PeNode> id2PeNode = new HashMap<>();
    private final Map<String, PeEdge> id2PeEdge = new HashMap<>();

    public XmlPeProcessBuilder(String xmlStr) {
        this.xmlStr = xmlStr;
    }

    public PeProcess build() throws Exception {
        //strToNode : 把一段xml轉換為org.w3c.dom.Node
        Node definations = XmlUtil.strToNode(xmlStr);
        //childByName : 找到definations子節點中nodeName為process的那個Node
        Node process = XmlUtil.childByName(definations, "process");
        NodeList childNodes = process.getChildNodes();

        for (int j = 0; j < childNodes.getLength(); j++) {
            Node node = childNodes.item(j);
            //#text node should be skip
            if (node.getNodeType() == Node.TEXT_NODE) continue;

            if ("sequenceFlow".equals(node.getNodeName()))
                buildPeEdge(node);
            else
                buildPeNode(node);
        }
        Map.Entry<String, PeNode> startEventEntry = id2PeNode.entrySet().stream().filter(entry -> "startEvent".equals(entry.getValue().type)).findFirst().get();
        return new PeProcess(startEventEntry.getKey(), startEventEntry.getValue());
    }

    private void buildPeEdge(Node node) {
        //attributeValue : 找到node節點上屬性為id的值
        PeEdge peEdge = id2PeEdge.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeEdge(id));
        peEdge.from = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "sourceRef"), id -> new PeNode(id));
        peEdge.to = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "targetRef"), id -> new PeNode(id));
    }

    private void buildPeNode(Node node) {
        PeNode peNode = id2PeNode.computeIfAbsent(XmlUtil.attributeValue(node, "id"), id -> new PeNode(id));
        peNode.type = node.getNodeName();
        peNode.xmlNode = node;

        List<Node> inPeEdgeNodes = XmlUtil.childsByName(node, "incoming");
        inPeEdgeNodes.stream().forEach(n -> peNode.in.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));

        List<Node> outPeEdgeNodes = XmlUtil.childsByName(node, "outgoing");
        outPeEdgeNodes.stream().forEach(n -> peNode.out.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));
    }
}

運行一下,看看結果如何,代碼如下:

public class ProcessEngineTest {

    @Test
    public void testRun() throws Exception {
        //讀取檔案內容到字串
        String modelStr = Tools.readResoucesFile("model/third/hello.xml");
        ProcessEngine processEngine = new ProcessEngine(modelStr);

        processEngine.registNodeProcessor(new OperatorOfApproval());
        processEngine.registNodeProcessor(new OperatorOfApprovalApply());
        processEngine.registNodeProcessor(new OperatorOfNotify());
        processEngine.registNodeProcessor(new OperatorOfSimpleGateway());

        processEngine.start();

        Thread.sleep(1000 * 1);
    }

}

 

approvalResult :false,price : 400
approvalResult :false,price : 300
approvalResult :true,price : 200
小張 提交的申請單 200 經理 審批,結果為 true
process finished!

 

至此,本需求實作完畢,除了直接實作了分支語意外,我們看到,這里還間接實作了回圈語意,

作為一個輕量級的作業流引擎,到此就基本講完了,接下來,我們做一下總結與展望,

 

七、總結與展望

經過以上三個迭代,我們可以得到一個相對穩定的作業流引擎的結構,如下圖所示:

 

通過此圖我們可知,這里有一個相對穩定的引擎層,同時為了提供擴展性,提供了一個節點算子層,所有的節點算子的新增都在此處中,

此外,進行了一定程度的控制反轉,即:由算子決定下一步走哪里,而不是引擎,這樣,極大地提高了引擎的靈活性,更好的進行了封裝,

最后,使用了背景關系,提供了一種全域變數的機制,便于節點之間的資料流動,

當然,以上的三個迭代距離實際的線上應用場景相距甚遠,還需實作與展望以下幾點才可,如下:

  • 一些例外情況的考慮與設計
  • 應把節點抽象成一個函式,要有入參、出參,資料型別等
  • 關鍵的地方加入埋點,用以控制引擎或吐出事件
  • 圖的語意合法性檢查,xsd、自定義檢查技術等
  • 圖的dag演算法檢測
  • 流程的流程歷史記錄,及回滾到任意節點
  • 流程圖的動態修改,即:可以在流程開始后,對流程圖進行修改
  • 并發修改情況下的考慮
  • 效率上的考慮
  • 防止重啟后流轉資訊丟失,需要持久化機制的加入
  • 流程的取消、重置、變數傳入等
  • 更合適的規則引擎及多種規則引擎的實作、配置
  • 前端的畫布、前后端流程資料結構定義及轉換

 

作者:劉洋

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/509613.html

標籤:其他

上一篇:《Vision Permutator: A Permutable MLP-Like ArchItecture For Visual Recognition》論文筆記

下一篇:一、目標檢測概述

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more