引子
在第02篇中,我通过 Flowable-UI 绘制了一个简单的绩效流程,并在后续章节中基于这个流程演示了 Flowable 的各种API调用。然而,在实际业务场景中,如果要求前端将用户绘制的流程文件发送给后端再进行解析处理,这种方式显得繁琐且不够优雅。那么,有没有更简便的方法,让前端通过常规的参数传递方式就能实现流程创建呢?
答案是肯定的。Flowable 提供了强大的 BpmnModel API,它允许我们以编程方式动态构建流程定义,无需依赖XML文件。
什么是 BpmnModel ?
BpmnModel 是 Flowable 提供的一个核心类,它允许开发者以编程方式构建完整的 BPMN 2.0 流程模型。与通过 Flowable-UI 设计流程并导出XML文件的方式不同,BpmnModel 让我们可以直接在代码中定义流程的各个元素。
所以,我们可以通过 BpmnModel 对象来动态创建流程定义,前端只需通过接口传递必要的流程参数即可,无需传递完整的 XML 文件。这里需要明确区分两个概念:一是后端如何接收参数并构建流程模型,二是前端如何提供流程设计的交互界面。对于后者,可以基于 bpmn.js 开发自定义设计器,也可以采用现有的开源方案,这些选项在第01篇中已有详细介绍。
代码编写
接下来,我将完全抛弃 Flowable UI 可视化建模方式,转而通过纯代码方式使用 BpmnModel 对象构建先前设计的绩效流程。让我们深入编码环节,一步步实现这一转换。
一、构建传参对象
1.流程定义实体
流程定义是创建流程的传参对象,包含流程的基本信息和节点结构。
java
import lombok.Data;
import java.io.Serializable;
/**
* 流程定义.
*/
@Data
public class ProcessDef implements Serializable {
/**
* 流程定义id.
*/
private String processDefKey;
/**
* 流程定义名称.
*/
private String processName;
/**
* 流程定义描述.
*/
private String description;
/**
* 创建人id.
*/
private Long creatorId;
/**
* 流程定义节点.
*/
private ProcessNode processNode;
}
2. 流程节点实体
流程节点定义了流程中的各个环节,包括节点类型、表单属性以及子节点关系等。
java
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 流程节点.
*/
@Data
public class ProcessNode implements Serializable {
/**
* 节点名称.
*/
private String name;
/**
* 节点类型.
*/
private NodeCategory category;
/**
* 是否启用 0否 1是.
*/
private Integer enable;
/**
* 办理人属性.
*/
private AssigneeProps assigneeProps;
/**
* 表单列表.
*/
private List<FormProp> formProps;
/**
* 子节点.
*/
private ProcessNode children;
}
3. 相关枚举和属性类
为了更好地定义流程节点的各种属性,我们最好还是定义相关枚举和辅助类。(Ps:在实际开发中,个人建议如果类型比较多的情况下,尽量使用枚举,避免魔法字段。)
节点类型枚举
java
/**
* 节点类型.
*/
public enum NodeCategory {
/**
* 自评.
*/
SELF_EVALUATION,
/**
* 上级评.
*/
LEADER_EVALUATION,
/**
* 隔级评.
*/
AUDIT,
/**
* 绩效确认.
*/
CONFIRMATION;
}
办理人属性
java
import java.io.Serializable;
import lombok.Data;
/**
* 办理人Props.
*/
@Data
public class AssigneeProps implements Serializable {
/**
* 办理人类型.
*/
private AssigneeType assigneeType;
/**
* 候选办理人类型.
*/
private AssigneeType candidateType;
}
办理人类型枚举
java
/**
* 办理人类型.
*/
public enum AssigneeType {
/**
* 节点处理人类型, 用户本人,直接上级,上级部门管理员,隔级上级,隔级部门管理员.
*/
USER_ASSESSED, LEADER, DEPT_MANAGER, SUPERIOR_LEADER, SUPERIOR_DEPT_MANAGER
}
表单属性
java
import java.io.Serializable;
import lombok.Data;
/**
* FormProps 表单属性.
*/
@Data
public class FormProp implements Serializable {
/**
* id.
*/
private String id;
/**
* 属性名称.
*/
private String name;
/**
* 属性变量.
*/
private String variable;
/**
* 变量类型.
*/
private String type;
/**
* 值.
*/
private String value;
/**
* 表单配置.
*/
private FormConfig config;
public FormConfig getConfig() {
return config;
}
}
表单配置
java
import java.io.Serializable;
import lombok.Data;
/**
* FormConfig 表单配置.
*/
@Data
public class FormConfig implements Serializable {
/**
* 分组名称.
*/
private String group;
/**
* 分组权重.
*/
private Double weight;
/**
* 分类:评分SCORE、评语COMMENT.
*/
private FormCategory category;
}
表单类别枚举
java
/**
* 表单类别.
*/
public enum FormCategory {
/**
* 评分.
*/
SCORE,
/**
* 评语.
*/
COMMENT;
}
二、创建控制器
java
import com.pitayafruit.base.BaseResponse;
import com.pitayafruit.rest.vo.ProcessDef;
import com.pitayafruit.service.ProcessDefService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 流程Controller.
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/process-def/v1")
public class ProcessDefControllerV1 {
private final ProcessDefService processDefService;
/**
* 创建流程模型并部署.
*
* @param processDef 流程定义
* @return 流程key
*/
@PostMapping
public ResponseEntity<Object> createProcessDef(@RequestBody ProcessDef processDef) {
//1,创建并部署流程模型
String processDefKey = processDefService.createProcessDefApi(processDef);
//2.返回流程模型key
return new ResponseEntity<>(new BaseResponse<>(processDefKey), HttpStatus.OK);
}
}
三、实现服务层主方法
这个负责将前端传递的数据结构转换为 Flowable 的 BpmnModel 模型,并进行部署。
java
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.pitayafruit.rest.vo.AssigneeProps;
import com.pitayafruit.rest.vo.ProcessDef;
import com.pitayafruit.rest.vo.ProcessNode;
import com.pitayafruit.service.ProcessDefService;
import com.pitayafruit.utils.UUIDUtil;
import lombok.RequiredArgsConstructor;
import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.Process;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.validation.ValidationError;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 流程定义ServiceImpl.
*/
@Service
@RequiredArgsConstructor
public class ProcessDefServiceImpl implements ProcessDefService {
private final RepositoryService repositoryService;
/**
* 创建流程模型并部署.
*
* @param processDef 流程定义
* @return 流程key
*/
@Override
public String createProcessDefApi(ProcessDef processDef) {
//1.设置流程定义Key
processDef.setProcessDefKey("processDef" + UUIDUtil.getUUID());
//2.设置流程定义创建人id
processDef.setCreatorId(1L);
//3.设置流程定义名称
processDef.setProcessName("绩效流程");
//4.创建流程模型
BpmnModel bpmnModel = toBpmn(processDef);
//5.部署流程模型
repositoryService.createDeployment()
//5-1.流程定义key
.key(processDef.getProcessDefKey())
//5-2.流程定义名称
.name(processDef.getProcessName())
//5-3.添加流程模型
.addBpmnModel(processDef.getProcessDefKey() + ".bpmn", bpmnModel)
//5-4.部署
.deploy();
//6.返回流程定义key
return processDef.getProcessDefKey();
}
四、流程模型转换方法
类型转换涉及到的逻辑比较多,所以把这个方法单独抽取出来。首先创建一个流程对象,设置流程的基本属性,添加开始事件,然后通过调用 buildTask
方法递归构建流程中的各个节点,最后生成并验证 BpmnModel。
java
/**
* 将流程定义转换为BpmnModel.
*
* @param processDef 流程定义
* @return Bpmn模型
*/
private BpmnModel toBpmn(ProcessDef processDef) {
//1.创建流程
//1-1.声明流程对象
Process process = new Process();
//1-2.设置流程id
process.setId(processDef.getProcessDefKey());
//1-3.设置流程名称
process.setName(processDef.getProcessName());
//2.创建开始事件
//2-1.声明开始事件对象
StartEvent startEvent = new StartEvent();
//2-2.设置开始事件id
startEvent.setId("startEvent" + UUIDUtil.getUUID());
//2-3.设置开始事件名称
startEvent.setName("开始");
//2-4.将开始事件添加到流程中
process.addFlowElement(startEvent);
//创建用户节点
ProcessNode processNode = processDef.getProcessNode();
buildTask(startEvent, processNode, process);
//创建流程模型
BpmnModel bpmnModel = new BpmnModel();
bpmnModel.addProcess(process);
// 验证BPMN模型
List<ValidationError> validationErrors = repositoryService.validateProcess(bpmnModel);
if (ObjectUtil.isNotEmpty(validationErrors)) {
//打印失败日志
validationErrors.forEach(validationError -> System.out.println(validationError.toString()));
throw new IllegalArgumentException("验证失败");
}
return bpmnModel;
}
五、任务节点构建方法
这个方法负责创建用户任务节点。根据传入的流程节点数据,创建一个用户任务对象,设置其属性,添加自定义属性和任务监听器,然后将任务添加到流程中,并构建与前一个节点的连线。如果当前节点未启用,则直接处理下一个节点。
java
/**
* 创建用户任务节点.
*
* @param parentTask 父节点
* @param processNode 流程节点
* @param process 流程定义
*/
private void buildTask(FlowNode parentTask, ProcessNode processNode, Process process) {
//如果节点启用,处理当前节点
if (ObjectUtil.isNotNull(processNode.getEnable()) && processNode.getEnable() == 1) {
UserTask userTask = new UserTask();
userTask.setId("userTask" + UUIDUtil.getUUID());
userTask.setName(processNode.getName());
//设置节点类型
userTask.setCategory(processNode.getCategory().toString());
List<CustomProperty> customProperties = new ArrayList<>();
//办理人属性
AssigneeProps assigneeProps = processNode.getAssigneeProps();
//设置办理人类型
customProperties.add(
buildCustomProperty("assigneeType", assigneeProps.getAssigneeType().toString()));
//设置候选办理人类型
if (ObjectUtil.isNotNull(assigneeProps.getCandidateType())) {
customProperties.add(buildCustomProperty("candidateType", assigneeProps.getCandidateType().toString()));
}
//绑定表单
userTask.setFormProperties(buildFormProperty(processNode));
//表单列表添加到节点扩展属性
customProperties.add(buildCustomProperty("formProps", JSON.toJSONString(processNode.getFormProps())));
//设置自定义属性,包括办理人类型和表单分组和权重配置
userTask.setCustomProperties(customProperties);
//监听器列表
List<FlowableListener> taskListeners = new ArrayList<>();
//任务创建时添加监听器,用于动态指定任务的办理人和设置变量
taskListeners.add(buildTaskListener(TaskListener.EVENTNAME_CREATE, "taskCreateListener"));
//任务完成后添加监听器,用于计算分数和保存表单
//如果是手动填写流程,添加手动填写的监听器
taskListeners.add(buildTaskListener(TaskListener.EVENTNAME_COMPLETE, "taskCompleteListener"));
//设置任务监听器
userTask.setTaskListeners(taskListeners);
// 将用户任务节点添加到流程定义
process.addFlowElement(userTask);
//添加流程连线
process.addFlowElement(new SequenceFlow(parentTask.getId(), userTask.getId()));
//解析下一个节点,参数为当前任务,当前流程节点,流程定义,流程类型
buildChildren(userTask, processNode, process);
} else {
//当前节点关闭的情况下,直接解析下一个节点,参数为父任务,当前流程节点,流程定义,流程类型
buildChildren(parentTask, processNode, process);
}
}
六、子节点处理方法
这个方法用于处理流程节点的子节点。它检查当前节点是否有子节点,如果有则递归调用 buildTask
方法构建子节点,如果没有则创建结束事件,表示流程的结束。
java
/**
* 解析子节点.
*
* @param parentTask 下一个节点的父任务
* @param processNode 流程节点
* @param process 流程定义
*/
private void buildChildren(FlowNode parentTask, ProcessNode processNode, Process process) {
ProcessNode childrenNode = processNode.getChildren();
if (ObjectUtil.isNotNull(childrenNode)) {
//创建子节点
buildTask(parentTask, childrenNode, process);
} else {
//创建结束事件
EndEvent endEvent = new EndEvent();
endEvent.setId("endEvent" + UUIDUtil.getUUID());
endEvent.setName("结束");
process.addFlowElement(endEvent);
//添加流程连线
process.addFlowElement(new SequenceFlow(parentTask.getId(), endEvent.getId()));
}
}
七、任务监听器创建方法
这个方法用于创建任务监听器。在Flowable中,任务监听器可以在任务的不同生命周期阶段触发特定逻辑,例如在任务创建时动态分配任务办理人,或在任务完成时处理表单数据。这里使用了委托表达式方式,将监听器的实现委托给Spring容器中的Bean。
java
/**
* 创建任务监听器,用于动态分配任务的办理人.
*
* @param event 事件
* @param beanName 类名
* @return 任务监听器
*/
private FlowableListener buildTaskListener(String event, String beanName) {
FlowableListener flowableListener = new FlowableListener();
flowableListener.setEvent(event);
flowableListener.setImplementationType("delegateExpression");
flowableListener.setImplementation("${" + beanName + "}");
return flowableListener;
}
八、表单属性创建方法
这个方法用于创建表单属性。它遍历流程节点中的表单属性列表,为每个表单属性创建一个Flowable的FormProperty对象,设置其ID、名称、变量名等属性,并将原始表单属性的ID和变量名更新为实际使用的值,以便在后续处理中使用。
java
/**
* 创建表单属性.
*
* @param processNode 流程节点
*/
private List<FormProperty> buildFormProperty(ProcessNode processNode) {
List<FormProperty> formProperties = new ArrayList<>();
if (ObjectUtil.isNull(processNode.getFormProps())) {
return formProperties;
}
processNode.getFormProps().forEach(prop -> {
//新建表单属性对象
FormProperty formProperty = new FormProperty();
//设置表单属性id
String id = "formProperty" + UUIDUtil.getUUID();
formProperty.setId(id);
//设置表单名称
formProperty.setName(prop.getName());
//设置表单变量名为表单id
formProperty.setVariable(id);
//设置表单变量类型
formProperty.setType(prop.getType());
//设置表单是否必填
formProperty.setRequired(true);
formProperties.add(formProperty);
//设置表单属性id
prop.setId(id);
prop.setVariable(id);
});
return formProperties;
}
九、自定义属性创建方法
这个方法用于创建自定义属性。在 Flowable 中,自定义属性可以用于存储不属于标准BPMN规范但业务上需要的信息,通常用来传递我们的业务数据。
java
/**
* 创建自定义属性.
*
* @param key 键
* @param value 值
* @return 自定义属性
*/
private CustomProperty buildCustomProperty(String key, String value) {
CustomProperty customProperty = new CustomProperty();
//自定义属性名称
customProperty.setName(key);
//自定义属性值
customProperty.setSimpleValue(value);
return customProperty;
}
接口测试
我把请求转换成了curl命令,方便直接在命令行中测试。创建的这个流程与前面篇章中介绍的结构一致,包含三个核心节点:员工自评、上级评价和隔级评价,每个节点都包含分数、评语等表单项,并且每个节点都设置了不同的权重。
为什么要加权重?这是因为在实际业务场景中,不同评价环节的重要性往往不同。
apl
curl -X POST 'http://localhost:8080/api/process-def/v1' \
-H 'Content-Type: application/json' \
-d '{
"processName": "通过bpmn model创建的绩效流程",
"description": "用于员工季度绩效评估",
"processNode": {
"name": "员工自评",
"category": "SELF_EVALUATION",
"enable": 1,
"assigneeProps": {
"assigneeType": "USER_ASSESSED"
},
"formProps": [
{
"name": "自评分数",
"type": "string",
"value": "0",
"config": {
"group": "绩效评分",
"weight": 0.3,
"category": "SCORE"
}
},
{
"name": "自我评价",
"type": "string",
"config": {
"group": "评语",
"weight": 0.3,
"category": "COMMENT"
}
}
],
"children": {
"name": "上级评价",
"category": "LEADER_EVALUATION",
"enable": 1,
"assigneeProps": {
"assigneeType": "LEADER"
},
"formProps": [
{
"name": "上级评分",
"type": "string",
"value": "0",
"config": {
"group": "绩效评分",
"weight": 0.5,
"category": "SCORE"
}
},
{
"name": "上级评语",
"type": "string",
"config": {
"group": "评语",
"weight": 0.5,
"category": "COMMENT"
}
}
],
"children": {
"name": "隔级评价",
"category": "AUDIT",
"enable": 1,
"assigneeProps": {
"assigneeType": "SUPERIOR_LEADER"
},
"formProps": [
{
"name": "隔级评分",
"type": "string",
"value": "0",
"config": {
"group": "绩效评分",
"weight": 0.2,
"category": "SCORE"
}
},
{
"name": "隔级评语",
"type": "string",
"config": {
"group": "评语",
"weight": 0.2,
"category": "COMMENT"
}
}
],
"children": null
}
}
}
}'
按照接口的定义,部署成功后会返回流程模型key。

同时查看部署表和流程运行表,可以看到这个流程已经启动了。


小结
通过本章的实践,我们可以明显看到利用 BpmnModel API 构建流程虽然灵活强大,但即使是构建一个相对简单的线性流程,也需要编写大量代码来处理各种细节。这种方法的复杂性在面对更复杂的流程结构(如并行网关、排他网关或子流程等)时会进一步增加。
因此,在实际项目中,我建议大家可以花点时间封装,比如可以将创建开始事件、任务节点、结束事件、网关等常见元素的代码封装成独立方法。通过这些封装,在写业务的时候就能像搭积木一样轻松组合各种流程元素,提高开发效率和代码可维护性。