仿钉钉流程轻松实现JSON转BPMN

5个月前 (12-11) 0 点赞 0 收藏 0 评论 8 已阅读

前言

写过工作流都会遇到这样的难题,希望流程的设计可以类似钉钉一样简单明了,而不是超级不有好的bpmn设计器,上网大概搜了一下实现方案,前端仿钉钉设计器一大堆,例如wflow,smart-flow-design,参照这些源码前端设计器不成问题

问题在于这样的设计器数据是json格式,不符合bpmn协议,就无法和activiti,flowable等工作流直接对接

如果自己开发工作流引擎,但开发成本肯定比较大,所以还是希望能实现自定义的json和xml可以转换

方案

转换这个活可以前端干,也可以后端干,如果前端干可以使用bpmn-moddle,bpmn.js就是使用它生成的xml,但大概看了一下发现文档稀缺,使用很起来很难

最终决定使用java转换,因为发现activiti包中的BpmnModel可以很轻松画出xml,而且基本不用看文档,.方法名基本就能和bpmn协议对上号

json协议

前后端使用json来表达流程设计,那一定要订一套自己的协议,大概按照smart-flow-design写一个简版的

smart-flow-design

{
  "id": "节点id",
  "name": "节点名称",
  "type": "申请节点/审核节点/分支节点/抄送节点",
  "next": "下一节点",
  "exclusive": [// 排他条件
    {
      // 条件
      "condition": "条件表达式",
      //分支节点内部流程
      "process": {}
    }
  ],
  // 委派人
  "assignee": {
    "users": [],
    "multiMode": "会签/顺序审批"
  },
  // 表单权限
  "formPerms": [
    {
      "key": "字段key",
      "perm": "权限类型 编辑/只读/隐藏",
      "required": true
    }
  ]
}

节点类型简单实现几个:申请节点/审核节点/分支节点/抄送节点

通过next指向下一节点,实现一个链表结构

如一个简单的流程设计如下

简单工作流

对应的json数据如下

{
  "id": "1",
  "name": "申请节点",
  "type": "ROOT",
  "next": {
    "id": "2",
    "name": "审批节点",
    "type": "APPROVAL",
    "next": {
      "id": "3",
      "name": "抄送节点",
      "type": "CC"
    }
  }
}

带分支的设计如下

分支工作流

对应的json:

