引子
在第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.流程定義實體
流程定義是創建流程的傳參對象,包含流程的基本信息和節點結構。
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. 流程節點實體
流程節點定義了流程中的各個環節,包括節點類型、表單屬性以及子節點關系等。
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:在實際開發中,個人建議如果類型比較多的情況下,盡量使用枚舉,避免魔法字段。)
節點類型枚舉
/*** 節點類型.*/
public enum NodeCategory {/*** 自評.*/SELF_EVALUATION,/*** 上級評.*/LEADER_EVALUATION,/*** 隔級評.*/AUDIT,/*** 績效確認.*/CONFIRMATION;
}
辦理人屬性
import java.io.Serializable;
import lombok.Data;/*** 辦理人Props.*/
@Data
public class AssigneeProps implements Serializable {/*** 辦理人類型.*/private AssigneeType assigneeType;/*** 候選辦理人類型.*/private AssigneeType candidateType;
}
辦理人類型枚舉
/*** 辦理人類型.*/
public enum AssigneeType {/*** 節點處理人類型, 用戶本人,直接上級,上級部門管理員,隔級上級,隔級部門管理員.*/USER_ASSESSED, LEADER, DEPT_MANAGER, SUPERIOR_LEADER, SUPERIOR_DEPT_MANAGER
}
表單屬性
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;}
}
表單配置
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;}
表單類別枚舉
/*** 表單類別.*/
public enum FormCategory {/*** 評分.*/SCORE,/*** 評語.*/COMMENT;
}
二、創建控制器
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*/@PostMappingpublic ResponseEntity<Object> createProcessDef(@RequestBody ProcessDef processDef) {//1,創建并部署流程模型String processDefKey = processDefService.createProcessDefApi(processDef);//2.返回流程模型keyreturn new ResponseEntity<>(new BaseResponse<>(processDefKey), HttpStatus.OK);}}
三、實現服務層主方法
這個負責將前端傳遞的數據結構轉換為 Flowable 的 BpmnModel 模型,并進行部署。
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*/@Overridepublic String createProcessDefApi(ProcessDef processDef) {//1.設置流程定義KeyprocessDef.setProcessDefKey("processDef" + UUIDUtil.getUUID());//2.設置流程定義創建人idprocessDef.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.返回流程定義keyreturn processDef.getProcessDefKey();}
四、流程模型轉換方法
類型轉換涉及到的邏輯比較多,所以把這個方法單獨抽取出來。首先創建一個流程對象,設置流程的基本屬性,添加開始事件,然后通過調用 buildTask
方法遞歸構建流程中的各個節點,最后生成并驗證 BpmnModel。
/*** 將流程定義轉換為BpmnModel.** @param processDef 流程定義* @return Bpmn模型*/
private BpmnModel toBpmn(ProcessDef processDef) {//1.創建流程//1-1.聲明流程對象Process process = new Process();//1-2.設置流程idprocess.setId(processDef.getProcessDefKey());//1-3.設置流程名稱process.setName(processDef.getProcessName());//2.創建開始事件//2-1.聲明開始事件對象StartEvent startEvent = new StartEvent();//2-2.設置開始事件idstartEvent.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;
}
五、任務節點構建方法
這個方法負責創建用戶任務節點。根據傳入的流程節點數據,創建一個用戶任務對象,設置其屬性,添加自定義屬性和任務監聽器,然后將任務添加到流程中,并構建與前一個節點的連線。如果當前節點未啟用,則直接處理下一個節點。
/*** 創建用戶任務節點.** @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
方法構建子節點,如果沒有則創建結束事件,表示流程的結束。
/*** 解析子節點.** @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。
/*** 創建任務監聽器,用于動態分配任務的辦理人.** @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和變量名更新為實際使用的值,以便在后續處理中使用。
/*** 創建表單屬性.** @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();//設置表單屬性idString id = "formProperty" + UUIDUtil.getUUID();formProperty.setId(id);//設置表單名稱formProperty.setName(prop.getName());//設置表單變量名為表單idformProperty.setVariable(id);//設置表單變量類型formProperty.setType(prop.getType());//設置表單是否必填formProperty.setRequired(true);formProperties.add(formProperty);//設置表單屬性idprop.setId(id);prop.setVariable(id);});return formProperties;
}
九、自定義屬性創建方法
這個方法用于創建自定義屬性。在 Flowable 中,自定義屬性可以用于存儲不屬于標準BPMN規范但業務上需要的信息,通常用來傳遞我們的業務數據。
/*** 創建自定義屬性.** @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命令,方便直接在命令行中測試。創建的這個流程與前面篇章中介紹的結構一致,包含三個核心節點:員工自評、上級評價和隔級評價,每個節點都包含分數、評語等表單項,并且每個節點都設置了不同的權重。
為什么要加權重?這是因為在實際業務場景中,不同評價環節的重要性往往不同。
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 構建流程雖然靈活強大,但即使是構建一個相對簡單的線性流程,也需要編寫大量代碼來處理各種細節。這種方法的復雜性在面對更復雜的流程結構(如并行網關、排他網關或子流程等)時會進一步增加。
因此,在實際項目中,我建議大家可以花點時間封裝,比如可以將創建開始事件、任務節點、結束事件、網關等常見元素的代碼封裝成獨立方法。通過這些封裝,在寫業務的時候就能像搭積木一樣輕松組合各種流程元素,提高開發效率和代碼可維護性。