仿釘釘流程輕松實(shí)現(xiàn)JSON轉(zhuǎn)BPMN完整實(shí)現(xiàn)過程示例
前言
寫過工作流都會(huì)遇到這樣的難題,希望流程的設(shè)計(jì)可以類似釘釘一樣簡單明了,而不是超級不有好的bpmn設(shè)計(jì)器,上網(wǎng)大概搜了一下實(shí)現(xiàn)方案,前端仿釘釘設(shè)計(jì)器一大堆,例如wflow,smart-flow-design,參照這些源碼前端設(shè)計(jì)器不成問題
問題在于這樣的設(shè)計(jì)器數(shù)據(jù)是json格式,不符合bpmn協(xié)議,就無法和activiti,flowable等工作流直接對接
如果自己開發(fā)工作流引擎,但開發(fā)成本肯定比較大,所以還是希望能實(shí)現(xiàn)自定義的json和xml可以轉(zhuǎn)換
方案
轉(zhuǎn)換這個(gè)活可以前端干,也可以后端干,如果前端干可以使用bpmn-moddle,bpmn.js就是使用它生成的xml,但大概看了一下發(fā)現(xiàn)文檔稀缺,使用很起來很難
最終決定使用java轉(zhuǎn)換,因?yàn)榘l(fā)現(xiàn)activiti包中的BpmnModel
可以很輕松畫出xml,而且基本不用看文檔,.方法名基本就能和bpmn協(xié)議對上號
json協(xié)議
前后端使用json來表達(dá)流程設(shè)計(jì),那一定要訂一套自己的協(xié)議,大概按照smart-flow-design寫一個(gè)簡版的
smart-flow-design
{ "id": "節(jié)點(diǎn)id", "name": "節(jié)點(diǎn)名稱", "type": "申請節(jié)點(diǎn)/審核節(jié)點(diǎn)/分支節(jié)點(diǎn)/抄送節(jié)點(diǎn)", "next": "下一節(jié)點(diǎn)", "exclusive": [// 排他條件 { // 條件 "condition": "條件表達(dá)式", //分支節(jié)點(diǎn)內(nèi)部流程 "process": {} } ], // 委派人 "assignee": { "users": [], "multiMode": "會(huì)簽/順序?qū)徟? }, // 表單權(quán)限 "formPerms": [ { "key": "字段key", "perm": "權(quán)限類型 編輯/只讀/隱藏", "required": true } ] }
節(jié)點(diǎn)類型簡單實(shí)現(xiàn)幾個(gè):申請節(jié)點(diǎn)/審核節(jié)點(diǎn)/分支節(jié)點(diǎn)/抄送節(jié)點(diǎn)
通過next指向下一節(jié)點(diǎn),實(shí)現(xiàn)一個(gè)鏈表結(jié)構(gòu)
如一個(gè)簡單的流程設(shè)計(jì)如下
簡單工作流
對應(yīng)的json數(shù)據(jù)如下
{ "id": "1", "name": "申請節(jié)點(diǎn)", "type": "ROOT", "next": { "id": "2", "name": "審批節(jié)點(diǎn)", "type": "APPROVAL", "next": { "id": "3", "name": "抄送節(jié)點(diǎn)", "type": "CC" } } }
帶分支的設(shè)計(jì)如下
分支工作流
對應(yīng)的json:
{ "id": "1", "name": "申請節(jié)點(diǎn)", "type": "ROOT", "next": { "id": "2", "name": "條件節(jié)點(diǎn)", "type": "EXCLUSIVE", "exclusive": [ { "condition": "amount>=100" }, { "condition": "amount<100", "process": { "id": "4", "name": "審批人1", "type": "APPROVAL", "next": null } } ], "next": { "id": "3", "name": "審批人2", "type": "APPROVAL" } } }
基本上這個(gè)json數(shù)據(jù)結(jié)構(gòu)就足夠標(biāo)識很多場景了,分支條件可以自己再寫復(fù)雜一點(diǎn),如果需要擴(kuò)展新增屬性即可
java
java 創(chuàng)建一些實(shí)體來接受json,很簡單就不詳細(xì)寫了,大概如下
@Data public class ProcessNode { @ApiModelProperty(value = "節(jié)點(diǎn)ID") private String id; @ApiModelProperty(value = "節(jié)點(diǎn)名稱") private String name; @ApiModelProperty(value = "節(jié)點(diǎn)類型") private String type; @ApiModelProperty(value = "下一節(jié)點(diǎn)") private ProcessNode next; @ApiModelProperty(value = "分支") private List<ExclusiveBranch> exclusive; @ApiModelProperty(value = "委托人") private Assignee assignee; @ApiModelProperty(value = "表單權(quán)限") private List<FormPerm> formPerms; } @Data public class ExclusiveBranch { @ApiModelProperty(value = "id") private String id; @ApiModelProperty(value = "分支條件") private String condition; @ApiModelProperty(value = "分支內(nèi)部流程") private ProcessNode process; } @Data public class Assignee { @ApiModelProperty(value = "委托人列表") private List<String> users; @ApiModelProperty(value = "多人審批方式") private String multiMode; }
在controller使用@RequestBody
接受一下前端傳來的json即可
轉(zhuǎn)BPMN
接下來就把這個(gè)java實(shí)體轉(zhuǎn)成xml,引入今天的主角:BpmnModel
引入依賴
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-bpmn-model</artifactId> <version>7.1.0.M1</version> </dependency>
即可開始使用BpmnModel
開始繪制bpmn協(xié)議的工作流
初始化
首先準(zhǔn)備工作流
BpmnModel model = new BpmnModel(); Process process = new Process(); model.addProcess(process); process.setId("Process_"+UUID.randomUUID()); process.setExecutable(true);
其中process就相當(dāng)于我們的圖紙,后續(xù)工作就是往這個(gè)圖紙上畫節(jié)點(diǎn)和線
繪制開始結(jié)束
由于json協(xié)議中不包含開始結(jié)束節(jié)點(diǎn),所以首先要繪制出兩個(gè)節(jié)點(diǎn)
開始節(jié)點(diǎn)
// 新建開始節(jié)點(diǎn) StartEvent startEvent = new StartEvent(); startEvent.setId("_start"); // 繪制到圖紙 process.addFlowElement(startEvent)
結(jié)束節(jié)點(diǎn)
// 新建結(jié)束節(jié)點(diǎn) EndEvent endEvent = new EndEvent(); endEvent.setId("_end"); // 繪制到圖紙 process.addFlowElement(endEvent)
到此兩個(gè)節(jié)點(diǎn)就畫出來了,但是還沒有任何線
繪制bpmn
接下來就根據(jù)json的節(jié)點(diǎn)來繪制bpmn節(jié)點(diǎn),同時(shí)還要考慮線的繪制節(jié)點(diǎn)的連接線
json協(xié)議中是next指向下一節(jié)點(diǎn),所以繪制節(jié)點(diǎn)的方法一定是要使用遞歸的畫法,為了處理畫線問題,可以在繪制方法中添加兩個(gè)參數(shù)preId
(上一節(jié)點(diǎn)ID)和endId
(結(jié)束節(jié)點(diǎn)ID)
這樣邏輯為如下:
- 繪制bpmn節(jié)點(diǎn)
- 繪制上一節(jié)點(diǎn)與當(dāng)前節(jié)點(diǎn)的連線
- 如果有next,遞歸繪制下一節(jié)點(diǎn)
- 如果沒有next,繪制當(dāng)前節(jié)點(diǎn)與結(jié)束節(jié)點(diǎn)的連接線
考慮到上一根線可能有條件,所以再加入?yún)?shù)preExpression
(上一根線的條件),最終方法如下
/** * 繪制節(jié)點(diǎn) * @param process bpmn process 圖紙 * @param node json的節(jié)點(diǎn) * @param preId 上一節(jié)點(diǎn)id * @param endId 結(jié)束節(jié)點(diǎn) * @param preExpression 上一節(jié)點(diǎn)表達(dá)式 */ public void drawNode(Process process, ProcessNode node, String preId, String endId, String preExpression) { // 根據(jù)type繪制不同種類的節(jié)點(diǎn) Inout inout = drawNodeByType(process, node); // 繪制前一根線 process.addFlowElement(createSequenceFlow(preId, inout.getIn(), preExpression)); if (node.getNext() == null) { // 沒有下一步,繪制指向結(jié)束的線 process.addFlowElement(createSequenceFlow(inout.getOut(), endId, null)); } else { // 有下一步,遞歸繪制下一個(gè)節(jié)點(diǎn) drawNode(process, node.getNext(), inout.getOut(), endId, null); } }
其中drawNodeByType(process, node)
方法根據(jù)不同的種類畫不通過的節(jié)點(diǎn),反回是一個(gè)Inout
@Data @AllArgsConstructor public class Inout { private String in; private String out; }
代表進(jìn)入節(jié)點(diǎn)的id和出節(jié)點(diǎn)的id,這是因?yàn)?strong>json的節(jié)點(diǎn)和bpmn的節(jié)點(diǎn)不是一一對應(yīng)的,普通的審核節(jié)點(diǎn),in和out都是審核節(jié)點(diǎn)id,而如果是分支節(jié)點(diǎn),in代表分支的開始網(wǎng)關(guān)id,out代表分支結(jié)束網(wǎng)關(guān)的id,接下來分別以兩種節(jié)點(diǎn)類型舉例來實(shí)現(xiàn)
/** * 繪制不同種類節(jié)點(diǎn) * @param process * @param node * @return */ private Inout drawNodeByType(Process process, ProcessNode node) { if (node.getType().equals("審核節(jié)點(diǎn)")) { return drawAuditNode(process, node); } else if (node.getType().equals("分支節(jié)點(diǎn)")) { return drawExclusiveNode(process, node); } else { throw new IllegalArgumentException(); } }
審核節(jié)點(diǎn)
/** * 繪制審核節(jié)點(diǎn) * @param process * @param node * @return */ private Inout drawAuditNode(Process process, ProcessNode node) { // 繪制節(jié)點(diǎn) String id = "Node_"+UUID.randomUUID(); UserTask userTask = new UserTask(); userTask.setId(id); userTask.setName(node.getName()); // 設(shè)置多實(shí)例 userTask.setAssignee("${user}"); MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = new MultiInstanceLoopCharacteristics(); if (node.getAssignee().getMultiMode().equals("順序?qū)徟?)) { multiInstanceLoopCharacteristics.setSequential(true); } multiInstanceLoopCharacteristics.setElementVariable("user"); // 完成條件 multiInstanceLoopCharacteristics.setCompletionCondition("${nrOfInstances == nrOfCompletedInstances}"); multiInstanceLoopCharacteristics.setInputDataItem("${users}"); userTask.setLoopCharacteristics(multiInstanceLoopCharacteristics); // 保存json節(jié)點(diǎn)配置到擴(kuò)展屬性 Map<String, Object> extensions = new HashMap<>(); extensions.put("node", node); BpmnUtil.addExtensionProperty(userTask, extensions); // 只有一個(gè)節(jié)點(diǎn),in&out相同 return new Inout(id, id); }
分支節(jié)點(diǎn)
/** * 繪制分支節(jié)點(diǎn) * @param process * @param node * @return */ private Inout drawExclusiveNode(Process process, ProcessNode node) { // 開始網(wǎng)關(guān) String startId = "Exclusive_"+UUID.randomUUID(); ExclusiveGateway startGateway = new ExclusiveGateway(); startGateway.setId(startId); process.addFlowElement(startGateway); // 結(jié)束網(wǎng)關(guān) String endId = "Exclusive_"+UUID.randomUUID(); ExclusiveGateway endGateway = new ExclusiveGateway(); endGateway.setId(endId); process.addFlowElement(endGateway); // 繪制分支 List<ExclusiveBranch> branches = node.getExclusive(); for (ExclusiveBranch branch : branches) { String expression = branch.getCondition(); if (branch.getProcess()==null) { // 沒有子流程,直接繪制結(jié)束線 process.addFlowElement(createSequenceFlow(startId, endId, expression)); } else { // 有子流程,遞歸繪制子流程 drawNode(process, branch.getProcess(), startId, endId, expression); } } // int和out不一樣 return new Inout(startId, endId); }
注意:繪制分支時(shí)如果有子流程,又回調(diào)用了drawNode,這是preId為開始網(wǎng)關(guān)id,endId是結(jié)束網(wǎng)關(guān)id,并且攜帶了表達(dá)式
其他類型的節(jié)點(diǎn)都類似,很簡單,不寫了
bpmn繪制完了,如果使用activiti就可以直接部署B(yǎng)pmnModel對象了
Deployment deployment = repositoryService .createDeployment() .addBpmnModel("test", bpmnModel) .deploy();
自動(dòng)布局
如果要轉(zhuǎn)換xml,上面的bpmnModel只有節(jié)點(diǎn)和線,并沒有布局,可以使用第三方輕松布局
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-bpmn-layout</artifactId> <version>7.1.0.M1</version> <scope>compile</scope> </dependency>
代碼一行就夠了
// 四.自動(dòng)布局 new BpmnAutoLayout(bpmnModel).execute();
轉(zhuǎn)xml
如果想把BpmnModel轉(zhuǎn)換為xml,也很簡單,引入依賴
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-bpmn-converter</artifactId> <version>7.1.0.M1</version> </dependency>
轉(zhuǎn)換代碼
// 五.轉(zhuǎn)xml BpmnXMLConverter bpmnXMLConverter=new BpmnXMLConverter(); byte[] convertToXML = bpmnXMLConverter.convertToXML(bpmnModel); String xml=new String(convertToXML); xml = xml.replaceAll("<","<").replaceAll(">",">");
最終
貼一下完整實(shí)例代碼(代碼只是簡版,只為提供思路
/** * @Author pq * @Date 2022/10/20 10:58 * @Description */ @SuppressWarnings("ALL") public class BpmnConvert { public String toBpmn(ProcessNode node) { // 一.準(zhǔn)備工作 BpmnModel bpmnModel = new BpmnModel(); Process process = new Process(); // 相當(dāng)于圖紙 bpmnModel.addProcess(process); process.setId("Process_"+UUID.randomUUID()); process.setExecutable(true); // 二.開始結(jié)束節(jié)點(diǎn) StartEvent startEvent = new StartEvent();// 新建開始節(jié)點(diǎn) startEvent.setId("_start"); process.addFlowElement(startEvent);// 繪制到圖紙 EndEvent endEvent = new EndEvent(); // 新建結(jié)束節(jié)點(diǎn) endEvent.setId("_end");// 繪制到圖紙 process.addFlowElement(endEvent); // 三.遞歸繪制節(jié)點(diǎn) drawNode(process, node, "_start", "_end", null); // 四.自動(dòng)布局 new BpmnAutoLayout(bpmnModel).execute(); // 五.轉(zhuǎn)xml BpmnXMLConverter bpmnXMLConverter=new BpmnXMLConverter(); byte[] convertToXML = bpmnXMLConverter.convertToXML(bpmnModel); String xml=new String(convertToXML); xml = xml.replaceAll("<","<").replaceAll(">",">"); return xml; } /** * 繪制節(jié)點(diǎn) * @param process bpmn process 圖紙 * @param node json的節(jié)點(diǎn) * @param preId 上一節(jié)點(diǎn)id * @param endId 結(jié)束節(jié)點(diǎn) * @param preExpression 上一節(jié)點(diǎn)表達(dá)式 */ public void drawNode(Process process, ProcessNode node, String preId, String endId, String preExpression) { // 根據(jù)type繪制不同種類的節(jié)點(diǎn) Inout inout = drawNodeByType(process, node); // 繪制前一根線 process.addFlowElement(createSequenceFlow(preId, inout.getIn(), preExpression)); if (node.getNext() == null) { // 沒有下一步, 繪制指向結(jié)束的線 process.addFlowElement(createSequenceFlow(inout.getOut(), endId, null)); } else { // 有下一步, 遞歸繪制下一個(gè)節(jié)點(diǎn) drawNode(process, node.getNext(), inout.getOut(), endId, null); } } /** * 繪制不同種類節(jié)點(diǎn) * @param process * @param node * @return */ private Inout drawNodeByType(Process process, ProcessNode node) { if (node.getType().equals("審核節(jié)點(diǎn)")) { return drawAuditNode(process, node); } else if (node.getType().equals("分支節(jié)點(diǎn)")) { return drawExclusiveNode(process, node); } else { throw new IllegalArgumentException(); } } /** * 繪制審核節(jié)點(diǎn) * @param process * @param node * @return */ private Inout drawAuditNode(Process process, ProcessNode node) { // 繪制節(jié)點(diǎn) String id = "Node_"+UUID.randomUUID(); UserTask userTask = new UserTask(); userTask.setId(id); userTask.setName(node.getName()); // 設(shè)置多實(shí)例 userTask.setAssignee("${user}"); MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = new MultiInstanceLoopCharacteristics(); if (node.getAssignee().getMultiMode().equals("順序?qū)徟?)) { multiInstanceLoopCharacteristics.setSequential(true); } multiInstanceLoopCharacteristics.setElementVariable("user"); // 完成條件 multiInstanceLoopCharacteristics.setCompletionCondition("${nrOfInstances == nrOfCompletedInstances}"); multiInstanceLoopCharacteristics.setInputDataItem("${users}"); userTask.setLoopCharacteristics(multiInstanceLoopCharacteristics); // 保存json節(jié)點(diǎn)配置到擴(kuò)展屬性 Map<String, Object> extensions = new HashMap<>(); extensions.put("node", node); BpmnUtil.addExtensionProperty(userTask, extensions); return new Inout(id, id); } /** * 繪制分支節(jié)點(diǎn) * @param process * @param node * @return */ private Inout drawExclusiveNode(Process process, ProcessNode node) { // 開始網(wǎng)關(guān) String startId = "Exclusive_"+UUID.randomUUID(); ExclusiveGateway startGateway = new ExclusiveGateway(); startGateway.setId(startId); process.addFlowElement(startGateway); // 結(jié)束網(wǎng)關(guān) String endId = "Exclusive_"+UUID.randomUUID(); ExclusiveGateway endGateway = new ExclusiveGateway(); endGateway.setId(endId); process.addFlowElement(endGateway); // 繪制分支 List<ExclusiveBranch> branches = node.getExclusive(); for (ExclusiveBranch branch : branches) { String expression = branch.getCondition(); if (branch.getProcess()==null) { // 沒有子流程,直接繪制結(jié)束線 process.addFlowElement(createSequenceFlow(startId, endId, expression)); } else { // 有子流程,遞歸繪制子流程 drawNode(process, branch.getProcess(), startId, endId, expression); } } // int和out不一樣 return new Inout(startId, endId); } /** * 創(chuàng)建連線 * @param from * @param to * @return */ public SequenceFlow createSequenceFlow(String from, String to, String conditionExpression) { SequenceFlow flow = new SequenceFlow(); flow.setId(from + "-" + to); flow.setSourceRef(from); flow.setTargetRef(to); if (conditionExpression != null) { flow.setConditionExpression(conditionExpression); } return flow; } }
核心代碼真的沒幾行,細(xì)節(jié)自己完善即可
我自己做了個(gè)相對復(fù)雜的json,轉(zhuǎn)換為xml最終在bpmn.js展示效果如下
功能都沒大問題,就是自動(dòng)布局的線有點(diǎn)扭曲
以上就是仿釘釘流程輕松實(shí)現(xiàn)JSON轉(zhuǎn)BPMN完整實(shí)現(xiàn)過程示例的詳細(xì)內(nèi)容,更多關(guān)于仿釘釘流程JSON轉(zhuǎn)BPMN的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java使用Freemarker頁面靜態(tài)化生成的實(shí)現(xiàn)
這篇文章主要介紹了Java使用Freemarker頁面靜態(tài)化生成的實(shí)現(xiàn),頁面靜態(tài)化是將原來的動(dòng)態(tài)網(wǎng)頁改為通過靜態(tài)化技術(shù)生成的靜態(tài)網(wǎng)頁,FreeMarker?是一個(gè)用?Java?語言編寫的模板引擎,它基于模板來生成文本輸,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下2022-06-06Java基于Graphics2D實(shí)現(xiàn)海報(bào)制作
這篇文章主要為大家詳細(xì)介紹了Java如何基于Graphics2D實(shí)現(xiàn)海報(bào)制作,并且支持自定義顏色,背景,logo,貼圖,感興趣的小伙伴可以了解一下2024-04-04淺談SpringBoot實(shí)現(xiàn)自動(dòng)裝配的方法原理
SpringBoot的自動(dòng)裝配是它的一大特點(diǎn),可以大大提高開發(fā)效率,減少重復(fù)性代碼的編寫。本文將詳細(xì)講解SpringBoot如何實(shí)現(xiàn)自動(dòng)裝配,需要的朋友可以參考下2023-05-05