一、什么是工作流引擎
工作流引擎是驅(qū)動(dòng)工作流執(zhí)行的一套代碼。
至于什么是工作流、為什么要有工作流、工作流的應(yīng)用景,同學(xué)們可以看一看網(wǎng)上的資料,在此處不在展開。
二、為什么要重復(fù)造輪子
開源的工作流引擎很多,比如 activiti、flowable、Camunda 等,那么,為什么沒有選它們呢?基于以下幾點(diǎn)考慮:
- 最重要的,滿足不了業(yè)務(wù)需求,一些特殊的場景無法實(shí)現(xiàn)。
- 有些需求實(shí)現(xiàn)起來比較繞,更有甚者,需要直接修改引擎數(shù)據(jù)庫,這對(duì)于引擎的穩(wěn)定運(yùn)行帶來了巨大的隱患,也對(duì)以后引擎的版本升級(jí)制造了一些困難。
- 資料、代碼量、API繁多,學(xué)習(xí)成本較高,維護(hù)性較差。
- 經(jīng)過分析與評(píng)估,我們的業(yè)務(wù)場景需要的BPMN元素較少,開發(fā)實(shí)現(xiàn)的代價(jià)不大。
因此,重復(fù)造了輪子,其實(shí),還有一個(gè)更深層次的戰(zhàn)略上的考慮,即:作為科技公司,我們一定要有我們自己的核心底層技術(shù)!這樣,才能不受制于人(參考最近的芯片問題)。
三、怎么造的輪子
對(duì)于一次學(xué)習(xí)型分享來講,過程比結(jié)果更重要,那些只說結(jié)果,不細(xì)說過程甚至不說的分享,我認(rèn)為是秀肌肉,而不是真正意義上的分享。因此,接下來,本文將重點(diǎn)描述造輪子的主要過程。
一個(gè)成熟的工作流引擎的構(gòu)建是很復(fù)雜的,如何應(yīng)對(duì)這種復(fù)雜性呢?一般來講,有以下三種方法:
- 確定性交付:弄清楚需求是什么,驗(yàn)收標(biāo)準(zhǔn)是什么,最好能夠?qū)懗鰷y(cè)試用例,這一步是為了明確目標(biāo)。
- 迭代式開發(fā):先從小的問題集的解決開始,逐步過渡到解決大的問題集上來,羅馬不是一天建成的,人也不是一天就能成熟的,是需要個(gè)過程的。
- 分而治之:把大的問題拆成小的問題,小問題的解決會(huì)推動(dòng)大問題的解決(這個(gè)思想適用場景比較多,同學(xué)們可以用心體會(huì)和理解哈)。
如果按照上述方法,一步一步的詳細(xì)展開,那么可能需要一本書。為了縮減篇幅而又不失干貨,本文會(huì)描述重點(diǎn)幾個(gè)迭代,進(jìn)而闡述輕量級(jí)工作流引擎的設(shè)計(jì)與主要實(shí)現(xiàn)。
那么,輕量級(jí)又是指什么呢?這里,主要是指以下幾點(diǎn)
- 少依賴:代碼的JAVA實(shí)現(xiàn)上,除了jdk8以外,不依賴與其他第三方j(luò)ar包,從而可以更好的減少依賴帶來的問題。
- 內(nèi)核化:設(shè)計(jì)上,采用了微內(nèi)核架構(gòu)模式,內(nèi)核小巧,實(shí)用,同時(shí)提供了一定的擴(kuò)展性。從而可以更好地理解與應(yīng)用本引擎。
- 輕規(guī)范:并沒有完全實(shí)現(xiàn)BPMN規(guī)范,也沒有完全按照BPMN規(guī)范進(jìn)行設(shè)計(jì),而只是參考了該規(guī)范,且只實(shí)現(xiàn)以一小部分必須實(shí)現(xiàn)的元素。從而降低了學(xué)習(xí)成本,可以按照需求自由發(fā)揮。
- 工具化:代碼上,只是一個(gè)工具(UTIL),不是一個(gè)應(yīng)用程序。從而你可以簡單的運(yùn)行它,擴(kuò)展你自己的數(shù)據(jù)層、節(jié)點(diǎn)層,更加方便的集成到其他應(yīng)用中去。
好,廢話說完了,開始第一個(gè)迭代......
四、Hello ProcessEngine
按照國際慣例,第一個(gè)迭代用來實(shí)現(xiàn) hello world 。
1、需求
作為一個(gè)流程管理員,我希望流程引擎可以運(yùn)行如下圖所示的流程,以便我能夠配置流程來打印不同的字符串。
2、分析
- 第一個(gè)流程,可以打印Hello ProcessEngine,第二個(gè)流程可以打印ProcessEngine Hello,這兩個(gè)流程的區(qū)別是只有順序不同,藍(lán)色的節(jié)點(diǎn)與紅色的節(jié)點(diǎn)的本身功能沒有發(fā)生變化
- 藍(lán)色的節(jié)點(diǎn)與紅色的節(jié)點(diǎn)都是節(jié)點(diǎn),它們的功能是不一樣的,即:紅色的節(jié)點(diǎn)打印Hello,藍(lán)色的節(jié)點(diǎn)打印ProcessEngine
- 開始與結(jié)束節(jié)點(diǎn)是兩個(gè)特殊的節(jié)點(diǎn),一個(gè)開始流程,一個(gè)結(jié)束流程
- 節(jié)點(diǎn)與節(jié)點(diǎn)之間是通過線來連接的,一個(gè)節(jié)點(diǎn)執(zhí)行完畢后,是通過箭頭來確定下一個(gè)要執(zhí)行的節(jié)點(diǎn)
- 需要一種表示流程的方式,或是XML、或是JSON、或是其他,而不是圖片
3、設(shè)計(jì)
(1)流程的表示
相較于JSON,XML的語義更豐富,可以表達(dá)更多的信息,因此這里使用XML來對(duì)流程進(jìn)行表示,如下所示
flow_1flow_1flow_2flow_2flow_3flow_3
- process表示一個(gè)流程
- startEvent表示開始節(jié)點(diǎn),endEvent表示結(jié)束節(jié)點(diǎn)
- printHello表示打印hello節(jié)點(diǎn),就是需求中的藍(lán)色節(jié)點(diǎn)
- processEngine表示打印processEngine節(jié)點(diǎn),就是需求中的紅色節(jié)點(diǎn)
- sequenceFlow表示連線,從sourceRef開始,指向targetRef,例如:flow_3,表示一條從printProcessEngine_1到endEvent_1的連線。
(2)節(jié)點(diǎn)的表示
- outgoing表示出邊,即節(jié)點(diǎn)執(zhí)行完畢后,應(yīng)該從那個(gè)邊出去。
- incoming表示入邊,即從哪個(gè)邊進(jìn)入到本節(jié)點(diǎn)。
- 一個(gè)節(jié)點(diǎn)只有outgoing而沒有incoming,如:startEvent,也可以 只有入邊而沒有出邊,如:endEvent,也可以既有入邊也有出邊,如:printHello、processEngine。
(3)流程引擎的邏輯
基于上述XML,流程引擎的運(yùn)行邏輯如下
- 找到開始節(jié)點(diǎn)(startEvent)
- 找到startEvent的outgoing邊(sequenceFlow)
- 找到該邊(sequenceFlow)指向的節(jié)點(diǎn)(targetRef)
- 執(zhí)行節(jié)點(diǎn)自身的邏輯
- 找到該節(jié)點(diǎn)的outgoing邊(sequenceFlow)
- 重復(fù)3-5,直到遇到結(jié)束節(jié)點(diǎn)(endEvent),流程結(jié)束
4、實(shí)現(xiàn)
首先要進(jìn)行數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì),即:要把問題域中的信息映射到計(jì)算機(jī)中的數(shù)據(jù)。
可以看到,一個(gè)流程(PeProcess)由多個(gè)節(jié)點(diǎn)(PeNode)與邊(PeEdge)組成,節(jié)點(diǎn)有出邊(out)、入邊(in),邊有流入節(jié)點(diǎn)(from)、流出節(jié)點(diǎn)(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 : 為了表述主要思想,在代碼上比較“奔放自由”,生產(chǎn)中不可直接復(fù)制粘貼!
接下來,構(gòu)建流程圖,代碼如下:
public class XmlPeProcessBuilder {private String xmlStr;private final Map id2PeNode = new HashMap<>();private final Map id2PeEdge = new HashMap<>();public XmlPeProcessBuilder(String xmlStr) {this.xmlStr = xmlStr;public PeProcess build() throws Exception {//strToNode : 把一段xml轉(zhuǎn)換為org.w3c.dom.NodeNode definations = XmlUtil.strToNode(xmlStr);//childByName : 找到definations子節(jié)點(diǎn)中nodeName為process的那個(gè)NodeNode 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 skipif (node.getNodeType() == Node.TEXT_NODE) continue;if ("sequenceFlow".equals(node.getNodeName()))buildPeEdge(node);elsebuildPeNode(node);Map.Entry 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節(jié)點(diǎn)上屬性為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的nodeValuepeNode.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));
接下來,實(shí)現(xiàn)流程引擎主邏輯,代碼如下:
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;
就這?工作流引擎就這?同學(xué)們可千萬不要這樣簡單理解啊,畢竟這還只是hello world而已,各種代碼量就已經(jīng)不少了。
另外,這里面還有很多可以改進(jìn)的空間,比如異常控制、泛化、設(shè)計(jì)模式等,但畢竟只是一個(gè)hello world而已,其目的是方便同學(xué)理解,讓同學(xué)入門。
那么,接下來呢,就要稍微貼近一些具體的實(shí)際應(yīng)用場景了,我們繼續(xù)第二個(gè)迭代。
五、簡單審批
一般來講工作流引擎屬于底層技術(shù),在它之上可以構(gòu)建審批流、業(yè)務(wù)流、數(shù)據(jù)流等類型的應(yīng)用,那么接下啦就以實(shí)際中的簡單審批場景為例,繼續(xù)深入工作流引擎的設(shè)計(jì),好,我們開始。
1、需求
作為一個(gè)流程管理員,我希望流程引擎可以運(yùn)行如下圖所示的流程,以便我能夠配置流程來實(shí)現(xiàn)簡單的審批流。
例如:小張?zhí)峤涣艘粋€(gè)申請(qǐng)單,然后經(jīng)過經(jīng)理審批,審批結(jié)束后,不管通過還是不通過,都會(huì)經(jīng)過第三步把結(jié)果發(fā)送給小張。
2、分析
- 總體上來講,這個(gè)流程還是線性順序類的,基本上可以沿用上次迭代的部分設(shè)計(jì)
- 審批節(jié)點(diǎn)的耗時(shí)可能會(huì)比較長,甚至?xí)_(dá)到幾天時(shí)間,工作流引擎主動(dòng)式的調(diào)取下一個(gè)節(jié)點(diǎn)的邏輯并不適合此場景
- 隨著節(jié)點(diǎn)類型的增多,工作流引擎里寫死的那部分節(jié)點(diǎn)類型自由邏輯也不合適
- 審批時(shí)需要申請(qǐng)單信息、審批人,結(jié)果郵件通知還需要審批結(jié)果等信息,這些信息如何傳遞也是一個(gè)要考慮的問題
3、設(shè)計(jì)
- 采用注冊(cè)機(jī)制,把節(jié)點(diǎn)類型及其自有邏輯注冊(cè)進(jìn)工作流引擎,以便能夠擴(kuò)展更多節(jié)點(diǎn),使得工作流引擎與節(jié)點(diǎn)解耦
- 工作流引擎增加被動(dòng)式驅(qū)動(dòng)邏輯,使得能夠通過外部來使工作流引擎執(zhí)行下一個(gè)節(jié)點(diǎn)
- 增加上下文語義,作為全局變量來使用,使得數(shù)據(jù)能夠流經(jīng)各個(gè)節(jié)點(diǎn)
4、實(shí)現(xiàn)
新的XML定義如下:
flow_1flow_1flow_2flow_2flow_3flow_3flow_4flow_4
首先要有一個(gè)上下文對(duì)象類,用于傳遞變量的,定義如下:
public class PeContext {private Map info = new ConcurrentHashMap<>();public Object getValue(String key) {return info.get(key);public void putValue(String key, Object value) {info.put(key, value);
每個(gè)節(jié)點(diǎn)的處理邏輯是不一樣的,此處應(yīng)該進(jìn)行一定的抽象,為了強(qiáng)調(diào)流程中節(jié)點(diǎn)的作用是邏輯處理,引入了一種新的類型--算子(Operator),定義如下:
public interface IOperator {//引擎可以據(jù)此來找到本算子String getType();//引擎調(diào)度本算子void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext);
對(duì)于引擎來講,當(dāng)遇到一個(gè)節(jié)點(diǎn)時(shí),需要調(diào)度之,但怎么調(diào)度呢?首先需要各個(gè)節(jié)點(diǎn)算子注冊(cè)(registNodeProcessor())進(jìn)來,這樣才能找到要調(diào)度的那個(gè)算子。
其次,引擎怎么知道節(jié)點(diǎn)算子自有邏輯處理完了呢?一般來講,引擎是不知道的,只能是由算子告訴引擎,所以引擎要提供一個(gè)功能(nodeFinished()),這個(gè)功能由算子調(diào)用。
最后,把算子任務(wù)的調(diào)度和引擎的驅(qū)動(dòng)解耦開來,放入不同的線程中。
修改后的ProcessEngine代碼如下:
public class ProcessEngine {private String xmlStr;//存儲(chǔ)算子private Map type2Operator = new ConcurrentHashMap<>();private PeProcess peProcess = null;private PeContext peContext = null;//任務(wù)數(shù)據(jù)暫存public final BlockingQueue arrayBlockingQueue = new LinkedBlockingQueue();//任務(wù)調(diào)度線程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;//算子注冊(cè)到引擎中,便于引擎調(diào)用之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);elseSystem.out.println("process finished!");public void nodeFinished(String peNodeID) {PeNode node = peProcess.peNodeWithID(peNodeID);executeNode(node.out.to);
接下來,簡單(簡陋)實(shí)現(xiàn)本示例所需的三個(gè)算子,代碼如下:
* 提交申請(qǐng)單public class OperatorOfApprovalApply implements IOperator {@Overridepublic String getType() {return "approvalApply";@Overridepublic void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {peContext.putValue("form", "formInfo");peContext.putValue("applicant", "小張");processEngine.nodeFinished(node.id);* 審批public class OperatorOfApproval implements IOperator {@Overridepublic String getType() {return "approval";@Overridepublic void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {peContext.putValue("approver", "經(jīng)理");peContext.putValue("message", "審批通過");processEngine.nodeFinished(node.id);* 結(jié)果郵件通知public class OperatorOfNotify implements IOperator {@Overridepublic String getType() {return "notify";@Overridepublic void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {System.out.println(String.format("%s 提交的申請(qǐng)單 %s 被 %s 審批,結(jié)果為 %s",peContext.getValue("applicant"),peContext.getValue("form"),peContext.getValue("approver"),peContext.getValue("message")));processEngine.nodeFinished(node.id);
運(yùn)行一下,看看結(jié)果如何,代碼如下:
public class ProcessEng.NETest {@Testpublic void testRun() throws Exception {//讀取文件內(nèi)容到字符串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);
小張 提交的申請(qǐng)單 formInfo 被 經(jīng)理 審批,結(jié)果為 審批通過process finished!
到此,輕量級(jí)工作流引擎的核心邏輯介紹的差不多了,然而,只支持順序結(jié)構(gòu)是太單薄的,我們知道,程序流程的三種基本結(jié)構(gòu)為順序、分支、循環(huán),有了這三種結(jié)構(gòu),基本上就可以表示絕大多數(shù)流程邏輯。循環(huán)可以看做一種組合結(jié)構(gòu),即:循環(huán)可以由順序與分支推導(dǎo)出來,我們已經(jīng)實(shí)現(xiàn)了順序,那么接下來只要實(shí)現(xiàn)分支即可,而分支有很多類型,如:二選一、N選一、N選M(1<=M<=N),其中N選一可以由二選一的組合推導(dǎo)出來,N選M也可以由二選一的組合推導(dǎo)出來,只是比較啰嗦,不那么直觀,所以,我們只要實(shí)現(xiàn)二選一分支,即可滿足絕大多數(shù)流程邏輯場景,好,第三個(gè)迭代開始。
六、一般審批
作為一個(gè)流程管理員,我希望流程引擎可以運(yùn)行如下圖所示的流程,以便我能夠配置流程來實(shí)現(xiàn)一般的審批流。
例如:小張?zhí)峤涣艘粋€(gè)申請(qǐng)單,然后經(jīng)過經(jīng)理審批,審批結(jié)束后,如果通過,發(fā)郵件通知,不通過,則打回重寫填寫申請(qǐng)單,直到通過為止。
1、分析
- 需要引入一種分支節(jié)點(diǎn),可以進(jìn)行簡單的二選一流轉(zhuǎn)
- 節(jié)點(diǎn)的入邊、出邊不只一條
- 需要一種邏輯表達(dá)式語義,可以配置分支節(jié)點(diǎn)
2、設(shè)計(jì)
- 節(jié)點(diǎn)要支持多入邊、多出邊
- 節(jié)點(diǎn)算子來決定從哪個(gè)出邊出
- 使用一種簡單的規(guī)則引擎,支持簡單的邏輯表達(dá)式的解析
- 簡單分支節(jié)點(diǎn)的XML定義
3、實(shí)現(xiàn)
新的XML定義如下:
flow_1flow_1flow_5flow_2flow_2flow_3flow_4approvalResultflow_3flow_4flow_5flow_4flow_6flow_6
其中,加入了simpleGateway這個(gè)簡單分支節(jié)點(diǎn),用于表示簡單的二選一分支,當(dāng)expr中的表達(dá)式為真時(shí),走trueOutGoing中的出邊,否則走另一個(gè)出邊。
節(jié)點(diǎn)支持多入邊、多出邊,修改后的PeNode如下:
public class PeNode {public String id;public String type;public List in = new ArrayList<>();public List 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();
以前只有一個(gè)出邊時(shí),是由當(dāng)前節(jié)點(diǎn)來決定下一節(jié)點(diǎn)的,現(xiàn)在多出邊了,該由邊來決定下一個(gè)節(jié)點(diǎn)是什么,修改后的流程引擎代碼如下:
public class ProcessEngine {private String xmlStr;//存儲(chǔ)算子private Map type2Operator = new ConcurrentHashMap<>();private PeProcess peProcess = null;private PeContext peContext = null;//任務(wù)數(shù)據(jù)暫存public final BlockingQueue arrayBlockingQueue = new LinkedBlockingQueue();//任務(wù)調(diào)度線程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;//算子注冊(cè)到引擎中,便于引擎調(diào)用之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);elseSystem.out.println("process finished!");public void nodeFinished(PeEdge nextPeEdgeID) {executeNode(nextPeEdgeID.to);
新加入的simpleGateway節(jié)點(diǎn)算子如下:
* 簡單是非判斷public class OperatorOfSimpleGateway implements IOperator {@Overridepublic String getType() {return "simpleGateway";@Overridepublic 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腳本作為表達(dá)式,當(dāng)然其中的弊端這里就不展開了。
為了方便同學(xué)們CC+CV,其他發(fā)生相應(yīng)變化的代碼如下:
* 審批public class OperatorOfApproval implements IOperator {@Overridepublic String getType() {return "approval";@Overridepublic void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {peContext.putValue("approver", "經(jīng)理");Integer price = (Integer) peContext.getValue("price");//價(jià)格<=200審批才通過,即:approvalResult=trueboolean approvalResult = price <= 200;peContext.putValue("approvalResult", approvalResult);System.out.println("approvalResult :" + approvalResult + ",price : " + price);processEngine.nodeFinished(node.onlyOneOut());* 提交申請(qǐng)單public class OperatorOfApprovalApply implements IOperator {public static int price = 500;@Overridepublic String getType() {return "approvalApply";@Overridepublic void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {//price每次減100peContext.putValue("price", price -= 100);peContext.putValue("applicant", "小張");processEngine.nodeFinished(node.onlyOneOut());* 結(jié)果郵件通知public class OperatorOfNotify implements IOperator {@Overridepublic String getType() {return "notify";@Overridepublic void doTask(ProcessEngine processEngine, PeNode node, PeContext peContext) {System.out.println(String.format("%s 提交的申請(qǐng)單 %s 被 %s 審批,結(jié)果為 %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 id2PeNode = new HashMap<>();private final Map id2PeEdge = new HashMap<>();public XmlPeProcessBuilder(String xmlStr) {this.xmlStr = xmlStr;public PeProcess build() throws Exception {//strToNode : 把一段xml轉(zhuǎn)換為org.w3c.dom.NodeNode definations = XmlUtil.strToNode(xmlStr);//childByName : 找到definations子節(jié)點(diǎn)中nodeName為process的那個(gè)NodeNode 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 skipif (node.getNodeType() == Node.TEXT_NODE) continue;if ("sequenceFlow".equals(node.getNodeName()))buildPeEdge(node);elsebuildPeNode(node);Map.Entry 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節(jié)點(diǎn)上屬性為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 inPeEdgeNodes = XmlUtil.childsByName(node, "incoming");inPeEdgeNodes.stream().forEach(n -> peNode.in.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));List outPeEdgeNodes = XmlUtil.childsByName(node, "outgoing");outPeEdgeNodes.stream().forEach(n -> peNode.out.add(id2PeEdge.computeIfAbsent(XmlUtil.text(n), id -> new PeEdge(id))));
運(yùn)行一下,看看結(jié)果如何,代碼如下:
public class ProcessEngineTest {@Testpublic void testRun() throws Exception {//讀取文件內(nèi)容到字符串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 : 400approvalResult :false,price : 300approvalResult :true,price : 200小張 提交的申請(qǐng)單 200 被 經(jīng)理 審批,結(jié)果為 trueprocess finished!
至此,本需求實(shí)現(xiàn)完畢,除了直接實(shí)現(xiàn)了分支語義外,我們看到,這里還間接實(shí)現(xiàn)了循環(huán)語義。
作為一個(gè)輕量級(jí)的工作流引擎,到此就基本講完了,接下來,我們做一下總結(jié)與展望。
七、總結(jié)與展望
經(jīng)過以上三個(gè)迭代,我們可以得到一個(gè)相對(duì)穩(wěn)定的工作流引擎的結(jié)構(gòu),如下圖所示:
通過此圖我們可知,這里有一個(gè)相對(duì)穩(wěn)定的引擎層,同時(shí)為了提供擴(kuò)展性,提供了一個(gè)節(jié)點(diǎn)算子層,所有的節(jié)點(diǎn)算子的新增都在此處中。
此外,進(jìn)行了一定程度的控制反轉(zhuǎn),即:由算子決定下一步走哪里,而不是引擎。這樣,極大地提高了引擎的靈活性,更好的進(jìn)行了封裝。
最后,使用了上下文,提供了一種全局變量的機(jī)制,便于節(jié)點(diǎn)之間的數(shù)據(jù)流動(dòng)。
當(dāng)然,以上的三個(gè)迭代距離實(shí)際的線上應(yīng)用場景相距甚遠(yuǎn),還需實(shí)現(xiàn)與展望以下幾點(diǎn)才可,如下:
- 一些異常情況的考慮與設(shè)計(jì)
- 應(yīng)把節(jié)點(diǎn)抽象成一個(gè)函數(shù),要有入?yún)ⅰ⒊鰠ⅲ瑪?shù)據(jù)類型等
- 關(guān)鍵的地方加入埋點(diǎn),用以控制引擎或吐出事件
- 圖的語義合法性檢查,xsd、自定義檢查技術(shù)等
- 圖的dag算法檢測(cè)
- 流程的流程歷史記錄,及回滾到任意節(jié)點(diǎn)
- 流程圖的動(dòng)態(tài)修改,即:可以在流程開始后,對(duì)流程圖進(jìn)行修改
- 并發(fā)修改情況下的考慮
- 效率上的考慮
- 防止重啟后流轉(zhuǎn)信息丟失,需要持久化機(jī)制的加入
- 流程的取消、重置、變量傳入等
- 更合適的規(guī)則引擎及多種規(guī)則引擎的實(shí)現(xiàn)、配置
- 前端的畫布、前后端流程數(shù)據(jù)結(jié)構(gòu)定義及轉(zhuǎn)換
作者:劉洋