对于流程设置不友好的问题,国内钉钉另行设计与实现了一套流程建模模式,跟bpmn规范无关,有人仿照实现了下,并做了开源(https://github.com/StavinLi/Workflow-Vue3),效果图如下:
实现大致原理是基于无限嵌套的子节点,输出json数据,传给后端,后端进行解析后,调用Camunda引擎的api,转换成流程模型后持久化。
上篇介绍了关键技术点模型转换技术方案,今天重点来说如何实现。
前端开源项目的建模,数据结构是一个无限嵌套的json对象,每一层都是一个节点实例,如下:
"nodeName": "发起人",//节点名称
"type": 0,// 0 发起人 1审批 2抄送 3条件 4路由
"priorityLevel": "",// 条件优先级
"settype": "",// 审批人设置 1指定成员 2主管 4发起人自选 5发起人自己 7连续多级主管
"selectMode": "", //审批人数 1选一个人 2选多个人
"selectRange": "", //选择范围 1.全公司 2指定成员 2指定角色
"directorLevel": "", //审批终点 最高层主管数
"examineMode": "", //多人审批时采用的审批方式 1依次审批 2会签
"noHanderAction": "",//审批人为空时 1自动审批通过/不允许发起 2转交给审核管理员
"examineEndDirectorLevel": "", //审批终点 第n层主管
"ccSelfSelectFlag": "", //允许发起人自选抄送人
"conditionList": [], //当审批单同时满足以下条件时进入此流程
"nodeUserList": [], //操作人
"childNode":{} //子节点
传给后端是一个json字符串,使用FastJson解析,映射到对象,方便后面读取属性等处理,因此新建一个名为FlowNode的类,如下:
package tech.abc.platform.workflow.entity;
import lombok.Data;
/**
* 流程节点
*
* @author wqliu
* @date 2023-07-12
*/
@Data
public class FlowNode {
/**
* 名称
*/
private String nodeName;
/**
* 子节点
*/
private FlowNode childNode;
}
节点属性暂时先设置两个核心属性,一个是nodeName,节点名称,另外一个是childNode,子节点。这里之所以没有一次性把所有属性都设置上去,是因为想通过迭代方式,逐步完善功能,不会全盘使用现有的数据定义,会根据实际需求增删及重命名,以及必要时调整数据结构。
将json转换为模型,不是一件简单的事情,需要大量的调试与测试。如直接内嵌于服务类中,则每次调试都要加载SpringBoot,效率低,耗时长。
因此在模块workflow中建了一个测试类,将转换的功能代码先直接写到测试类中,快速启动与验证。待转换功能开发完成或大部分完成后,再迁移回服务类中进行集成。
package tech.abc.platform.workflow.service.impl;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import tech.abc.platform.workflow.entity.FlowNode;
/**
* @author wqliu
* @date 2023-07-12
*/
@Slf4j
class FlowTemplateServiceImplTest {
@Test
void convertJsonToModel() {
String json="{\n" +
" \"nodeName\": \"发起人\",\n" +
" \"type\": 0,\n" +
" \"priorityLevel\": \"\",\n" +
" \"settype\": \"\",\n" +
" \"selectMode\": \"\",\n" +
" \"selectRange\": \"\",\n" +
" \"directorLevel\": \"\",\n" +
" \"examineMode\": \"\",\n" +
" \"noHanderAction\": \"\",\n" +
" \"examineEndDirectorLevel\": \"\",\n" +
" \"ccSelfSelectFlag\": \"\",\n" +
" \"conditionList\": [\n" +
" \n" +
" ],\n" +
" \"nodeUserList\": [\n" +
" \n" +
" ],\n" +
" \"childNode\": {\n" +
" \"nodeName\": \"审核人\",\n" +
" \"error\": false,\n" +
" \"type\": 1,\n" +
" \"settype\": 2,\n" +
" \"selectMode\": 0,\n" +
" \"selectRange\": 0,\n" +
" \"directorLevel\": 1,\n" +
" \"examineMode\": 1,\n" +
" \"noHanderAction\": 2,\n" +
" \"examineEndDirectorLevel\": 0,\n" +
" \"childNode\": {\n" +
" \"nodeName\": \"抄送人\",\n" +
" \"type\": 2,\n" +
" \"ccSelfSelectFlag\": 1,\n" +
" \"childNode\": null,\n" +
" \"nodeUserList\": [\n" +
" \n" +
" ],\n" +
" \"error\": false\n" +
" },\n" +
" \"nodeUserList\": [\n" +
" \n" +
" ]\n" +
" },\n" +
" \"conditionNodes\": [\n" +
" \n" +
" ]\n" +
"}";
// 解析json,反序列化为节点对象
FlowNode flowNode = JSON.parseObject(json, FlowNode.class);
log.info("json数据:{}",JSON.toJSONString(flowNode));
}
}
为方便开发,使用的模型为一个三节点的简单流程
json字符串也直接固化在测试类中了,运行,控制台输出如下:
{
"childNode": {
"childNode": {
"nodeName": "抄送人"
},
"nodeName": "审核人"
},
"nodeName": "发起人"
}
符合预期输出,fastjson将字符串映射到java对象时,会自动忽略未定义的属性,并不会报错。
上面两步实际都是准备工作,接下来进行真正的功能开发。
节点有不同的类型,如开始、结束、分支、用户任务、服务任务等。不同的节点类型,处理是不一样的,转换时也同理。
节点类型是一个重要的属性,原开源项目前端定义如下: 0 发起人 1审批 2抄送 3条件 4路由,存在几个问题:
1.类型与后端工作流Camunda的概念不一致
2.数量较少,可视为仅是示例,后面还需要扩展
3.使用数字0、1、2,可读性差
基于上述问题,对节点类型进行了重定义,新建枚举类型类,如下:
package tech.abc.platform.workflow.enums;
import lombok.Getter;
/**
* 流程节点类型
*
* @author wqliu
* @date 2023-07-12
*/
@Getter
public enum FlowCodeTypeEnum {
/**
* 开始环节
*/
START,
/**
* 用户任务
*/
USER_TASK,
/**
* 服务任务
*/
SERVICE_TASK,
/**
* 结束环节
*/
END,
;
}
同样采用迭代方式,先建一个三环节简单审批流程使用到的类型,后面逐步完善功能。
前端传来的流程节点,实际是流程的主体,并不包括开始环节和结束环节。这样做其实也挺好的,聚焦流程主体,后端首先做下框架工作,使用Camunda提供的流畅API,把流程建立起来。
// 解析json,反序列化为节点对象
FlowNode flowNode = JSON.parseObject(json, FlowNode.class);
log.info("json数据:{}",JSON.toJSONString(flowNode));
// 新建流程
ProcessBuilder processBuilder = Bpmn.createProcess()
.name("请假流程")
.executable();
// 开始环节
StartEventBuilder startEventBuilder = processBuilder.startEvent().name("流程开始");
// TODO 添加流程主体
// 添加结束环节
EndEventBuilder endEventBuilder = startEventBuilder.endEvent().name("流程结束");
// 构建模型
BpmnModelInstance modelInstance =endEventBuilder.done();
// 验证模型
Bpmn.validateModel(modelInstance);
// 转换为xml
String xmlString = Bpmn.convertToString(modelInstance);
log.info(xmlString);
后端自动创建头尾,中间流程主体待处理,执行测试,模型验证通过,转换为模型的xml如下:
<definitions xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="definitions_53f2ba72-be03-45dd-9175-928a8a447a2c" targetNamespace="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">
<process id="process_e6ea25a6-2b09-4b5e-90dd-f0a11032a3bf" isExecutable="true" name="请假流程">
<startEvent id="startEvent_70613787-3877-46ba-a372-ac51189cd8e3" name="流程开始">
<outgoing>sequenceFlow_8fe2b87b-3c93-4482-b24a-677a06535253outgoing>
startEvent>
<endEvent id="endEvent_38e23a33-beee-46a3-9140-c01bc0589a7b" name="流程结束">
<incoming>sequenceFlow_8fe2b87b-3c93-4482-b24a-677a06535253incoming>
endEvent>
<sequenceFlow id="sequenceFlow_8fe2b87b-3c93-4482-b24a-677a06535253" sourceRef="startEvent_70613787-3877-46ba-a372-ac51189cd8e3" targetRef="endEvent_38e23a33-beee-46a3-9140-c01bc0589a7b"/>
process>
<bpmndi:BPMNDiagram id="BPMNDiagram_b0291ab8-3af6-41ea-bb62-6ac25f8a9a6c">
<bpmndi:BPMNPlane bpmnElement="process_e6ea25a6-2b09-4b5e-90dd-f0a11032a3bf" id="BPMNPlane_ac95d669-a321-4a73-ae59-2d7c060a5440">
<bpmndi:BPMNShape bpmnElement="startEvent_70613787-3877-46ba-a372-ac51189cd8e3" id="BPMNShape_a148e0aa-8760-4d98-8ebb-6787a7d81754">
<dc:Bounds height="36.0" width="36.0" x="100.0" y="100.0"/>
bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endEvent_38e23a33-beee-46a3-9140-c01bc0589a7b" id="BPMNShape_d79d35ef-7fd7-421f-920b-5cfecaa3ee32">
<dc:Bounds height="36.0" width="36.0" x="186.0" y="100.0"/>
bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sequenceFlow_8fe2b87b-3c93-4482-b24a-677a06535253" id="BPMNEdge_ee90e10c-89ee-44cc-bafc-b209569e1376">
<di:waypoint x="136.0" y="118.0"/>
<di:waypoint x="186.0" y="118.0"/>
bpmndi:BPMNEdge>
bpmndi:BPMNPlane>
bpmndi:BPMNDiagram>
definitions>
接下来,就处理最核心的部分,把前端传来的json数据,转换为camunda的模型。
这时候,就发现流畅API能力不足了,需要依托标准API,来创建节点,以及绘制节点之间的连接边。同时,数据机构是节点层级嵌套的,使用到了递归。
package tech.abc.platform.workflow.service.impl;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.EnumUtils;
import org.apache.poi.ss.formula.functions.T;
import org.camunda.bpm.model.bpmn.Bpmn;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;
import org.camunda.bpm.model.bpmn.builder.EndEventBuilder;
import org.camunda.bpm.model.bpmn.builder.ProcessBuilder;
import org.camunda.bpm.model.bpmn.builder.StartEventBuilder;
import org.camunda.bpm.model.bpmn.builder.UserTaskBuilder;
import org.camunda.bpm.model.bpmn.instance.*;
import org.camunda.bpm.model.bpmn.instance.Process;
import org.junit.jupiter.api.Test;
import tech.abc.platform.workflow.entity.MyFlowNode;
import tech.abc.platform.workflow.enums.FlowCodeTypeEnum;
import java.util.UUID;
/**
* @author wqliu
* @date 2023-07-12
*/
@Slf4j
class FlowTemplateServiceImplTest {
@Test
void convertJsonToModel() {
String json="{\n" +
" \"nodeName\": \"发起人\",\n" +
" \"type\": \"USER_TASK\",\n" +
" \"priorityLevel\": \"\",\n" +
" \"settype\": \"\",\n" +
" \"selectMode\": \"\",\n" +
" \"selectRange\": \"\",\n" +
" \"directorLevel\": \"\",\n" +
" \"examineMode\": \"\",\n" +
" \"noHanderAction\": \"\",\n" +
" \"examineEndDirectorLevel\": \"\",\n" +
" \"ccSelfSelectFlag\": \"\",\n" +
" \"conditionList\": [\n" +
" \n" +
" ],\n" +
" \"nodeUserList\": [\n" +
" \n" +
" ],\n" +
" \"childNode\": {\n" +
" \"nodeName\": \"审核人\",\n" +
" \"error\": false,\n" +
" \"type\": \"USER_TASK\",\n" +
" \"settype\": 2,\n" +
" \"selectMode\": 0,\n" +
" \"selectRange\": 0,\n" +
" \"directorLevel\": 1,\n" +
" \"examineMode\": 1,\n" +
" \"noHanderAction\": 2,\n" +
" \"examineEndDirectorLevel\": 0,\n" +
" \"nodeUserList\": [\n" +
" \n" +
" ]\n" +
" },\n" +
" \"conditionNodes\": [\n" +
" \n" +
" ]\n" +
"}";
// 解析json,反序列化为节点对象
MyFlowNode flowNode = JSON.parseObject(json, MyFlowNode.class);
log.info("json数据:{}",JSON.toJSONString(flowNode));
// 新建流程
ProcessBuilder processBuilder = Bpmn.createProcess()
.name("请假流程")
.executable();
Process process = processBuilder.getElement();
// 开始环节
StartEventBuilder startEventBuilder = processBuilder.startEvent().name("流程开始");
// 流程节点转换
// 流程发起,固化为用户任务
MyFlowNode firstNode=flowNode;
UserTaskBuilder userTaskBuilder = startEventBuilder.userTask().name(flowNode.getNodeName());
// 其他节点
if(firstNode.getChildNode()!=null){
// 存在子节点,处理子节点
createElement(process,userTaskBuilder.getElement(),firstNode.getChildNode());
}else{
//无子节点,附加结束节点
appendEndNode(process, userTaskBuilder.getElement());
}
// 构建模型
BpmnModelInstance modelInstance =userTaskBuilder.done();
// 验证模型
Bpmn.validateModel(modelInstance);
// 转换为xml
String xmlString = Bpmn.convertToString(modelInstance);
log.info(xmlString);
}
/**
* 创建元素
*
* @param process 流程
* @param parentElement 父元素
* @param flowNode 流程节点
*/
private void createElement(Process process,FlowNode parentElement,
MyFlowNode flowNode) {
// 构建节点
FlowNode element=null;
FlowCodeTypeEnum type = EnumUtils.getEnum(FlowCodeTypeEnum.class, flowNode.getType());
switch (type){
case USER_TASK:
element = process.getModelInstance().newInstance(UserTask.class);
element.setAttributeValue("name", flowNode.getNodeName());
break;
case SERVICE_TASK:
element = process.getModelInstance().newInstance(ServiceTask.class);
break;
default:
log.error("未找到合适的类型");
}
element.setAttributeValue("id", UUID.randomUUID().toString(), true);
process.addChildElement(element);
// 构建边
createSequenceFlow(process, parentElement, element);
//递归处理子节点
if(flowNode.getChildNode()!=null){
createElement(process,element,flowNode.getChildNode());
}else{
//附加结束节点
appendEndNode(process, element);
}
}
/**
* 创建元素
*
* @param parentElement 父元素
* @param id 标识
* @param elementClass 元素类别
* @return {@link T}
*/
private <T extends BpmnModelElementInstance> T createElement(BpmnModelElementInstance parentElement,
String id, Class<T> elementClass) {
T element = parentElement.getModelInstance().newInstance(elementClass);
element.setAttributeValue("id", id, true);
parentElement.addChildElement(element);
return element;
}
/**
* 创建序列流
*
* @param process 流程
* @param from 起始节点
* @param to 终止节点
* @return {@link SequenceFlow}
*/
private SequenceFlow createSequenceFlow(Process process, FlowNode from, FlowNode to) {
String identifier = from.getId() + "-" + to.getId();
SequenceFlow sequenceFlow = createElement(process,identifier, SequenceFlow.class);
process.addChildElement(sequenceFlow);
sequenceFlow.setSource(from);
from.getOutgoing().add(sequenceFlow);
sequenceFlow.setTarget(to);
to.getIncoming().add(sequenceFlow);
return sequenceFlow;
}
/**
* 附加结束节点
*
* @param process 流程
* @param flowNode 流程节点
*/
private void appendEndNode(Process process, FlowNode flowNode) {
EndEvent endEvent = process.getModelInstance().newInstance(EndEvent.class);
endEvent.setAttributeValue("name", "流程结束");
process.addChildElement(endEvent);
createSequenceFlow(process,flowNode, endEvent);
}
}
处理结果如下:
<definitions xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="definitions_3e6f9f0d-132b-4e4e-9274-0ef7faf59fa6" targetNamespace="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">
<process id="process_a9d3ae2d-b794-4793-b1f8-db8b52e5ddaf" isExecutable="true" name="请假流程">
<startEvent id="startEvent_f665528a-f446-41ef-ad03-ea138e83b64a" name="流程开始">
<outgoing>sequenceFlow_a8e1d8e3-e204-4cf5-9bb2-1a80bf1c3594outgoing>
startEvent>
<userTask id="userTask_7f6639a1-bdf0-4451-b52e-8dad049c9d64" name="发起人">
<incoming>sequenceFlow_a8e1d8e3-e204-4cf5-9bb2-1a80bf1c3594incoming>
<outgoing>userTask_7f6639a1-bdf0-4451-b52e-8dad049c9d64-a58d3872-cb1e-4f54-8de7-396ecf169b90outgoing>
userTask>
<sequenceFlow id="sequenceFlow_a8e1d8e3-e204-4cf5-9bb2-1a80bf1c3594" sourceRef="startEvent_f665528a-f446-41ef-ad03-ea138e83b64a" targetRef="userTask_7f6639a1-bdf0-4451-b52e-8dad049c9d64"/>
<userTask id="a58d3872-cb1e-4f54-8de7-396ecf169b90">
<incoming>userTask_7f6639a1-bdf0-4451-b52e-8dad049c9d64-a58d3872-cb1e-4f54-8de7-396ecf169b90incoming>
<outgoing>a58d3872-cb1e-4f54-8de7-396ecf169b90-endEvent_882384ce-03da-4f79-ab1a-770bb3b28bc0outgoing>
userTask>
<sequenceFlow id="userTask_7f6639a1-bdf0-4451-b52e-8dad049c9d64-a58d3872-cb1e-4f54-8de7-396ecf169b90" sourceRef="userTask_7f6639a1-bdf0-4451-b52e-8dad049c9d64" targetRef="a58d3872-cb1e-4f54-8de7-396ecf169b90"/>
<endEvent id="endEvent_882384ce-03da-4f79-ab1a-770bb3b28bc0">
<incoming>a58d3872-cb1e-4f54-8de7-396ecf169b90-endEvent_882384ce-03da-4f79-ab1a-770bb3b28bc0incoming>
endEvent>
<sequenceFlow id="a58d3872-cb1e-4f54-8de7-396ecf169b90-endEvent_882384ce-03da-4f79-ab1a-770bb3b28bc0" sourceRef="a58d3872-cb1e-4f54-8de7-396ecf169b90" targetRef="endEvent_882384ce-03da-4f79-ab1a-770bb3b28bc0"/>
process>
<bpmndi:BPMNDiagram id="BPMNDiagram_6248d292-31ca-4119-a3b8-0c8286fb4c39">
<bpmndi:BPMNPlane bpmnElement="process_a9d3ae2d-b794-4793-b1f8-db8b52e5ddaf" id="BPMNPlane_89c9d599-07cb-4838-addf-d18408cf1052">
<bpmndi:BPMNShape bpmnElement="startEvent_f665528a-f446-41ef-ad03-ea138e83b64a" id="BPMNShape_7070673c-6eeb-4315-8b5e-e58911ddbe0e">
<dc:Bounds height="36.0" width="36.0" x="100.0" y="100.0"/>
bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="userTask_7f6639a1-bdf0-4451-b52e-8dad049c9d64" id="BPMNShape_5c5a851b-220f-4228-a26f-34168d9b3059">
<dc:Bounds height="80.0" width="100.0" x="186.0" y="78.0"/>
bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sequenceFlow_a8e1d8e3-e204-4cf5-9bb2-1a80bf1c3594" id="BPMNEdge_d24ed1bc-d7b0-4dcc-940c-09db8ca2523f">
<di:waypoint x="136.0" y="118.0"/>
<di:waypoint x="186.0" y="118.0"/>
bpmndi:BPMNEdge>
bpmndi:BPMNPlane>
bpmndi:BPMNDiagram>
definitions>
这一步遇到了一个问题,有大约30%几率会报错,如下:
org.camunda.bpm.model.xml.ModelValidationException: DOM document is not valid
at org.camunda.bpm.model.xml.impl.parser.AbstractModelParser.validateModel(AbstractModelParser.java:170)
at org.camunda.bpm.model.bpmn.Bpmn.doValidateModel(Bpmn.java:281)
at org.camunda.bpm.model.bpmn.Bpmn.validateModel(Bpmn.java:179)
at tech.abc.platform.workflow.service.impl.FlowTemplateServiceImplTest.convertJsonToModel(FlowTemplateServiceImplTest.java:169)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:212)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:208)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.util.ArrayList.forEach(ArrayList.java:1259)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.util.ArrayList.forEach(ArrayList.java:1259)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.xml.sax.SAXParseException; cvc-datatype-valid.1.2.1: '860fc4aa-45bc-484c-80dc-607470ae930f' 不是 'NCName' 的有效值。
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:204)
at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.error(ErrorHandlerWrapper.java:135)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:395)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:326)
at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:283)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator$XSIErrorReporter.reportError(XMLSchemaValidator.java:453)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.reportSchemaError(XMLSchemaValidator.java:3231)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.processOneAttribute(XMLSchemaValidator.java:2826)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.processAttributes(XMLSchemaValidator.java:2763)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.handleStartElement(XMLSchemaValidator.java:2051)
at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.startElement(XMLSchemaValidator.java:741)
at com.sun.org.apache.xerces.internal.jaxp.validation.DOMValidatorHelper.beginNode(DOMValidatorHelper.java:277)
at com.sun.org.apache.xerces.internal.jaxp.validation.DOMValidatorHelper.validate(DOMValidatorHelper.java:244)
at com.sun.org.apache.xerces.internal.jaxp.validation.DOMValidatorHelper.validate(DOMValidatorHelper.java:190)
at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorImpl.validate(ValidatorImpl.java:108)
at javax.xml.validation.Validator.validate(Validator.java:124)
at org.camunda.bpm.model.xml.impl.parser.AbstractModelParser.validateModel(AbstractModelParser.java:165)
... 68 more
Process finished with exit code -1
核心错误是这里
Caused by: org.xml.sax.SAXParseException; cvc-datatype-valid.1.2.1: '860fc4aa-45bc-484c-80dc-607470ae930f' 不是 'NCName' 的有效值。
提示有点怪,但是分析一下,推测原因是我们使用uuid作为节点元素的id,如果生成的uuid刚好以数字开头,那么就不符合标识符必须以字母或下划线起始的规则了。这也简单,id前面拼点字母上去就好了,这里我加了一个node开头。当前聚焦在流程建模上,本来使用uuid只是临时方案,后面会用雪花算法替代掉。
element.setAttributeValue("id", "node"+UUID.randomUUID().toString(), true);
经过上面调整,多次测试,100%成功通过模型验证,转换输出了xml,不再出现报错情况。
代码调试通了,回顾下,可以把流程首环节,也拿到递归方法里去,不需要单独处理,重构如下:
// 解析json,反序列化为节点对象
MyFlowNode flowNode = JSON.parseObject(json, MyFlowNode.class);
log.info("json数据:{}",JSON.toJSONString(flowNode));
// 新建流程
ProcessBuilder processBuilder = Bpmn.createProcess()
.name("请假流程")
.executable();
Process process = processBuilder.getElement();
// 开始环节
StartEventBuilder startEventBuilder = processBuilder.startEvent().name("流程开始");
// 流程节点转换
convertJsonToModel(process,startEventBuilder.getElement(),flowNode);
// 构建模型
BpmnModelInstance modelInstance =startEventBuilder.done();
// 验证模型
Bpmn.validateModel(modelInstance);
// 转换为xml
String xmlString = Bpmn.convertToString(modelInstance);
log.info(xmlString);
平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
开源不易,欢迎收藏、点赞、评论。