
简介面向流程引擎开发者的功能示例代码包以动态加签与指定节点审批人为核心清晰演示流程部署、流程变量传递、任务分配等关键环节的实现思路。压缩包共200个文件、约1.57MB其中包含63个Java源码、62个class编译文件、56个流程定义文件并配套XML配置、SQL脚本及properties属性文件覆盖流程设计、引擎配置到数据初始化的完整链路。已有818人学习浏览内容适合正在使用主流流程引擎、需要实现复杂审批流的后端工程师。通过源码可掌握流程变量的读取与写入、任务认领与委派等接口的实际调用方式对照流程定义文件可理解排他网关、包含网关、子流程等建模元素结合配置文件和数据库脚本可快速搭起可运行的流程测试工程并在实际业务中实现动态加签与指定审批人功能。1. 用 Activity 跑通审批流的日常先看这批 bpmn 里有什么拿到这份代码的第一感觉是东西不多但每一样都踩在 Activiti 项目最常被问到的点上。exelusive.bpmn 明显是排他网关模型请假或审批场景里金额超过阈值走总监、否则直接归档就是这种结构sub.bpmn 是子流程把公共审批步骤抽出来复用holiday2.bpmn 是请假流程加签、指定审批人的需求基本都长在请假、报销、采购这类场景里。摘要里提到的部署、动态加签、流程变量、指定节点审批人正好对应着工作流开发里绕不开的四件事——把流程定义塞进引擎、跑起来、传参数、控制每一步谁来做。这篇笔记就是把这一整套东西从部署讲到跑通再把最容易出问题的几个点一次性说透适合刚接触 Activiti 、被网上各种说法绕晕的开发也适合准备在自己项目里引入审批流的团队。2. 流程部署与核心 API从 BPMN 文件到可执行流程实例2.1 五种流程文件的差异排他网关、子流程、包含网关怎么选先看 exelusive.bpmn。Activiti 里排他网关的节点类型是 exclusiveGateway特征是菱形里面一个 X分支条件互斥执行时从上往下找第一个条件为 true 的出口。请假流程里这么写days 3走部门经理否则直接人事归档。这里最容易被忽略的是边界条件——如果所有分支条件都不满足流程直接抛异常。include.bpmn 对应包含网关 inclusiveGateway分支条件允许同时命中多条满足的出口全部执行。比如审批通过和有会签需求同时触发两个分支并行跑。它和排他网关的区别是多选和单选和并行网关的区别是有条件地并行而不是无条件并行。sub.bpmn 是子流程把一段审批逻辑抽出来做成可复用的内嵌或调用子流程主流程里用一个 callActivity 节点引用它。demo4.bpmn 就是一个演示模型结构最简单适合做 API 测试。实际选型的判断标准就两条条件之间是否互斥以及是否需要并行协同。互斥用排他网关需要并行但带条件用包含网关公共审批逻辑抽出来用子流程。2.2 部署代码RepositoryService 的完整用法部署在 Activiti 里就是把 BPMN 文件交给 RepositoryService 解析存进引擎的部署表和流程定义表。项目里最常见的写法是通过 classpath 读取资源Autowired private RepositoryService repositoryService; public String deployProcess(String bpmnResourcePath, String processName) { Deployment deployment repositoryService.createDeployment() .addClasspathResource(bpmnResourcePath) .name(processName) .enableDuplicateFiltering() .deploy(); return deployment.getId(); }createDeployment()构建部署对象addClasspathResource()指定类路径下的 BPMN 文件name()给部署起个名字方便查enableDuplicateFiltering()是防重复部署开关——相同资源内容不会重复部署这个在反复改模型、高频测试时特别有用不加的话每跑一次就多一条部署记录。deploy()提交后引擎解析 BPMN 并落库返回的 deployment ID 可以在ACT_RE_DEPLOYMENT表查到。部署完成后需要确认流程定义是否生成成功ListProcessDefinition definitions repositoryService .createProcessDefinitionQuery() .deploymentId(deploymentId) .list(); for (ProcessDefinition pd : definitions) { String message String.format( 流程定义: key%s, version%d, id%s, pd.getKey(), pd.getVersion(), pd.getId() ); System.out.println(message); }这里的 key 来自 BPMN 文件里process idholiday的 id 属性version 表示同一 key 下的版本号每次重新部署版本号加一。查询结果为空基本就是文件没被解析到先去检查 classpath 路径有没有写错、文件名大小写对不对。2.3 启动流程实例与流程变量的传递部署只是把模型存好真正跑起来要启动流程实例启动的同时把流程变量传进去MapString, Object variables new HashMap(); variables.put(days, 5); variables.put(applicant, zhangsan); variables.put(amount, 12800d); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(holiday, BIZ-2024-001, variables);startProcessInstanceByKey用的是流程定义的 key不是部署 ID这样同一个流程模型新版本部署后业务代码不用改。第二个参数是 businessKey把业务单号和流程实例绑定后续从业务表反查流程走到哪一步就靠它。variables 里的数据会被引擎存到ACT_RU_VARIABLE表排他网关的分支判断就是读这些变量算出来的。变量类型要注意days传 Integeramount传 Double网关条件里写${amount 10000}时类型要能对上字符串 12800 和数字 12800 在 SpEL 表达式里的表现完全不同。如果是自定义对象引擎默认走 Java 序列化存进数据库的是字节流后续想跨流程实例查询某个字段就查不了。能传基础类型的就传基础类型实在要传对象建议转成 JSON 字符串再存查询的灵活性高很多。2.4 流程变量的作用域与更新时机流程变量按作用域分为流程实例级和任务级。流程实例级变量从启动伴随到流程结束任务级变量的生命周期只到该任务完成。查询的时候要分清用哪个 APIMapString, Object taskVars taskService.getVariables(taskId); MapString, Object processVars runtimeService.getVariables(processInstanceId);更新变量也是同样的区别taskService.setVariable()更新任务级变量runtimeService.setVariable()更新流程实例级变量。任务完成后任务级变量会被清理而流程实例级变量会保留到流程结束并归档到历史表。实际项目里任务监听器中动态改变量的高频写法是delegateTask.getExecution().setVariable(key, value)这是监听器上下文里最稳的更新方式能确保变量挂在当前执行实例上。3. 动态加签与指定审批人两处最实用的扩展点3.1 加签的本质向运行中的任务插入审批节点加签这个词在不同项目里含义不完全一样但核心诉求是同一个审批过程中当前审批人觉得需要多一个人参与意见。Activiti 里没有一键加签的原生 API常见做法分两类。一类是在 BPMN 模型层面预留加签分支用并行网关或者多实例节点实现运行时通过流程变量控制要不要激活加签子流程。这个方案干净、可追踪但流程定义复杂。另一类是直接操作运行时任务往当前任务上追加候选人代码是这个Task task taskService.createTaskQuery() .processInstanceId(processInstanceId) .singleResult(); taskService.addCandidateUser(task.getId(), shenpi_wang);addCandidateUser加的是候选审批人王工可以在自己的待办列表里看到这个任务但任务不独占归属需要他自己 claim认领之后才成为办理人。这个语义上的差异必须分清候选人出现在待办查询里可认领办理人是独占归属其他人在待办里看不到这个任务。如果业务上要求直接指派给某个人并立刻出现在他的待办里用setAssigneetaskService.setAssignee(task.getId(), shenpi_wang);3.2 指定节点审批人setAssignee 与流程变量的组合指定节点审批人是用户任务最核心的配置项摘要里提到的taskOwner和setAssignee是两种语义不同的设置。owner 是创建人assignee 是办理人。实际项目里最灵活的做法不是写死 assignee而是通过流程变量动态指定。在 BPMN 的 userTask 节点上这样设计userTask idapproveTask name审批 activiti:assignee${approver}/approver 的值在启动流程或任务流转时通过变量传入。启动时指定审批人MapString, Object variables new HashMap(); variables.put(approver, lisi); variables.put(days, 4); runtimeService.startProcessInstanceByKey(holiday, variables);这样流程定义本身不绑定具体人员谁审批由运行时变量决定。如果需要在任务创建时动态计算审批人用监听器更合适Component(approverListener) public class ApproverListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { if (TASK_CREATE.equals(delegateTask.getEventName())) { String approver calculateApprover(delegateTask); delegateTask.setAssignee(approver); } } private String calculateApprover(DelegateTask delegateTask) { Integer days (Integer) delegateTask.getVariable(days); return days ! null days 3 ? manager01 : hr01; } }监听器里setAssignee生效的时机是在用户任务创建事件里这个时间点拿到的流程变量是完整的。监听器比直接在 BPMN 里写${approver}灵活的地方在于审批人经过逻辑计算而不是纯变量传递。3.3 把加签做成完整的业务功能任务复制方案的取舍如果业务上要的是主审批人审批的同时加一个协办人只addCandidateUser不够因为候选人被认领后原审批人的任务就没了。一种粗暴但常见的做法是复制当前任务再生成一个新任务把两个任务指向同一节点这样两个人都在自己的待办里看到各批各的。代码上很多人直接往ACT_RU_TASK插数据但这会带来两个问题流程实例里存在两个并行任务后续任务完成时不知道以谁为准历史表里两个任务没有关联关系审计追溯困难。Activiti 社区推荐的做法是在流程设计阶段就预留并行分支。在用户任务后面接一个并行网关网关出来两个分支一个走原审批人一个走协办人两条分支汇合后再进入下一节点。这个方案叫静态加签加签分支可通过变量控制是否激活模型上看起来复杂一点但流程数据的完整性和可追溯性是最好的。实际项目的取舍建议低频加签场景用并行网关加变量控制模型改一次就行高频且加签人数不固定的场景考虑多实例节点Activiti 的activiti:collection属性可以直接指定一个审批人列表实现会签userTask idmultiApproveTask name会签审批 activiti:assignee${approver} activiti:collection${assigneeList} activiti:elementVariableapprover/assigneeList是流程变量存一个 List 引擎会为列表里的每个人创建一个任务实例这就是多实例会签的标准做法。4. 实战避坑流程跑不通时先查这五处4.1 排他网关条件永远命中第一个分支现象流程实例流转到网关后明明 amount 是 8000设计上应该走经理审批结果直接走了总监审批分支。原因Activiti 的排他网关从上到下匹配条件第一个条件为 true 就选它不会做最优匹配。如果第一个分支条件写得宽泛比如${amount 0}后面严格的${amount 10000}永远没机会执行。解决给每个出口条件加上显式边界优先把最严格的条件放在最前面。条件表达式改成${amount 10000}、${amount 3000 and amount 10000}、${amount 3000}这样逐层收窄并保证边界值不会同时落入两个分支。4.2 加签后两个人都审批流程却提前结束了现象给当前任务加了协办人后主审批人完成任务流程直接走到了下一个节点协办人的待办任务还挂着但已经没有意义。原因复制任务或直接在运行时任务表插数据创建的并行任务不在流程模型的网关控制范围内。主任务完成时引擎认为当前节点已结束直接推进到下一步不会等其他任务。解决不要绕过流程模型去插运行时任务。需要在同一节点并行审批的用并行网关或activiti:collection多实例节点让引擎自己管理分支汇合。加签需求最好在流程设计阶段就预留分支而不是运行期改数据。4.3 流程变量传了自定义对象历史表查询直接报错现象流程跑完后通过 HistoryService 查历史变量时抛 ClassNotFoundException 或者反序列化异常。原因流程变量存了自定义 Java 对象引擎默认 Java 序列化写入ACT_HI_VARINST。一旦类的包名或结构发生变化反序列化时找不到原类就炸了。而且历史数据跨版本查询时老的字节流和新代码不兼容。解决流程变量尽量用 String、Integer、Double 等基础类型。必须传对象时手动转 JSON 字符串存入取出来再反序列化这样即使实体类结构变了JSON 字段也能兼容。历史表里存 JSON 还有一个好处写 SQL 可以直接按内容模糊查询。4.4 指定了审批人待办列表里却查不到现象BPMN 里activiti:assignee${approver}流程启动时变量里也传了 approver但用 TaskQuery 按 assignee 查待办结果是空的。原因变量名拼写不一致或者启动流程时流程变量没传到用户任务所在的执行实例上。如果审批任务在子流程里父流程设置的变量不一定自动可见变量作用域是执行实例级的。解决先确认 BPMN 文件里${approver}和代码里variables.put(approver, ...)的 key 完全一致一个字符都不能差。变量在子流程里不可见时通过ExecutionListener或TaskListener显式把父流程变量拷贝到子流程执行实例上。调试时打印delegateTask.getExecution().getVariables()看实际有哪些变量。4.5 多次部署后跑的流程还是旧逻辑现象改了 BPMN 文件重新部署新发起的流程实例执行得还是旧的节点顺序看起来像是没生效。原因startProcessInstanceByKey默认启动当前 key 最新版本的定义但如果部署时加了enableDuplicateFiltering()并且文件内容哈希没变化引擎会跳过这次部署导致流程定义版本没更新。另一种情况是缓存Activiti 对流程定义有内存缓存同一 JVM 里修改后立即启动可能命中旧缓存。解决部署确认processDefinition.getVersion()确实 1 了再去启动流程实例。排查缓存可以先强制重新部署一次确认版本变化后再启动。如果还是旧逻辑检查启动代码是不是用了固定的部署 ID 或流程定义 ID那会绕过 version 逻辑。5. 把示例流程改成真实业务的完整套路替换 BPMN 的三个步骤有了前面的基础把 holiday2.bpmn 改成真实业务是最快的内化方式。我自己改的话流程固定三个阶段。第一步复制模型文件替换业务节点和网关条件。拿一份 holiday2.bpmn把用户任务的 name 改成真实业务节点比如把部门经理审批改成区域经理审批网关条件从${days 3}改成${amount 50000}。改完先别急着部署把文件拖到浏览器里检查节点 id 有没有重复条件表达式里的变量名和计划里的流程变量能不能对上。id 重复是新手最高频的错误HTML 解析器不报错但引擎部署时直接抛异常。第二步写一个最小化测试类覆盖主流程核心逻辑是三段断言部署返回的 deployment ID 不为空、启动流程实例成功、网关分支走到预期的用户任务上Test public void testHolidayProcess() { String deploymentId deployProcess(holiday2.bpmn, 请假流程); Assert.assertNotNull(deploymentId); MapString, Object variables new HashMap(); variables.put(days, 5); variables.put(approver, lisi); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(holiday, variables); Assert.assertNotNull(processInstance); Task task taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .singleResult(); Assert.assertEquals(部门经理审批, task.getName()); Assert.assertEquals(lisi, task.getAssignee()); }这段测试的作用是帮你在改流程定义时快速反馈改了网关条件会不会走到错误分支改了审批人变量能不能正确落到任务上。开发环境每改一次就跑一遍比部署到测试环境再手工点流程要快得多。第三步验证历史数据的完整性。流程走完一个完整实例后用 HistoryService 查一遍这个流程实例的完整轨迹这个习惯能帮你发现运行时进程表和历史进程表中是否一致。比如加签节点的历史任务是否有完整的时间记录流程变量是否在流程结束后仍然可查。ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); for (HistoricActivityInstance activity : activities) { String message String.format( %s - %s (%s), activity.getActivityName(), activity.getActivityType(), activity.getDurationInMillis() ); System.out.println(message); }查出来的结果应当和 BPMN 文件里的节点顺序完全一致如果出现重复节点或缺失节点说明网关条件的判断有问题或某个边界事件的触发时机不对。说句实在话Activiti 项目翻车的场景我见过很多大部分不是 API 不会调而是流程模型和代码对不上——变量名拼错、网关条件有缝隙、部署版本没更新都是在小地方栽跟头。所以我现在每改一个 BPMN 文件都强制自己先部署确认版本号 1再启动流程实例跑一遍完整链路最后查历史表验证节点序列和变量记录。这套动作看着繁琐但救回过很多次上线的命。希望这份笔记能让你少走几个弯路把这些功能一次跑通。本文还有配套的精品资源点击获取