{
  "id": "1",
  "name": "申请节点",
  "type": "ROOT",
  "next": {
    "id": "2",
    "name": "条件节点",
    "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数据结构就足够标识很多场景了,分支条件可以自己再写复杂一点,如果需要扩展新增属性即可

java

java 创建一些实体来接受json,很简单就不详细写了,大概如下

@Data
public class ProcessNode {

    @ApiModelProperty(value = "节点ID")
    private String id;

    @ApiModelProperty(value = "节点名称")
    private String name;

    @ApiModelProperty(value = "节点类型")
    private String type;

    @ApiModelProperty(value = "下一节点")
    private ProcessNode next;

    @ApiModelProperty(value = "分支")
    private List<ExclusiveBranch> exclusive;

    @ApiModelProperty(value = "委托人")
    private Assignee assignee;

    @ApiModelProperty(value = "表单权限")
    private List<FormPerm> formPerms;

}

@Data
public class ExclusiveBranch {
    @ApiModelProperty(value = "id")
    private String id;
    @ApiModelProperty(value = "分支条件")
    private String condition;
    @ApiModelProperty(value = "分支内部流程")
    private ProcessNode process;
}

@Data
public class Assignee {
    @ApiModelProperty(value = "委托人列表")
    private List<String> users;
    @ApiModelProperty(value = "多人审批方式")
    private String multiMode;
}

在controller使用@RequestBody接受一下前端传来的json即可

转BPMN

接下来就把这个java实体转成xml,引入今天的主角:BpmnModel

引入依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-bpmn-model</artifactId>
    <version>7.1.0.M1</version>
</dependency>

即可开始使用BpmnModel开始绘制bpmn协议的工作流

初始化

首先准备工作流

BpmnModel model = new BpmnModel();
Process process = new Process();
model.addProcess(process);
process.setId("Process_"+UUID.randomUUID());
process.setExecutable(true);

其中process就相当于我们的图纸,后续工作就是往这个图纸上画节点和线

绘制开始结束

由于json协议中不包含开始结束节点,所以首先要绘制出两个节点

开始节点

// 新建开始节点
StartEvent startEvent = new StartEvent();
startEvent.setId("_start");
// 绘制到图纸
process.addFlowElement(startEvent)

结束节点

// 新建结束节点
EndEvent endEvent = new EndEvent();
endEvent.setId("_end");
// 绘制到图纸
process.addFlowElement(endEvent)

到此两个节点就画出来了,但是还没有任何线

绘制bpmn

接下来就根据json的节点来绘制bpmn节点,同时还要考虑线的绘制节点的连接线

json协议中是next指向下一节点,所以绘制节点的方法一定是要使用递归的画法,为了处理画线问题,可以在绘制方法中添加两个参数preId(上一节点ID)和endId(结束节点ID)

这样逻辑为如下:

绘制bpmn节点
绘制上一节点与当前节点的连线
如果有next,递归绘制下一节点
如果没有next,绘制当前节点与结束节点的连接线

考虑到上一根线可能有条件,所以再加入参数preExpression(上一根线的条件),最终方法如下

/**
 * 绘制节点
 * @param process bpmn process 图纸
 * @param node    json的节点
 * @param preId   上一节点id
 * @param endId   结束节点
 * @param preExpression 上一节点表达式
 */
public void drawNode(Process process, ProcessNode node, String preId, String endId, String preExpression) {
    // 根据type绘制不同种类的节点
    Inout inout = drawNodeByType(process, node);
    // 绘制前一根线
    process.addFlowElement(createSequenceFlow(preId, inout.getIn(), preExpression));
    if (node.getNext() == null) {
        // 没有下一步,绘制指向结束的线
        process.addFlowElement(createSequenceFlow(inout.getOut(), endId, null));
    } else {
        // 有下一步,递归绘制下一个节点
        drawNode(process, node.getNext(), inout.getOut(), endId, null);
    }
}

其中drawNodeByType(process, node)方法根据不同的种类画不通过的节点,反回是一个Inout

@Data
@AllArgsConstructor
public class Inout {
    private String in;
    private String out;
}

代表进入节点的id和出节点的id,这是因为json的节点和bpmn的节点不是一一对应的,普通的审核节点,in和out都是审核节点id,而如果是分支节点,in代表分支的开始网关id,out代表分支结束网关的id,接下来分别以两种节点类型举例来实现

/**
 * 绘制不同种类节点
 * @param process
 * @param node
 * @return
 */
private Inout drawNodeByType(Process process, ProcessNode node) {
    if (node.getType().equals("审核节点")) {
        return drawAuditNode(process, node);
    } else if (node.getType().equals("分支节点")) {
        return drawExclusiveNode(process, node);
    } else {
        throw new IllegalArgumentException();
    }
}
审核节点
/**
 * 绘制审核节点
 * @param process
 * @param node
 * @return
 */
private Inout drawAuditNode(Process process, ProcessNode node) {
    // 绘制节点
    String id = "Node_"+UUID.randomUUID();
    UserTask userTask = new UserTask();
    userTask.setId(id);
    userTask.setName(node.getName());
    // 设置多实例
    userTask.setAssignee("${user}");
    MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = new MultiInstanceLoopCharacteristics();
    if (node.getAssignee().getMultiMode().equals("顺序审批")) {
        multiInstanceLoopCharacteristics.setSequential(true);
    }
    multiInstanceLoopCharacteristics.setElementVariable("user");
    // 完成条件
    multiInstanceLoopCharacteristics.setCompletionCondition("${nrOfInstances == nrOfCompletedInstances}");
    multiInstanceLoopCharacteristics.setInputDataItem("${users}");
    userTask.setLoopCharacteristics(multiInstanceLoopCharacteristics);
    // 保存json节点配置到扩展属性
    Map<String, Object> extensions = new HashMap<>();
    extensions.put("node", node);
    BpmnUtil.addExtensionProperty(userTask, extensions);
    // 只有一个节点,in&out相同
    return new Inout(id, id);
}
分支节点
/**
 * 绘制分支节点
 * @param process
 * @param node
 * @return
 */
private Inout drawExclusiveNode(Process process, ProcessNode node) {
    // 开始网关
    String startId = "Exclusive_"+UUID.randomUUID();
    ExclusiveGateway startGateway = new ExclusiveGateway();
    startGateway.setId(startId);
    process.addFlowElement(startGateway);
    // 结束网关
    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) {
            // 没有子流程,直接绘制结束线
            process.addFlowElement(createSequenceFlow(startId, endId, expression));
        } else {
            // 有子流程,递归绘制子流程
            drawNode(process, branch.getProcess(), startId, endId, expression);
        }
    }
    // int和out不一样
    return new Inout(startId, endId);
}

注意:绘制分支时如果有子流程,又回调用了drawNode,这是preId为开始网关id,endId是结束网关id,并且携带了表达式

其他类型的节点都类似,很简单,不写了

bpmn绘制完了,如果使用activiti就可以直接部署BpmnModel对象了

Deployment deployment = repositoryService
        .createDeployment()
        .addBpmnModel("test", bpmnModel)
        .deploy();

自动布局

如果要转换xml,上面的bpmnModel只有节点和线,并没有布局,可以使用第三方轻松布局

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-bpmn-layout</artifactId>
    <version>7.1.0.M1</version>
    <scope>compile</scope>
</dependency>

代码一行就够了

// 四.自动布局
new BpmnAutoLayout(bpmnModel).execute();

转xml

如果想把BpmnModel转换为xml,也很简单,引入依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-bpmn-converter</artifactId>
    <version>7.1.0.M1</version>
</dependency>

转换代码

