上周接手了一个内部审批系统的改造,原本以为只是简单的CRUD增删改查,结果发现核心逻辑全卡在“流程”上。一个请假申请,从提交到审批通过,中间要经过组长、经理、HR等多个节点,每个节点都可能通过、驳回或转交。更麻烦的是,不同的申请类型(如报销、采购)流程还不一样。如果全靠硬编码if-else来串联状态,代码很快就会变成一团乱麻,而且每次业务调整流程,开发都要跟着改代码、发版本。
这就是工作流引擎要解决的问题:把“业务流程”从具体的业务代码中抽离出来,变成一个可以独立设计、可视化配置、动态调整的“引擎”。而bpmn-js作为基于 BPMN 2.0 标准的 Web 流程编辑器,则是连接业务人员和开发者的桥梁——让非技术人员也能通过拖拽画出流程图,并直接生成引擎可执行的流程定义文件。
很多人一听到“集成工作流引擎”,就觉得是架构升级的大工程,下意识想找现成的 SaaS 或低代码平台。但对于很多已有成熟业务系统、只是需要将部分模块流程化的团队来说,在 Spring Boot 项目中嵌入一个轻量级引擎(如 Flowable、Activiti)并搭配前端编辑器,往往是更可控、成本也更低的方案。今天,我们就来彻底拆解这个方案的“上集”:如何将一个可视化的流程设计器(bpmn-js)无缝集成到你的 Spring Boot 后台服务中,并为后续的引擎驱动打下坚实基础。
1. 为什么是“流程编辑器”先行,而不是先写后端逻辑?
在集成工作流时,一个常见的误区是:先埋头研究 Flowable 或 Activiti 的 Java API,写一堆RuntimeService、TaskService的调用代码,试图用程序逻辑去“拼凑”出一个流程。这相当于还没画图纸就开始砌墙,很容易导致前后端认知不一致,流程逻辑散落在代码各处,难以维护。
更合理的路径是“设计驱动开发”:
- 先定义流程:业务方、产品经理和开发一起,使用可视化工具明确流程的节点、路径、审批人、表单和规则。
- 再实现引擎:将设计好的流程定义文件部署到引擎中,引擎负责驱动流程实例的流转。
- 最后对接业务:开发具体的业务接口(如提交申请、审批任务),这些接口内部调用引擎的 API。
bpmn-js扮演的就是第一步中的“设计工具”角色。它是一个基于 BPMN 2.0 标准的 JavaScript 库,能让你在浏览器里画出专业的流程图(就像 Visio 或 ProcessOn),并且这个图背后是标准的 XML 文件(.bpmn 或 .bpmn20.xml)。这个 XML 文件就是工作流引擎(如 Flowable)能直接“读懂”并执行的“源代码”。
所以,集成 bpmn-js 的本质,是为你的 Spring Boot 应用添加一个“流程设计中心”。这个中心负责流程的创建、编辑、保存和版本管理。后续引擎集成时,只需要从这个中心获取流程定义文件进行部署即可。
2. 理解核心:BPMN 2.0 标准与 bpmn-js 的定位
在动手之前,需要先建立两个关键认知:
BPMN 2.0 (Business Process Model and Notation):这是一套由 OMG 组织维护的、描述业务流程的全球通用标准。它定义了一套丰富的图形元素(如事件、活动、网关、顺序流)和对应的 XML 模式(XSD)。它的最大价值在于“可视化与可执行性统一”。你用 BPMN 画出的图,不仅能给人看,还能被符合标准的引擎(Flowable, Activiti, Camunda等)直接解析和执行。这就消除了流程图和实际代码之间的“翻译”成本。
bpmn-js:它是 Camunda 公司(也是 Flowable 项目的重要贡献者)开源的一个工具包,用于在 Web 应用中渲染和编辑 BPMN 2.0 图表。你可以把它理解为一个“BPMN 的富文本编辑器”。它不关心你的后端是 Java 还是 Python,也不关心你用哪个工作流引擎。它只负责两件事:
- 将 BPMN XML 渲染成可交互的流程图。
- 将用户在界面上的拖拽操作,同步更新到底层的 BPMN XML。
因此,我们的集成目标非常清晰:在 Spring Boot 后端提供一个文件存储和管理的服务,在前端 Vue/React 页面中嵌入 bpmn-js 编辑器,并实现前后端关于 BPMN XML 文件的增删改查同步。
3. 环境搭建与基础集成:从前端编辑器到后端文件服务
假设我们有一个基础的 Spring Boot 2.7 + Vue 3 的前后端分离项目。集成 bpmn-js 主要在前端完成,后端主要负责模型文件的持久化。
3.1 前端:嵌入 bpmn-js 编辑器
首先,在前端项目中安装 bpmn-js 及其相关依赖:
# 在你的 Vue/React 项目目录下 npm install bpmn-js bpmn-js-properties-panel camunda-bpmn-moddle --savebpmn-js: 核心编辑器库。bpmn-js-properties-panel: 右侧属性面板,用于编辑选中元素的属性(如任务名称、办理人表达式)。camunda-bpmn-moddle: 扩展包,使编辑器支持 Flowable/Activiti/Camunda 等引擎的扩展属性(如flowable:assignee,flowable:candidateUsers)。
接下来,创建一个流程设计器组件(如BpmnModeler.vue):
<template> <div class="container"> <div class="header"> <el-button @click="handleCreateNew">新建</el-button> <el-button @click="handleSave">保存</el-button> <el-button @click="handleDeploy">部署</el-button> <el-select v-model="currentModelId" placeholder="选择流程模型" @change="loadModel"> <el-option v-for="model in modelList" :key="model.id" :label="model.name" :value="model.id" /> </el-select> </div> <div class="content"> <div class="canvas" ref="canvas"></div> <div class="properties-panel" id="js-properties-panel"></div> </div> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount } from 'vue'; import BpmnModeler from 'bpmn-js/lib/Modeler'; import propertiesPanelModule from 'bpmn-js-properties-panel'; import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'; import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json'; import axios from 'axios'; const canvas = ref(null); const currentModelId = ref(''); const modelList = ref([]); let bpmnModeler = null; // 初始化编辑器 const initBpmnModeler = () => { bpmnModeler = new BpmnModeler({ container: canvas.value, propertiesPanel: { parent: '#js-properties-panel' }, additionalModules: [ propertiesPanelModule, propertiesProviderModule ], moddleExtensions: { camunda: camundaModdleDescriptor } }); // 加载一个空的默认流程图 createNewDiagram(); }; // 创建一个空的、带基本结构的BPMN图 const createNewDiagram = async () => { const xml = `<?xml version="1.0" encoding="UTF-8"?> <bpmn2:definitions xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:flowable="http://flowable.org/bpmn" id="sample-diagram" targetNamespace="http://flowable.org/bpmn"> <bpmn2:process id="Process_1" isExecutable="true"> <bpmn2:startEvent id="StartEvent_1" /> </bpmn2:process> <bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1"> <bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1"> <dc:Bounds x="150" y="100" width="36" height="36" /> </bpmndi:BPMNShape> </bpmndi:BPMNPlane> </bpmndi:BPMNDiagram> </bpmn2:definitions>`; await bpmnModeler.importXML(xml); }; // 从后端加载模型列表 const loadModelList = async () => { const response = await axios.get('/api/bpmn/models'); modelList.value = response.data; }; // 加载指定模型的XML const loadModel = async (modelId) => { if (!modelId) return; const response = await axios.get(`/api/bpmn/model/${modelId}/xml`); await bpmnModeler.importXML(response.data.xml); }; // 保存当前模型到后端 const handleSave = async () => { const { xml } = await bpmnModeler.saveXML({ format: true }); const modelName = `流程模型_${new Date().getTime()}`; await axios.post('/api/bpmn/model', { name: modelName, xml: xml }); // 保存后刷新列表 await loadModelList(); }; // 部署流程(这里只是触发后端部署,下篇详述) const handleDeploy = async () => { const { xml } = await bpmnModeler.saveXML({ format: true }); await axios.post('/api/bpmn/deploy', { xml: xml }); }; onMounted(() => { initBpmnModeler(); loadModelList(); }); onBeforeUnmount(() => { if (bpmnModeler) { bpmnModeler.destroy(); } }); </script> <style scoped> .container { height: 100vh; display: flex; flex-direction: column; } .header { padding: 10px; border-bottom: 1px solid #eee; } .content { flex: 1; display: flex; overflow: hidden; } .canvas { flex: 1; border-right: 1px solid #eee; } .properties-panel { width: 300px; overflow-y: auto; } </style>这个组件完成了编辑器初始化、创建空白图、从后端加载已有模型、保存模型XML等核心功能。属性面板允许你编辑任务节点的详细信息。
3.2 后端:提供模型管理的 REST API
前端编辑器需要和后端交互,进行模型的增删改查。我们在 Spring Boot 中创建相应的控制器和实体。
首先,定义一个流程模型实体,用于在数据库中存储模型的基本信息和XML内容:
// BpmnModel.java @Data @Entity @Table(name = "bpmn_model") public class BpmnModel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // 模型名称 private String key; // 模型Key,通常与流程定义Key一致 private String description; @Lob // 大文本字段,用于存储XML @Column(columnDefinition = "LONGTEXT") private String xmlContent; private String version; // 模型版本 private Long createUserId; private String createUserName; private LocalDateTime createTime; private LocalDateTime updateTime; private Boolean deployed = false; // 是否已部署到引擎 }然后,创建对应的 Repository 和 Service 层。这里提供一个简单的控制器示例:
// BpmnModelController.java @RestController @RequestMapping("/api/bpmn") public class BpmnModelController { @Autowired private BpmnModelService bpmnModelService; // 获取模型列表 @GetMapping("/models") public Result<List<BpmnModelVO>> listModels() { List<BpmnModel> models = bpmnModelService.listAll(); List<BpmnModelVO> vos = models.stream().map(this::convertToVO).collect(Collectors.toList()); return Result.success(vos); } // 根据ID获取模型详情(含XML) @GetMapping("/model/{id}/xml") public Result<BpmnModelXmlVO> getModelXml(@PathVariable Long id) { BpmnModel model = bpmnModelService.getById(id); if (model == null) { return Result.error("模型不存在"); } BpmnModelXmlVO vo = new BpmnModelXmlVO(); vo.setId(model.getId()); vo.setName(model.getName()); vo.setXml(model.getXmlContent()); return Result.success(vo); } // 创建或更新模型 @PostMapping("/model") public Result<Long> saveModel(@RequestBody SaveModelRequest request) { BpmnModel model = new BpmnModel(); model.setName(request.getName()); model.setXmlContent(request.getXml()); model.setCreateTime(LocalDateTime.now()); model.setUpdateTime(LocalDateTime.now()); // 可以从XML中解析出key和版本等信息 // String processId = parseProcessIdFromXml(request.getXml()); // model.setKey(processId); Long savedId = bpmnModelService.saveOrUpdate(model); return Result.success(savedId); } // 触发部署(此处预留接口,实际部署逻辑在下篇与引擎集成时实现) @PostMapping("/deploy") public Result<String> deployModel(@RequestBody DeployRequest request) { // 1. 将XML保存为临时文件或直接传入引擎 // 2. 调用 Flowable 的 RepositoryService 进行部署 // 3. 更新模型状态为已部署 // 具体实现见下篇 return Result.success("部署请求已接收,具体实现需集成Flowable引擎"); } // 省略 VO 和 Request 类定义... }至此,一个最基础的“流程设计中心”骨架就搭建完成了。前端可以画图、保存、加载,后端负责存储。但这只是万里长征第一步,一个可用于生产环境的设计器,还需要解决一系列工程化问题。
4. 从“能用”到“好用”:编辑器集成的关键细节与避坑指南
如果只是把 bpmn-js 的官方示例跑通,你会觉得集成很简单。但一旦投入实际项目,以下几个问题会立刻浮现:
4.1 自定义 Palette(工具栏):只留下业务需要的元素
默认的 bpmn-js 工具栏包含了 BPMN 2.0 全量的元素,如各种事件、网关、活动。但对于大多数审批流场景,业务人员可能只需要“开始事件”、“结束事件”、“用户任务”、“并行网关”、“排他网关”等少数几个。过多的选项反而会造成困惑。
你需要自定义 Palette,隐藏不必要的元素:
// 自定义Palette模块 const customPaletteModule = { paletteProvider: ['type', function(palette, create, elementFactory, globalConnect) { // 创建一个“仅包含必要元素”的分组 palette.registerProvider('custom-palette', function() { return { getPaletteEntries: function(element) { return { // 隐藏默认的“工具”分组 'tool-separator': { group: 'tools', separator: true }, // 创建我们自己的分组 'custom-start-event': { group: 'custom', className: 'bpmn-icon-start-event-none', title: '创建开始节点', action: { dragstart: createStart, click: createStart } }, 'custom-user-task': { group: 'custom', className: 'bpmn-icon-user-task', title: '创建用户任务', action: { dragstart: createUserTask, click: createUserTask } }, 'custom-exclusive-gateway': { group: 'custom', className: 'bpmn-icon-gateway-xor', title: '创建排他网关', action: { dragstart: createExclusiveGateway, click: createExclusiveGateway } }, 'custom-end-event': { group: 'custom', className: 'bpmn-icon-end-event-none', title: '创建结束节点', action: { dragstart: createEnd, click: createEnd } } }; } }; }); }] }; // 在初始化Modeler时加入自定义模块 bpmnModeler = new BpmnModeler({ container: canvas.value, propertiesPanel: { parent: '#js-properties-panel' }, additionalModules: [ propertiesPanelModule, propertiesProviderModule, customPaletteModule // 加入自定义模块 ], moddleExtensions: { camunda: camundaModdleDescriptor } });4.2 属性面板的深度定制:绑定业务数据
默认的属性面板只能编辑 BPMN 标准属性。但在实际业务中,我们需要为“用户任务”节点设置审批人(flowable:assignee)、候选组(flowable:candidateGroups)、表单Key(flowable:formKey)等引擎扩展属性。更进一步的,我们可能希望直接在下拉框中选择系统中的角色或用户,而不是手动输入表达式。
这需要对属性面板的 Provider 进行扩展。以下是一个简化示例,展示如何添加一个“审批人”自定义字段:
// 自定义属性提供者 import { is } from 'bpmn-js/lib/util/ModelUtil'; function CustomPropertiesProvider(propertiesPanel, translate) { // 调用父类构造函数 propertiesPanel.BaseProvider.call(this); // 为“用户任务”提供额外的属性组 this.getGroups = function(element) { return function(groups) { // 只针对 UserTask 类型 if (is(element, 'flowable:UserTask')) { // 添加一个“审批设置”分组 groups.push(createApprovalGroup(element, translate)); } return groups; }; }; } // 创建“审批设置”属性组 function createApprovalGroup(element, translate) { return { id: 'approval', label: translate('审批设置'), entries: [ { id: 'assignee', label: translate('指定审批人'), modelProperty: 'assignee', widget: 'textField', // 可以改为 'select' 并绑定用户列表 get: function(element) { const bo = getBusinessObject(element); return { assignee: bo.get('flowable:assignee') }; }, set: function(element, values) { const bo = getBusinessObject(element); return bo.set('flowable:assignee', values.assignee || ''); } }, { id: 'candidateGroups', label: translate('候选组'), modelProperty: 'candidateGroups', widget: 'textField', get: function(element) { const bo = getBusinessObject(element); return { candidateGroups: bo.get('flowable:candidateGroups') }; }, set: function(element, values) { const bo = getBusinessObject(element); return bo.set('flowable:candidateGroups', values.candidateGroups || ''); } } ] }; } // 注册自定义属性提供者 propertiesPanelModule.__init__ = [ 'propertiesProvider', CustomPropertiesProvider ];在实际项目中,widget: 'select'的数据源需要从后端 API 动态获取角色和用户列表,这需要更复杂的前后端交互。
4.3 流程图的导出与导入:版本管理与协作
- 导出:
bpmnModeler.saveXML({ format: true })可以获取格式化后的 XML 字符串。你可以将其提供为.bpmn文件下载,方便离线存档或与其他工具交换。 - 导入:除了从后端数据库加载,还应支持用户直接上传本地的
.bpmn或.bpmn20.xml文件,通过bpmnModeler.importXML()加载到编辑器中。这是实现流程版本迭代和跨团队协作的基础。 - 图片导出:
bpmnModeler.saveSVG()可以导出当前流程图的 SVG 格式,用于生成审批单上的流程图,或在流程监控界面显示。注意 SVG 可能包含大量细节,需要后端进行压缩或转换为 PNG。
4.4 后端存储的优化:不仅仅是存 XML
- 元数据分离:不要只存一个巨大的 XML 字段。应将流程的
key,name,version等元数据单独存储,并建立索引,方便快速检索和列表展示。 - 版本控制:每次保存应生成新版本,而不是覆盖旧版本。可以借鉴 Git 的思想,记录版本号、创建人和备注,支持回滚到历史版本。
- 模型解析与校验:在后端保存 XML 前,应尝试用引擎的
BpmnXMLConverter进行解析校验,确保 XML 语法正确且符合引擎要求。避免存储无法部署的无效流程。 - 大字段处理:对于超大的流程图 XML,考虑使用对象存储(如 MinIO、OSS)存储文件,数据库中只存文件地址。
5. 集成不是终点:为后续引擎驱动铺平道路
集成 bpmn-js 编辑器,看似只是一个前端功能,实则是在为整个工作流体系搭建“设计层”。这个设计层的质量,直接决定了后续引擎集成的顺畅度。在完成本部分集成后,你应该能清晰地回答以下问题,这也是为下一篇《SpringBoot集成工作流引擎,Flowable/Activiti驱动(下)》做的准备:
- 流程定义从哪里来?-> 从我们刚建好的“流程设计中心”来,通过部署接口下发。
- 流程图的节点属性(审批人、表单)如何与业务系统关联?-> 在属性面板定制时,我们已经将
flowable:assignee等表达式与编辑器绑定,这些表达式会在引擎运行时被解析。 - 如何保证设计出的流程一定能被引擎执行?-> 通过后端的 XML 校验和引擎的部署前检查。
- 流程变更后,如何平滑升级?-> 依靠我们设计的版本管理机制,新部署的流程定义会自动作用于新的流程实例,旧的实例通常继续按原定义走(取决于引擎配置)。
当你拥有了一个稳定、易用、可定制的流程设计器后,工作流项目最难的部分——“如何将业务需求可视化、结构化地定义出来”——就已经解决了大半。剩下的引擎集成、任务查询、审批接口开发,更像是按照设计好的“图纸”(BPMN XML)去组装“机器”(流程实例),虽然也有不少细节,但路径是清晰的。
在下一篇文章中,我们将把这张“图纸”交给 Flowable 引擎,让它真正运转起来,并实现启动流程、查询待办、完成任务、追踪进度等核心业务接口。你会发现,前期的编辑器集成工作越扎实,后面的引擎驱动开发就越像是一场按图索骥的愉快旅程。