仿釘釘流程輕松實現(xiàn)JSON轉(zhuǎn)BPMN完整實現(xiàn)過程示例
前言
寫過工作流都會遇到這樣的難題,希望流程的設(shè)計可以類似釘釘一樣簡單明了,而不是超級不有好的bpmn設(shè)計器,上網(wǎng)大概搜了一下實現(xiàn)方案,前端仿釘釘設(shè)計器一大堆,例如wflow,smart-flow-design,參照這些源碼前端設(shè)計器不成問題
問題在于這樣的設(shè)計器數(shù)據(jù)是json格式,不符合bpmn協(xié)議,就無法和activiti,flowable等工作流直接對接
如果自己開發(fā)工作流引擎,但開發(fā)成本肯定比較大,所以還是希望能實現(xiàn)自定義的json和xml可以轉(zhuǎn)換
方案
轉(zhuǎn)換這個活可以前端干,也可以后端干,如果前端干可以使用bpmn-moddle,bpmn.js就是使用它生成的xml,但大概看了一下發(fā)現(xiàn)文檔稀缺,使用很起來很難
最終決定使用java轉(zhuǎn)換,因為發(fā)現(xiàn)activiti包中的BpmnModel可以很輕松畫出xml,而且基本不用看文檔,.方法名基本就能和bpmn協(xié)議對上號
json協(xié)議
前后端使用json來表達(dá)流程設(shè)計,那一定要訂一套自己的協(xié)議,大概按照smart-flow-design寫一個簡版的

smart-flow-design
{
"id": "節(jié)點id",
"name": "節(jié)點名稱",
"type": "申請節(jié)點/審核節(jié)點/分支節(jié)點/抄送節(jié)點",
"next": "下一節(jié)點",
"exclusive": [// 排他條件
{
// 條件
"condition": "條件表達(dá)式",
//分支節(jié)點內(nèi)部流程
"process": {}
}
],
// 委派人
"assignee": {
"users": [],
"multiMode": "會簽/順序?qū)徟?
},
// 表單權(quán)限
"formPerms": [
{
"key": "字段key",
"perm": "權(quán)限類型 編輯/只讀/隱藏",
"required": true
}
]
}節(jié)點類型簡單實現(xiàn)幾個:申請節(jié)點/審核節(jié)點/分支節(jié)點/抄送節(jié)點
通過next指向下一節(jié)點,實現(xiàn)一個鏈表結(jié)構(gòu)
如一個簡單的流程設(shè)計如下

簡單工作流
對應(yīng)的json數(shù)據(jù)如下
{
"id": "1",
"name": "申請節(jié)點",
"type": "ROOT",
"next": {
"id": "2",
"name": "審批節(jié)點",
"type": "APPROVAL",
"next": {
"id": "3",
"name": "抄送節(jié)點",
"type": "CC"
}
}
}帶分支的設(shè)計如下