// 五.转xml
BpmnXMLConverter bpmnXMLConverter=new BpmnXMLConverter();
byte[] convertToXML = bpmnXMLConverter.convertToXML(bpmnModel);
String xml=new String(convertToXML);
xml = xml.replaceAll("&lt;","<").replaceAll("&gt;",">");

最终

贴一下完整实例代码(代码只是简版,只为提供思路

/**
 * @Author pq
 * @Date 2022/10/20 10:58
 * @Description
 */
@SuppressWarnings("ALL")
public class BpmnConvert {

    public String toBpmn(ProcessNode node) {
        // 一.准备工作
        BpmnModel bpmnModel = new BpmnModel();
        Process process = new Process(); // 相当于图纸
        bpmnModel.addProcess(process);
        process.setId("Process_"+UUID.randomUUID());
        process.setExecutable(true);
        // 二.开始结束节点
        StartEvent startEvent = new StartEvent();// 新建开始节点
        startEvent.setId("_start");
        process.addFlowElement(startEvent);// 绘制到图纸
        EndEvent endEvent = new EndEvent(); // 新建结束节点
        endEvent.setId("_end");// 绘制到图纸
        process.addFlowElement(endEvent);
        // 三.递归绘制节点
        drawNode(process, node, "_start", "_end", null);
        // 四.自动布局
        new BpmnAutoLayout(bpmnModel).execute();
        // 五.转xml
        BpmnXMLConverter bpmnXMLConverter=new BpmnXMLConverter();
        byte[] convertToXML = bpmnXMLConverter.convertToXML(bpmnModel);
        String xml=new String(convertToXML);
        xml = xml.replaceAll("&lt;","<").replaceAll("&gt;",">");
        return xml;
    }

    /**
     * 绘制节点
     * @param process bpmn process 图纸
     * @param node    json的节点
     * @param preId   上一节点id
     * @param endId   结束节点
     * @param preExpression 上一节点表达式
     */
    public void drawNode(Process process, ProcessNode node, String preId, String endId, String preExpression) {
        // 根据type绘制不同种类的节点
        Inout inout = drawNodeByType(process, node);
        // 绘制前一根线
        process.addFlowElement(createSequenceFlow(preId, inout.getIn(), preExpression));
        if (node.getNext() == null) {
            // 没有下一步, 绘制指向结束的线
            process.addFlowElement(createSequenceFlow(inout.getOut(), endId, null));
        } else {
            // 有下一步, 递归绘制下一个节点
            drawNode(process, node.getNext(), inout.getOut(), endId, null);
        }
    }

    /**
     * 绘制不同种类节点
     * @param process
     * @param node
     * @return
     */
    private Inout drawNodeByType(Process process, ProcessNode node) {
        if (node.getType().equals("审核节点")) {
            return drawAuditNode(process, node);
        } else if (node.getType().equals("分支节点")) {
            return drawExclusiveNode(process, node);
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * 绘制审核节点
     * @param process
     * @param node
     * @return
     */
    private Inout drawAuditNode(Process process, ProcessNode node) {
        // 绘制节点
        String id = "Node_"+UUID.randomUUID();
        UserTask userTask = new UserTask();
        userTask.setId(id);
        userTask.setName(node.getName());
        // 设置多实例
        userTask.setAssignee("${user}");
        MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = new MultiInstanceLoopCharacteristics();
        if (node.getAssignee().getMultiMode().equals("顺序审批")) {
            multiInstanceLoopCharacteristics.setSequential(true);
        }
        multiInstanceLoopCharacteristics.setElementVariable("user");
        // 完成条件
        multiInstanceLoopCharacteristics.setCompletionCondition("${nrOfInstances == nrOfCompletedInstances}");
        multiInstanceLoopCharacteristics.setInputDataItem("${users}");
        userTask.setLoopCharacteristics(multiInstanceLoopCharacteristics);
        // 保存json节点配置到扩展属性
        Map<String, Object> extensions = new HashMap<>();
        extensions.put("node", node);
        BpmnUtil.addExtensionProperty(userTask, extensions);
        return new Inout(id, id);
    }

    /**
     * 绘制分支节点
     * @param process
     * @param node
     * @return
     */
    private Inout drawExclusiveNode(Process process, ProcessNode node) {
        // 开始网关
        String startId = "Exclusive_"+UUID.randomUUID();
        ExclusiveGateway startGateway = new ExclusiveGateway();
        startGateway.setId(startId);
        process.addFlowElement(startGateway);
        // 结束网关
        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) {
                // 没有子流程,直接绘制结束线
                process.addFlowElement(createSequenceFlow(startId, endId, expression));
            } else {
                // 有子流程,递归绘制子流程
                drawNode(process, branch.getProcess(), startId, endId, expression);
            }
        }
        // int和out不一样
        return new Inout(startId, endId);
    }

    /**
     * 创建连线
     * @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;
    }
}

核心代码真的没几行,细节自己完善即可

我自己做了个相对复杂的json,转换为xml最终在bpmn.js展示效果如下

bpmn.js

功能都没大问题,就是自动布局的线有点扭曲


仿钉钉流程轻松实现JSON转BPMN

本文收录在
0评论

登录

忘记密码 ?

切换登录

注册