写过工作流都会遇到这样的难题,希望流程的设计可以类似钉钉一样简单明了,而不是超级不有好的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写一个简版的
"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 创建一些实体来接受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) {
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);
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);
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();
如果想把BpmnModel转换为xml,也很简单,引入依赖
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-converter</artifactId>
<version>7.1.0.M1</version>
</dependency>
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) {
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();
BpmnXMLConverter bpmnXMLConverter=new BpmnXMLConverter();
byte[] convertToXML = bpmnXMLConverter.convertToXML(bpmnModel);
String xml=new String(convertToXML);
xml = xml.replaceAll("<","<").replaceAll(">",">");
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) {
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);
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);
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展示效果如下
功能都没大问题,就是自动布局的线有点扭曲
- 694
-
October_CanYang
Vue.js
- 420
-
烤奶不加冰_
Vue.js
bpmn-js
- 710
-
MiyueFE
JavaScript
TypeScript