简介:一份关于Activiti流程设计器actiBPM使用经验的文档,适配在IDEA环境下进行BPM流程建模与业务自动化开发的工程师。文档从实际痛点出发,详细记录了actiBPM插件导致中文乱码的排查与修复方法:即便File Encodings已设为UTF-8,仍可能因虚拟机参数未生效而乱码,需在IDEA安装目录bin下的idea64.exe.vmoptions末尾添加-Dfile.encoding=UTF-8并重启。同时,文档还分析了IDEA 2020版本中流程属性编辑框消失带来的不便,给出了两条实用路径——直接编辑BPMN 2.0 XML文件,或了解用户任务、服务任务、排他网关等标准符号后进行更精确的流程调整,并提醒不同版本工具的差异。文档着重强调掌握BPMN 2.0标准是灵活调整流程的核心,即便界面变化也能通过XML完成属性设置。资源以单个docx文件提供,大小约712KB,结构简洁,便于查阅。目前已有91人学习,适合想要理解Activiti流程设计细节、减少中文乱码与版本适配麻烦的开发人员。通过这份经验整理,可快速绕过常见障碍,提升流程模型的设计效率与正确性。
1. actiBPM是什么?用了它之后,流程审批图终于不再靠截图传话
很多做工作流开发的同行第一次听到 Activiti流程设计器、actiBPM 这两个词,基本都出现在同一个场景里:产品经理改审批流程,业务方说“这里加一级审批”,你画完流程图,业务方又说“不对,是采购先审、部门再审”……改来改去,最后靠聊天窗口传图确认。actiBPM 就是用来结束这种扯皮的:它是一个基于 Web 的 BPMN 流程设计器,常见做法是把它挂到 Activiti 引擎旁边,业务和开发直接在同一个页面上拖节点、连线、配置审批人,点保存后生成 BPMN 模型,再部署成引擎能执行的流程定义。这篇文章写给两类人:一是正在用 Activiti 做审批流、想找个可视化建模入口的开发者;二是已经踩过不少设计器的坑、想看看别人怎么绕过黑匣子的人。
2. 选型前先看清:actiBPM 在流程设计器里到底解决哪一段
2.1 它是“前端建模 + 引擎存储”的组合,不是独立流程引擎
actiBPM 常被人误会成一个独立的流程引擎,其实它从不自己跑流程。它做的事只有三件:画图、保存模型、把模型交给 Activiti 部署。你在页面上拖出来的每个节点、每条连线,最终都会落成一份 BPMN 2.0 规范的 XML;点保存时,这份 XML 和对应的模型元数据写入 Activiti 的模型表;点部署时,引擎把 XML 解析成流程定义,之后发起流程就按这个定义执行。
这个定位决定了它的两个特性。第一,它依赖 Activiti 的 RepositoryService 或对应的 REST API,没有引擎后端,actiBPM 就是一张空画布;第二,它跟表单设计器不是一回事,actiBPM 只管流程拓扑和节点属性,表单关联、任务办理界面都要另做。我一般会在项目启动前给团队划这条线:actiBPM 解决的是“流程怎么走”,不解决“审批单长什么样”。
从交付形态上看,actiBPM 常见的接入方式有两种:一种是把前端页面打成独立 war 包部署到 Java Web 容器里,另一种是把静态资源塞进现有工程,只通过接口访问引擎。我实际用下来更推荐后者,因为流程设计器几乎没有独享服务器的必要,挂在业务系统里,会话和权限都好统一。无论哪种方式,它都只是客户端,数据可靠性最后还是看引擎侧的三张核心表:模型表、部署表、流程定义表。
2.2 三条建模路线对比:纯前端绘图库、官方控制台、actiBPM
选型时要先看团队现状。纯前端 BPMN 绘图库是最轻的方案,画图能力完整,有属性面板插件,但保存、版本管理、部署全要自己写;官方全量建模控制台功能最全,有完整的流程管理界面,但如果你只需要一个嵌入式画图入口,整套东西会很重;actiBPM 正好卡在中间——它把建模界面和引擎 REST API 打通,你可以只把它当作一个模块嵌进现有系统。
我经常用下面这张表帮团队做判断:
| 路线 | 交付物 | 你需要自己做的部分 | 适合场景 |
|---|---|---|---|
| 纯前端绘图库 | 前端画布 + 导出 BPMN XML | 保存接口、模型列表、部署接口、版本管理 | 只想把画图能力嵌进自己页面里 |
| 官方全量建模控制台 | 完整流程管理界面与引擎 | 用户体系对接、权限、性能调优 | 新项目从零开始,愿意接受整套框架 |
| actiBPM | 可嵌入的 Web 设计器 | REST 接口适配、静态资源部署、参数映射 | 已有 Activiti 引擎,缺一个可视化建模入口 |
这个对比的经验是:不是越全越好。如果你已经有一套基于 Spring Boot 的引擎访问层,只是缺一个浏览器里的流程图编辑器,actiBPM 这种可嵌入的定位通常比换引擎成本低得多。反过来,如果连流程列表、版本对比、权限都没有,那就别只上一个设计器,老老实实用完整控制台更省事。
3. 把 actiBPM 接入现有系统:部署、静态资源与最小配置
3.1 用静态资源方式嵌入,避免跨域和端口冲突
我一般不建议把 actiBPM 做成一个独立服务,再让业务系统跨域调用,因为跨域和会话同步会平白多出一堆问题。更省心的做法是把设计器的静态资源放进现有 Web 工程的静态目录,再用网关或 Nginx 把特定路径转发到引擎服务。
先看一个 Nginx 配置的例子:
server { listen 80; server_name flow.example.com; # 设计器静态资源,直接由 Nginx 托管 location /actibpm/ { alias /data/web/actibpm/; index index.html; } # 引擎 REST 接口,转发到后端 Java 服务 location /activiti-rest/ { proxy_pass http://127.0.0.1:8080/activiti-rest/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里关键的是两个 location 的边界:静态路径和接口路径分开,前端页面不直接访问数据库,所有建模数据都走 REST 接口。proxy_pass 末尾的路径一定要和 location 对应,否则后端收到的前缀错位,actiBPM 的接口请求会一直 404。我在这里翻过一次车:location 写的是 /activiti-rest/,proxy_pass 却写成了 http://127.0.0.1:8080/,后端收到的路径丢了前缀,排查半天发现就是少了一段路径对齐。
如果项目用 Spring Boot 内置静态资源,可以把前端文件放到 src/main/resources/static/actibpm/ 下面,再在配置里把 REST 前缀配好。这种方式单机方便,但多实例部署时要留意:静态资源属于每个实例,模型数据在共用数据库里,节点级扩展没问题,前提是会话共享已经配好。
3.2 设计器连接引擎 REST 接口:配置项与权限建议
actiBPM 页面上通常有一段前端全局配置,用来告诉设计器引擎接口地址、认证方式、模型保存路径。常见的配置项如下:
| 配置项 | 含义 | 建议值 |
|---|---|---|
| engineApiBaseUrl | 引擎 REST 接口的基础路径 | /activiti-rest |
| modelSavePath | 保存模型用的接口路径 | /service/model |
| deployPath | 部署流程用的接口路径 | /service/repository/deployments |
| authType | 认证方式 | basic 或 token,按你自己的权限体系定 |
一段典型配置看起来像这样:
{ "engineApiBaseUrl": "/activiti-rest", "modelRoot": "/service/model", "deploymentPath": "/service/repository/deployments", "auth": { "type": "basic", "username": "bpm-modeler", "password": "换成你自己的密码" } }配置里最容易被忽略的是 authType。actiBPM 这类老牌 Web 设计器很多默认走 Basic 认证,如果你后端用的是登录态,保存模型时会一直收到 401。我的习惯是单独建一个建模服务账号,只给模型增删改查和部署权限,不要把管理员密码直接填进前端配置。这样万一配置泄露,损失的只是一个受限账号。
另一个容易踩的点是路径末尾斜杠。actiBPM 对 URL 拼接很敏感,baseUrl 末尾带斜杠、路径开头又带斜杠,拼出来就是双斜杠,有些网关会直接拒绝或重写。我统一用“baseUrl 末尾不加斜杠、具体路径前不加斜杠”的写法,能少一类玄学问题。
3.3 后端适配:Spring Boot 封装保存与部署接口
如果后端不是直接用 Activiti 自带 REST 服务,而是要暴露给 actiBPM 调用的接口,常见做法是写一个 Controller 包一层 RepositoryService。下面是一个最小实现:
@RestController @RequestMapping("/api/actibpm") public class ActiBPMModelController { private final RepositoryService repositoryService; public ActiBPMModelController(RepositoryService repositoryService) { this.repositoryService = repositoryService; } @PostMapping("/model") public String saveModel(@RequestBody ModelPayload payload) { Model model = repositoryService.newModel(); model.setKey(payload.getKey()); model.setName(payload.getName()); model.setMetaInfo(payload.getMetaInfo()); repositoryService.saveModel(model); repositoryService.addModelEditorSource( model.getId(), payload.getBpmnXml().getBytes(StandardCharsets.UTF_8)); return model.getId(); } @PostMapping("/model/{modelId}/deploy") public void deploy(@PathVariable String modelId) { Model model = repositoryService.getModel(modelId); byte[] bpmnBytes = repositoryService.getModelEditorSource(modelId); String filename = model.getName() + ".bpmn20.xml"; Deployment deployment = repositoryService.createDeployment() .addByteArray(filename, bpmnBytes) .name(model.getName()) .enableDuplicateFiltering() .deploy(); List<ProcessDefinition> definitions = repositoryService.createProcessDefinitionQuery() .deploymentId(deployment.getId()) .list(); } }保存接口做了两件事:先创建模型记录,再把 BPMN XML 作为模型的 EditorSource 存进数据库。注意这里用了 enableDuplicateFiltering(),意思是流程内容没变化时,重复部署会直接复用原有部署。这个开关有用但也有代价:如果只改了 XML 里的变量名而整体变化太小,它会认为内容没变而不生成新版本,线上怎么改都不生效,所以开不开要根据团队习惯决定。
deploy 接口里,文件名必须以 .bpmn20.xml 结尾,否则引擎解析不出这个文件是流程定义。这是我见过很多人翻车的地方:文件名写成 .bpmn,引擎虽然记录下文件,但不会自动识别成流程定义。部署后一定要拿 processDefinitionQuery 查一次,确认有定义生成,再返回前端“部署成功”。
3.4 集成时最容易忘的放行配置与跨域处理
如果后端有 Spring Security,最容易忘的是把设计器的静态资源和接口路径放行或配置角色。老版本写法常见的是:
http.authorizeRequests() .antMatchers("/static/actibpm/**").permitAll() .antMatchers("/api/actibpm/**").hasRole("BPM_MODELER") .anyRequest().authenticated();这段配置的意思是:任何人都能看到设计器页面,但保存和部署模型需要 BPM_MODELER 角色。这样设计器可以作为一个低频操作入口,不会暴露给普通登录用户。跨域方面,如果静态资源和接口不在同一个域名下,需要在后端加 CORS 允许来源,但更省心的方式是像前面 Nginx 配置那样,让两个路径走同一个域名,前端就不存在跨域问题。
4. 从拖节点到流程跑起来:actiBPM 的完整建模操作链
4.1 一个能跑的最小审批流程:五步走完模型创建
打开 actiBPM 后的第一原则:先画主线,再配属性,最后才保存。很多人习惯拖一个节点就配一堆参数,结果连错线又要返工。一个能跑的最简流程,按下面五步操作:
- 新建模型,填写流程名称和流程 key。流程 key 最好用有业务含义的英文,比如 leave_approval,它会成为引擎里流程定义的关键标识。
- 从左侧组件区拖入开始事件、用户任务、结束事件各一个,依次放在画布上。
- 从开始事件边界拉线连到用户任务,再从用户任务边界拉线连到结束事件。
- 点击用户任务,在属性面板里填审批人。如果审批人是动态指定的,先留空,后面用流程变量在发起时赋。
- 点击保存模型,确认 XML 没有报错。
这五步走完,只是完成了模型。要让它真正能被发起,还需要回到模型列表点部署,部署成功后流程定义的 key 才是你要用的那个。注意模型 key 和流程 key 是两个字段:模型的 key 用于模型管理,流程定义的 key 对应 BPMN XML 里 process 元素的 id。actiBPM 保存模型时会提示你填 key,但真正影响流程发起的是 XML 里的 id,所以自查时不要只看模型 key。
4.2 用户任务、网关、监听器:建模时最常用的三个属性区
画完线之后,80% 的建模工作集中在三个属性区。
用户任务属性区要重点看 assignee、candidateUsers 和 candidateGroups。assignee 填单人审批人,candidateGroups 填候选组,适合会签或部门审批。如果项目里审批人经常变,我不会在 actiBPM 里把 assignee 写死,而是在发起流程时用流程变量传递,任务创建监听器里再赋值。这样流程定义不用每次换人就重新部署,能显著减少上线次数。
排他网关属性区最核心的是条件表达式。在连出网关的连线上写 conditionExpression,格式是 ${变量 比较符 值},例如 ${amount > 5000}。这里最容易翻车的不是语法,而是变量类型。如果发起流程时传的是字符串 “6000”,表达式会抛异常或恒为 false,流程走到网关找不到出口。我会在发起接口里把所有参与网关判断的变量统一转成数字,顺便打一条日志,省去黑匣子式排查。
执行监听器属性区位于事件或节点下,支持 class、expression 或 delegateExpression 三种写法。class 写全限定类名,要求类实现相应的监听器接口;expression 适合写简单表达式;delegateExpression 常用于与 Spring 集成的场景。我在项目里只允许使用 delegateExpression 和少量 class 方式,理由是 expression 写在 XML 里难以单测,一旦流程埋到生产,改一个表达式就要重新部署;而 delegate 表达式可以指向同一个 Spring Bean,改逻辑只改代码。
4.3 保存前先说清:模型 key、process id 和发起 key 三者关系
actiBPM 的易混淆点,往往集中在三个“名字”上。模型 key 是模型管理用的,你在模型列表里看到的是它;BPMN XML 里 process 元素的 id,是部署后生成定义的根源;发起流程时 startProcessInstanceByKey 用的 key,实际必须和 process id 一致。
这三者不统一,最常见的结果就是:模型保存成功、部署成功,但代码里发起流程时找不到流程定义。所以我会在团队里定一条硬规则:模型 key、process id、发起 key 三者全部用同一个英文名。规则统一之后,UI 上显示的名称只影响展示,不影响引擎行为。
一份最简 BPMN XML 大致长这样:
<process id="leave_approval" name="请假审批" isExecutable="true"> <startEvent id="startEvent1" name="开始"></startEvent> <userTask id="userTask1" name="部门审批" activiti:assignee="${deptManager}"></userTask> <endEvent id="endEvent1" name="结束"></endEvent> <sequenceFlow id="flow1" sourceRef="startEvent1" targetRef="userTask1"></sequenceFlow> <sequenceFlow id="flow2" sourceRef="userTask1" targetRef="endEvent1"></sequenceFlow> </process>这段 XML 里 userTask 的 activiti:assignee 用的是 ${deptManager},值来自流程变量。使用这种写法时,发起流程的人有义务在启动时把 deptManager 变量传进去,否则任务会落到待认领池里,没有指定办理人,容易造成“任务消失了”的假象。
4.4 部署后如何验证流程定义真正可用
部署完成后,不要只看 actiBPM 页面上的提示。页面提示“部署成功”只代表引擎接受了这个包,不代表节点和连线合法。我的确认路径分三步:第一步,进数据库查流程定义表,确认 key、version 都正常;第二步,用引擎管理接口解析一次 BPMN XML,看有没有非法元素或缺少出口的节点;第三步,发一条测试流程,走一个最简单分支,确认能生成任务。
SELECT KEY_, VERSION_, NAME_ FROM ACT_RE_PROCDEF WHERE KEY_ = 'leave_approval' ORDER BY VERSION_ DESC;这条 SQL 是部署后首先要跑的。如果查出来的 VERSION_ 不是预期递增,而是跳号或没变,说明部署时有重复过滤或脏数据。另一个常用查询是模型表和部署表的关系:
SELECT M.ID_ AS MODEL_ID, M.NAME_ AS MODEL_NAME, D.ID_ AS DEPLOYMENT_ID FROM ACT_RE_MODEL M LEFT JOIN ACT_RE_DEPLOYMENT D ON M.NAME_ = D.NAME_;这里用名称关联只是临时排查手法,不建议当正式逻辑,因为模型表和部署表本来就没有外键。可靠做法是在部署接口里把 deploymentId 回写到模型元数据,这样模型和部署包之间才有稳定映射。
5. actiBPM 使用避坑:5 个让人血压升高的常见问题与排查记录
5.1 流程导出或打印时中文变成方块
现象:在 actiBPM 里看到的中文完全正常,但导出图片或打印时,流程节点上的中文全部变成小方块。
原因:导出图片时的 BPMN 图形是由 Java 服务端根据字体库渲染的,服务端缺少中文字体时,渲染引擎找不到能显示中文的字体,就用默认字体顶替,结果就是方块。actiBPM 本身只负责编辑,字体渲染依赖 JVM 所在操作系统的字库,这属于服务器环境问题,不是前端问题。
解决:在服务端安装中文字体包,比如常见的文泉驿中文字体,然后刷新字体缓存。如果安装后还没生效,查看渲染代码用的字体名,在字体配置里加别名,把“宋体”映射到已安装字体。这条看起来是环境小事,但在只跑最小化镜像的容器里特别常见,属于“本地好端端、一上线就废”的问题。
5.2 模型保存成功,但发起流程时找不到这个流程
现象:actiBPM 里保存并部署都提示成功,发起流程时用模型 key 去查,却一直查不到流程定义。
原因:发起流程用的 key 和 BPMN XML 里的 process id 对不上。actiBPM 保存模型时,模型 key 是给人看的管理 key,而部署生成流程定义时用的是 XML 里 process 元素的 id。很多人在新建模型时填的 key 和代码里 startProcessInstanceByKey 传的 key 不是同一个值。
解决:把代码里的发起 key 改成从流程定义表查出来的真实 KEY_,或者在建模时让模型 key 和 process id 保持一致。我建议后者,命名规则统一后维护成本最低。如果存量已经混乱,临时做法是部署后立刻查定义表,把真实 KEY_ 反馈给发起方。
5.3 排他网关条件一直走默认分支
现象:流程走到网关处,明明条件满足,但任务总是跑到默认分支或直接结束。
原因:最常见的是变量类型不一致。网关表达式里判断的是数字,发起的流程变量却是字符串;或者变量名大小写不一致,表达式 ${approve} 和变量 approve 对不上。另一个原因是条件表达式写在了错误的连线上,比如从开始事件直接拉出去的线也被填了条件,导致网关分支判断混乱。
解决:先在发起流程处打印所有流程变量,确认名称和类型,再在网关前加一个调试脚本任务。脚本任务的写法大致像这样:
<scriptTask id="debugVars" name="printVars" scriptFormat="groovy" activiti:autoStoreVariables="false"> <script> println execution.getVariables() </script> </scriptTask>这段脚本放在网关前面,跑测试流程时看日志。如果变量类型不对,就在发起流程的地方统一转型。我见过最隐蔽的情况是:条件写 ${amount > 5000},但用户提交的 amount 是 “6000.00” 字符串,表达式抛异常后引擎按 false 处理,流程永远走默认分支。这种问题用打印变量一查,五分钟定位。
5.4 自动保存把同事的改动覆盖了
现象:两个同事同时打开同一个模型,A 改左侧,B 改右侧,后保存的人把先保存的人整段覆盖,模型回滚到只有 B 的改动。
原因:actiBPM 这类设计器保存模型时通常是整包覆盖,不提供字段级合并;模型表里如果没有版本号约束,后一个保存请求就直接覆盖了记录。这是多人在线建模最常见的事故。
解决:给保存接口加乐观锁。思路是在模型元数据里维护一个版本号,每次保存时前端把上一次读到的版本号带给后端,后端先比对再更新:
if (!expectedRevision.equals(currentRevision)) { throw new BizException("模型已被其他人修改,请刷新后再保存"); }不管底层是用引擎自带的版本字段,还是自己在模型元数据里维护一个版本号,原则都一样:更新前先比对,不一致就拒绝。配合前端在保存冲突时提示用户刷新,基本能根治静默覆盖问题。
5.5 部署后修改模型不生效:版本号和缓存的坑
现象:修改了流程、保存、部署,但新发起的流程实例还在按旧版本执行,像完全没改过一样。
原因:三个因素叠加。一是发起流程的代码里指定了旧版本号或缓存了旧的流程定义 id;二是部署接口可能开了重复过滤,内容变化太小时被判定为重复部署;三是流程引擎对流程定义的解析有缓存,集群环境下某个节点的缓存没刷新。
解决:先确认流程定义表里确实生成了新版本;如果没有,查部署过滤逻辑。如果新版本存在但代码还用旧版,就把发起逻辑改成只按 key 发起,由引擎自动选择最新版本。需要指定版本时,明确到流程定义 id,不要写死版本号数字。集群环境要检查部署请求是否落到同一节点,不能用本地缓存。我一般会在部署后强制清一次相关缓存并跑冒烟测试,避免用户反馈“改了等于没改”。
6. actiBPM 的进阶技巧:在保存前给流程模型加一道校验闸
actiBPM 用久了,我发现最有价值的一个小改造,是在模型保存接口里加一道校验,而不是等部署后才报错。很多用户拖了一张图就保存,既没有开始事件,也没有结束事件,甚至节点之间没有连线。模型能存下来,部署也提示成功,但发起流程就报错。与其让人去引擎日志里找原因,不如在保存的时候拦下来。
我的做法是在保存的前置校验里解析 BPMN XML:
@PostMapping("/model/check") public String checkModel(@RequestBody ModelPayload payload) { Document document = DocumentHelper.parseText(payload.getBpmnXml()); Element root = document.getRootElement(); List<Element> startEvents = root.elements("startEvent"); List<Element> endEvents = root.elements("endEvent"); List<Element> userTasks = root.elements("userTask"); if (startEvents.isEmpty() || endEvents.isEmpty()) { throw new BizException("流程必须包含开始事件和结束事件"); } for (Element userTask : userTasks) { String assignee = userTask.attributeValue("activiti:assignee"); if (assignee == null || assignee.trim().isEmpty()) { throw new BizException("用户任务缺少审批人配置: " + userTask.attributeValue("id")); } } return "ok"; }这里的解析很简单,前提是工程里引入了 dom4j。检查点有三条:必须有开始事件、必须有结束事件、用户任务必须配置审批人或候选组。这三条覆盖了我遇到的大部分建模错误。更完整的校验可以继续检查每个有出线的节点是否都有出口、网关是否有默认分支、连线是否连到了正确目标,但那些检查容易误伤合法模型。我的经验是先做硬性规则,放开复杂的拓扑检查,给部署接口留一道二次校验。
把校验放到保存接口里还有一个好处:错误暴露在建模阶段,用户还停留在设计器页面,可以直接改;如果放到部署阶段,用户已经切到列表页,沟通成本就高了很多。除了校验,我还会在保存接口里顺手做流程 key 和 process id 一致性检查,不一致就按模型名生成一个规范 id。
说到底,actiBPM 只是一个画图工具,它能帮我们把流程图变成引擎可执行的东西,但流程定义的质量还是由使用它的人和组织保证。我自己始终把“校验前置”当第一原则:凡是能在入口拦住的问题,绝不放它到运行期变成线上故障。希望这篇使用经验能帮你少走几步弯路,把流程建模这件看似琐碎的事做得更稳一点。
本文还有配套的精品资源,点击获取