分支工作流
對應(yīng)的json:
{
"id": "1",
"name": "申請節(jié)點",
"type": "ROOT",
"next": {
"id": "2",
"name": "條件節(jié)點",
"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"
}
}
}基本上這個json數(shù)據(jù)結(jié)構(gòu)就足夠標(biāo)識很多場景了,分支條件可以自己再寫復(fù)雜一點,如果需要擴(kuò)展新增屬性即可
java
java 創(chuàng)建一些實體來接受json,很簡單就不詳細(xì)寫了,大概如下
@Data
public class ProcessNode {
@ApiModelProperty(value = "節(jié)點ID")
private String id;
@ApiModelProperty(value = "節(jié)點名稱")
private String name;
@ApiModelProperty(value = "節(jié)點類型")
private String type;
@ApiModelProperty(value = "下一節(jié)點")
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
接下來就把這個java實體轉(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ù)工作就是往這個圖紙上畫節(jié)點和線
繪制開始結(jié)束
由于json協(xié)議中不包含開始結(jié)束節(jié)點,所以首先要繪制出兩個節(jié)點
開始節(jié)點
// 新建開始節(jié)點
StartEvent startEvent = new StartEvent();
startEvent.setId("_start");
// 繪制到圖紙
process.addFlowElement(startEvent)
結(jié)束節(jié)點
// 新建結(jié)束節(jié)點
EndEvent endEvent = new EndEvent();
endEvent.setId("_end");
// 繪制到圖紙
process.addFlowElement(endEvent)
到此兩個節(jié)點就畫出來了,但是還沒有任何線
繪制bpmn
接下來就根據(jù)json的節(jié)點來繪制bpmn節(jié)點,同時還要考慮線的繪制節(jié)點的連接線
json協(xié)議中是next指向下一節(jié)點,所以繪制節(jié)點的方法一定是要使用遞歸的畫法,為了處理畫線問題,可以在繪制方法中添加兩個參數(shù)preId(上一節(jié)點ID)和endId(結(jié)束節(jié)點ID)
這樣邏輯為如下:
- 繪制bpmn節(jié)點
- 繪制上一節(jié)點與當(dāng)前節(jié)點的連線
- 如果有next,遞歸繪制下一節(jié)點
- 如果沒有next,繪制當(dāng)前節(jié)點與結(jié)束節(jié)點的連接線
考慮到上一根線可能有條件,所以再加入?yún)?shù)preExpression(上一根線的條件),最終方法如下
/**
* 繪制節(jié)點
* @param process bpmn process 圖紙
* @param node json的節(jié)點
* @param preId 上一節(jié)點id
* @param endId 結(jié)束節(jié)點
* @param preExpression 上一節(jié)點表達(dá)式
*/
public void drawNode(Process process, ProcessNode node, String preId, String endId, String preExpression) {
// 根據(jù)type繪制不同種類的節(jié)點
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 {
// 有下一步,遞歸繪制下一個節(jié)點
drawNode(process, node.getNext(), inout.getOut(), endId, null);
}
}其中drawNodeByType(process, node)方法根據(jù)不同的種類畫不通過的節(jié)點,反回是一個Inout
@Data
@AllArgsConstructor
public class Inout {
private String in;
private String out;
}
代表進(jìn)入節(jié)點的id和出節(jié)點的id,這是因為json的節(jié)點和bpmn的節(jié)點不是一一對應(yīng)的,普通的審核節(jié)點,in和out都是審核節(jié)點id,而如果是分支節(jié)點,in代表分支的開始網(wǎng)關(guān)id,out代表分支結(jié)束網(wǎng)關(guān)的id,接下來分別以兩種節(jié)點類型舉例來實現(xiàn)
/**
* 繪制不同種類節(jié)點
* @param process
* @param node
* @return
*/
private Inout drawNodeByType(Process process, ProcessNode node) {
if (node.getType().equals("審核節(jié)點")) {
return drawAuditNode(process, node);
} else if (node.getType().equals("分支節(jié)點")) {
return drawExclusiveNode(process, node);
} else {
throw new IllegalArgumentException();
}
}審核節(jié)點
/**
* 繪制審核節(jié)點
* @param process
* @param node
* @return
*/
private Inout drawAuditNode(Process process, ProcessNode node) {
// 繪制節(jié)點
String id = "Node_"+UUID.randomUUID();
UserTask userTask = new UserTask();
userTask.setId(id);
userTask.setName(node.getName());
// 設(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é)點配置到擴(kuò)展屬性
Map<String, Object> extensions = new HashMap<>();
extensions.put("node", node);
BpmnUtil.addExtensionProperty(userTask, extensions);
// 只有一個節(jié)點,in&out相同
return new Inout(id, id);
}分支節(jié)點
/**
* 繪制分支節(jié)點
* @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);
}
注意:繪制分支時如果有子流程,又回調(diào)用了drawNode,這是preId為開始網(wǎng)關(guān)id,endId是結(jié)束網(wǎng)關(guān)id,并且攜帶了表達(dá)式
其他類型的節(jié)點都類似,很簡單,不寫了
bpmn繪制完了,如果使用activiti就可以直接部署B(yǎng)pmnModel對象了
Deployment deployment = repositoryService
.createDeployment()
.addBpmnModel("test", bpmnModel)
.deploy();
自動布局
如果要轉(zhuǎn)換xml,上面的bpmnModel只有節(jié)點和線,并沒有布局,可以使用第三方輕松布局
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-layout</artifactId>
<version>7.1.0.M1</version>
<scope>compile</scope>
</dependency>代碼一行就夠了
// 四.自動布局 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(">",">");最終
貼一下完整實例代碼(代碼只是簡版,只為提供思路
/**
* @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é)點
StartEvent startEvent = new StartEvent();// 新建開始節(jié)點
startEvent.setId("_start");
process.addFlowElement(startEvent);// 繪制到圖紙
EndEvent endEvent = new EndEvent(); // 新建結(jié)束節(jié)點
endEvent.setId("_end");// 繪制到圖紙
process.addFlowElement(endEvent);
// 三.遞歸繪制節(jié)點
drawNode(process, node, "_start", "_end", null);
// 四.自動布局
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é)點
* @param process bpmn process 圖紙
* @param node json的節(jié)點
* @param preId 上一節(jié)點id
* @param endId 結(jié)束節(jié)點
* @param preExpression 上一節(jié)點表達(dá)式
*/
public void drawNode(Process process, ProcessNode node, String preId, String endId, String preExpression) {
// 根據(jù)type繪制不同種類的節(jié)點
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 {
// 有下一步, 遞歸繪制下一個節(jié)點
drawNode(process, node.getNext(), inout.getOut(), endId, null);
}
}
/**
* 繪制不同種類節(jié)點
* @param process
* @param node
* @return
*/
private Inout drawNodeByType(Process process, ProcessNode node) {
if (node.getType().equals("審核節(jié)點")) {
return drawAuditNode(process, node);
} else if (node.getType().equals("分支節(jié)點")) {
return drawExclusiveNode(process, node);
} else {
throw new IllegalArgumentException();
}
}
/**
* 繪制審核節(jié)點
* @param process
* @param node
* @return
*/
private Inout drawAuditNode(Process process, ProcessNode node) {
// 繪制節(jié)點
String id = "Node_"+UUID.randomUUID();
UserTask userTask = new UserTask();
userTask.setId(id);
userTask.setName(node.getName());
// 設(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é)點配置到擴(kuò)展屬性
Map<String, Object> extensions = new HashMap<>();
extensions.put("node", node);
BpmnUtil.addExtensionProperty(userTask, extensions);
return new Inout(id, id);
}
/**
* 繪制分支節(jié)點
* @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é)自己完善即可
我自己做了個相對復(fù)雜的json,轉(zhuǎn)換為xml最終在bpmn.js展示效果如下

功能都沒大問題,就是自動布局的線有點扭曲
以上就是仿釘釘流程輕松實現(xiàn)JSON轉(zhuǎn)BPMN完整實現(xiàn)過程示例的詳細(xì)內(nèi)容,更多關(guān)于仿釘釘流程JSON轉(zhuǎn)BPMN的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java使用Freemarker頁面靜態(tài)化生成的實現(xiàn)
這篇文章主要介紹了Java使用Freemarker頁面靜態(tài)化生成的實現(xiàn),頁面靜態(tài)化是將原來的動態(tài)網(wǎng)頁改為通過靜態(tài)化技術(shù)生成的靜態(tài)網(wǎng)頁,FreeMarker?是一個用?Java?語言編寫的模板引擎,它基于模板來生成文本輸,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下2022-06-06

