1. 为什么我们绕不开工作流引擎如果你在开发一个稍微复杂点的业务系统比如OA审批、采购流程、请假报销或者更复杂的金融信贷审批、生产线工单流转你大概率会遇到一个头疼的问题业务流程的代码怎么写最直接的想法可能是用一堆if-else或者switch-case来硬编码。一个简单的请假流程你可能会写出这样的伪代码if (status.equals(提交)) { // 通知部门经理 notifyManager(); status 经理审批中; } else if (status.equals(经理审批通过)) { // 判断请假天数 if (leaveDays 3) { // 通知总监 notifyDirector(); status 总监审批中; } else { // 直接归档 archive(); status 已完成; } } else if (status.equals(总监审批通过)) { // 归档 archive(); status 已完成; } else if (status.equals(经理驳回) || status.equals(总监驳回)) { // 通知申请人 notifyApplicant(); status 已驳回; }看起来好像也能跑对吧但问题很快就会接踵而至产品经理说经理审批后要加一个财务知会环节老板说超过5天的假期需要HR备案甚至法规变了某个环节需要增加会签。每一次变动你都需要去修改这坨已经盘根错节的业务代码小心翼翼地测试生怕改出新的Bug。更麻烦的是流程的状态、当前处理人、历史记录这些信息和你的业务数据请假单本身高度耦合想单独查看流程的进展图想统计每个节点的平均处理时间几乎不可能。这个时候工作流引擎的价值就凸显出来了。它本质上是一个业务流程的建模、执行和监控平台。你把“谁在什么条件下做什么事”这个流程画出来建模引擎负责按照这个图纸去驱动流程流转执行并且完整地记录下每一步的痕迹监控。Activiti作为Java领域最主流、最成熟的开源工作流引擎之一就是帮你解决上述痛点的利器。它让你能把经常变化的业务流程从硬编码中剥离出来变成可配置、可视化的模型从而实现业务逻辑与流程控制的解耦。接下来的内容我会结合自己多年在金融和政务项目中使用Activiti的经验带你从零开始彻底搞懂它。2. 核心概念扫盲BPMN2.0与Activiti的“世界观”在动手写代码之前必须理解Activiti赖以生存的“语言”——BPMN2.0。你可以把它理解为绘制流程的“工程制图标准”。Activiti是这套标准的Java实现。不理解这些核心概念看代码和文档会非常吃力。2.1 流程定义与流程实例这是最容易混淆的一对概念但至关重要。流程定义就是你的流程图是静态的模板。它定义了流程有哪些步骤、怎么连线。好比是“请假流程”这张设计图纸。流程实例是流程定义的一次具体执行。好比是小张在2023年10月26日发起的那一次具体的请假申请。一个流程定义可以产生无数个流程实例。在Activiti中你首先需要部署一个流程定义通常是一个.bpmn20.xml文件部署成功后引擎会将其解析并存储到数据库。当有人发起申请时你就基于这个流程定义启动一个流程实例。2.2 关键元素与符号BPMN2.0的元素很多但掌握以下几个核心的就能覆盖80%的场景事件用圆圈表示代表流程中“发生的事情”。开始事件流程的起点一个流程必须有且只有一个。结束事件流程的终点可以有多个。中间事件挂在活动边界上的比如“超时提醒”定时中间事件。活动用圆角矩形表示代表需要完成的“工作”。用户任务需要人工参与的活动比如“经理审批”。这是最常用的活动类型引擎会为此生成一条待办任务。服务任务自动执行的活动比如调用一个Java类JavaDelegate去发送邮件、更新某个业务状态。脚本任务执行一段脚本如Groovy来自动处理逻辑。网关用菱形表示负责控制流程的分支与合并。排他网关像一道单选题。多条流出路径引擎会只选择第一条条件为true的路径执行。这是最常用的网关。并行网关允许多条路径同时进行所有并行分支都完成后流程才继续向下。比如“会签”需要所有会签人都同意。包容网关更灵活可以同时激活多条符合条件的路径也可以只激活一条。顺序流带箭头的实线连接各个元素表示执行顺序。流程变量这是Activiti的“血液”。它是在流程实例级别或任务级别存储的键值对数据。比如leaveDays请假天数、applicant申请人、approvalComment审批意见都可以作为流程变量。网关的判断条件、任务的处理人指派都依赖于流程变量。注意很多初学者喜欢把业务实体的ID如leaveId作为流程变量存进去然后在任务处理时再去查业务库。这是一个好习惯保持了工作流数据和业务数据的松耦合。工作流引擎只关心流程怎么走不关心请假单的详细信息是什么。3. 环境搭建与核心API初探理论懂了我们来点实际的。搭建一个可用的Activiti环境远不止加一个Maven依赖那么简单。3.1 依赖引入与版本选择首先通过Maven引入核心依赖。这里以Activiti 7.xSpring Boot风格为例它比老版本更易于集成。dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version !-- 注意使用稳定版本 -- /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope !-- 初期测试用H2内存数据库方便 -- /dependency版本选择的经验谈Activiti 5.x 是经典版本极其稳定文档丰富但架构稍旧。Activiti 6.x 引入了新的ProcessEngineConfiguration配置方式。Activiti 7.x 最大的变化是拥抱了Spring Boot自动配置做得很好并且将REST API分离成了activiti-cloud项目。对于新项目我建议从7.x开始。但要注意7.x的某些小版本可能存在Bug生产环境务必选择社区反馈稳定的版本。3.2 数据库的考量与表结构Activiti需要数据库来存储流程定义、实例、任务、变量等所有运行时数据。它会自动创建28张表以ACT_为前缀。这些表可以分为几类ACT_RE_*:RE代表repository存储流程定义、模型等静态部署信息。ACT_RU_*:RU代表runtime存储运行时的流程实例、任务、变量等。这些表的数据在流程结束后会被删除历史数据移入历史表。ACT_HI_*:HI代表history存储所有历史数据用于查询和报表。ACT_GE_*:GE代表general通用数据如二进制资源流程图的BPMN XML和图片。生产环境数据库选型MySQL/PostgreSQL/Oracle均可。这里有个大坑MySQL的默认存储引擎InnoDB对事务支持好但早期版本在ACT_HI_历史表频繁插入时可能有性能问题。如果流程量非常大日流程实例数万级需要重点监控和优化历史表或者考虑使用ACTIVITI_HISTORY级别配置来减少历史记录。另一个经验是为ACT_RU_TASK运行时任务表和ACT_HI_TASKINST历史任务表的PROC_INST_ID_流程实例ID字段加上索引能极大提升根据流程实例查任务的效率。3.3 核心服务对象你的操作手柄Activiti的核心API围绕ProcessEngine展开通过它你可以获取各种服务每个服务职责单一// 通常由Spring容器注入这里演示直接获取 ProcessEngine processEngine ProcessEngines.getDefaultProcessEngine(); // 仓库服务管理流程部署、定义 RepositoryService repositoryService processEngine.getRepositoryService(); // 运行时服务启动流程实例、管理流程变量 RuntimeService runtimeService processEngine.getRuntimeService(); // 任务服务查询、办理、指派任务 TaskService taskService processEngine.getTaskService(); // 历史服务查询历史数据 HistoryService historyService processEngine.getHistoryService(); // 管理服务管理引擎、数据库作业等一般用不上 ManagementService managementService processEngine.getManagementService();记住这几个服务的分工你就知道该找谁办事了要部署流程找RepositoryService要发起申请找RuntimeService要处理待办找TaskService要查统计报表找HistoryService。4. 第一个端到端流程从建模到运行让我们用一个极简的“请假申请-经理审批”流程串起整个生命周期。我们会创建一个BPMN文件部署它然后启动实例并完成审批。4.1 绘制流程模型你可以使用Activiti官方提供的Eclipse插件或者更流行的在线/桌面工具如Camunda Modeler与Activiti兼容性好来画图。这里我们直接写XML来理解本质。创建一个文件simple-leave.bpmn20.xml:?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:activitihttp://activiti.org/bpmn targetNamespacehttp://www.activiti.org/processdef !-- 定义一个流程id是代码中引用的keyname是显示名 -- process idsimpleLeaveProcess name简单请假流程 isExecutabletrue !-- 开始事件 -- startEvent idstartEvent1 name开始/startEvent !-- 用户任务填写申请 -- userTask idfillRequest name填写请假申请 activiti:assignee${applicant} extensionElements !-- 通常这里可以绑定前端表单 -- /extensionElements /userTask !-- 用户任务经理审批 -- userTask idmanagerApprove name经理审批 activiti:candidateGroupsmanager extensionElements !-- 审批表单 -- /extensionElements /userTask !-- 排他网关根据审批结果判断 -- exclusiveGateway iddecisionGateway name审批结果/exclusiveGateway !-- 结束事件同意 -- endEvent idendEventApprove name同意结束/endEvent !-- 结束事件驳回 -- endEvent idendEventReject name驳回结束/endEvent !-- 顺序流连接所有元素 -- sequenceFlow idflow1 sourceRefstartEvent1 targetReffillRequest/sequenceFlow sequenceFlow idflow2 sourceReffillRequest targetRefmanagerApprove/sequenceFlow sequenceFlow idflow3 sourceRefmanagerApprove targetRefdecisionGateway/sequenceFlow !-- 带条件的顺序流 -- sequenceFlow idflowToApprove sourceRefdecisionGateway targetRefendEventApprove conditionExpression xsi:typetFormalExpression !-- 判断变量 approvalResult 是否为 agree -- ![CDATA[${approvalResult agree}]] /conditionExpression /sequenceFlow sequenceFlow idflowToReject sourceRefdecisionGateway targetRefendEventReject conditionExpression xsi:typetFormalExpression ![CDATA[${approvalResult reject}]] /conditionExpression /sequenceFlow /process /definitions关键点解析activiti:assignee${applicant}这是一个任务指派表达式。${applicant}是一个流程变量在运行时会被替换为实际的值比如用户ID “zhangsan”。这意味着任务的具体处理人是在流程运行时动态决定的。activiti:candidateGroupsmanager这是候选组。任务不是指派给具体某个人而是指派给一个组角色。所有属于“manager”这个组的用户都能看到并认领这个任务。条件表达式${approvalResult agree}网关的流转路径依赖流程变量approvalResult的值。4.2 部署流程定义有了BPMN文件我们需要将其部署到引擎中使其成为一个可用的流程定义。Autowired private RepositoryService repositoryService; public void deployProcess() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/simple-leave.bpmn20.xml) // 从类路径加载 // .addInputStream(simple-leave.bpmn, inputStream) // 也可以从输入流部署 .name(简单请假流程部署) .deploy(); // 执行部署 System.out.println(部署成功部署ID: deployment.getId()); System.out.println(部署名称: deployment.getName()); }部署成功后你可以通过repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult()查询到生成的流程定义。它的key就是XML中process的id即simpleLeaveProcess。4.3 启动流程实例现在员工“张三”要请假了我们需要为他启动一个流程实例。Autowired private RuntimeService runtimeService; Autowired private IdentityService identityService; // 用于设置流程发起人 public String startLeaveProcess(String applicantUserId, int leaveDays, String reason) { // 通常我们会用业务键businessKey关联业务实体 String businessKey LEAVE_20231026001; // 例如请假单号 // 设置流程发起人会记录到ACT_HI_PROCINST表的START_USER_ID_字段 identityService.setAuthenticatedUserId(applicantUserId); // 准备流程变量 MapString, Object variables new HashMap(); variables.put(applicant, applicantUserId); // 用于填充 fillRequest 任务的 assignee variables.put(leaveDays, leaveDays); variables.put(reason, reason); variables.put(businessKey, businessKey); // 也可以作为变量存储 // 启动流程实例 ProcessInstance processInstance runtimeService.startProcessInstanceByKey( simpleLeaveProcess, // 流程定义的key businessKey, // 业务键 variables // 流程变量 ); System.out.println(流程实例启动成功实例ID: processInstance.getId()); System.out.println(业务键: processInstance.getBusinessKey()); return processInstance.getId(); }启动后流程会停留在第一个用户任务节点fillRequest。因为assignee是${applicant}而变量applicant被设置为了applicantUserId比如“zhangsan”所以这个任务会自动指派给张三。4.4 查询与办理任务现在张三需要查询自己的待办任务并完成“填写申请”这个步骤。Autowired private TaskService taskService; // 1. 查询用户的任务列表 public ListTask getTasksForUser(String userId) { return taskService.createTaskQuery() .taskAssignee(userId) // 查找指派给该用户的任务 // .taskCandidateUser(userId) // 查找该用户作为候选人的任务组任务 // .processDefinitionKey(simpleLeaveProcess) // 按流程类型过滤 .orderByTaskCreateTime().desc() // 按创建时间排序 .list(); } // 2. 办理任务填写申请 public void completeFillRequest(String taskId, MapString, Object moreVars) { // 办理任务时可以提交更多的流程变量 // 例如张三提交后确定了具体的请假类型 moreVars.put(leaveType, 年假); // 完成任务流程会自动流向下一个节点 managerApprove taskService.complete(taskId, moreVars); System.out.println(任务[ taskId ]已完成流程已进入经理审批环节。); }当张三完成任务后流程会到达managerApprove节点。这是一个候选组任务所有“manager”组的成员比如李四、王五都能在候选任务列表中看到它。4.5 处理组任务与网关决策经理“李四”需要先认领这个组任务将其变为自己的个人任务然后进行审批。// 经理李四认领任务 public void claimTask(String taskId, String userId) { taskService.claim(taskId, userId); System.out.println(任务[ taskId ]已被用户[ userId ]认领。); } // 经理李四审批办理任务 public void completeManagerApprove(String taskId, String approvalResult, String comment) { MapString, Object vars new HashMap(); vars.put(approvalResult, approvalResult); // 必须是 agree 或 reject vars.put(managerComment, comment); // 可以在完成任务时添加评论 taskService.addComment(taskId, null, comment); // null 代表关联到流程实例 taskService.complete(taskId, vars); System.out.println(经理审批完成结果: approvalResult); }当taskService.complete被调用后流程到达排他网关decisionGateway。引擎会计算两条流出顺序流的条件如果approvalResult变量等于agree流程流向endEventApprove流程实例正常结束。如果等于reject流程流向endEventReject流程实例同样结束但路径不同在历史记录中可以看到不同的结束节点。至此一个完整的、最简单的流程就跑通了。你可以通过HistoryService查询这个流程实例的完整生命周期轨迹。5. 深入流程变量引擎的“记忆”与“决策依据”流程变量是Activiti中贯穿始终的核心概念它决定了流程的走向、任务的处理人也是业务数据与流程交互的桥梁。理解它的作用域和生命周期至关重要。5.1 变量的作用域流程实例 vs 任务实例流程实例变量作用域是整个流程实例。在流程实例启动时或任何节点通过runtimeService.setVariable()设置。在任何后续的节点、网关、表达式中都可以访问。适合存储全局性的数据如applicant,leaveDays,businessKey。任务局部变量作用域仅限于单个任务。通过taskService.setVariableLocal()设置。只有在这个任务执行期间可以访问任务完成后如果未将其提升为流程变量它就会消失。适合存储仅在当前任务中有用的临时数据比如一个临时的计算中间值。一个常见的误用场景在审批任务中审批意见comment通常只与该任务相关。很多人会把它作为流程实例变量存储。这虽然可以但会导致流程变量表膨胀。更好的做法是使用taskService.addComment()添加任务评论或者将其作为任务局部变量。查询历史时可以通过HistoryService专门查询任务评论。5.2 变量的序列化与存储当你调用setVariable(“myObject”, myComplexObj)时Activiti需要把这个Java对象存到数据库的ACT_RU_VARIABLE或ACT_HI_VARINST表。这里有个大坑默认的序列化机制是Java原生序列化。public class LeaveRequest implements Serializable { // 必须实现Serializable private Long id; private String type; // getters and setters } LeaveRequest request new LeaveRequest(); runtimeService.setVariable(processInstanceId, leaveRequestObj, request);为什么这是坑数据库可读性差存进去的是二进制BLOB无法直接查看和调试。版本兼容性问题如果LeaveRequest类结构变了增删字段反序列化旧流程实例的变量时会失败导致流程无法继续。数据库迁移困难二进制数据在不同数据库间迁移可能有问题。最佳实践建议存储引用而非对象只存业务实体的ID如leaveRequestId需要时再用这个ID去业务数据库查询完整的业务对象。必须存对象时使用JSON利用Activiti的JsonType或CustomVariableType。你可以配置一个自定义的变量类型使用Jackson等库将对象序列化为JSON字符串存入数据库的TEXT类型字段。这样数据可读且对类结构变化的容忍度更高反序列化时忽略未知字段。5.3 通过变量驱动动态指派与条件分支这是流程变量的高级用法让流程变得灵活。动态任务指派userTask iddynamicTask name动态审批 activiti:assignee${nextApprover}/在流程中你可以在一个服务任务里根据复杂规则计算出下一个审批人是谁并将其赋值给流程变量nextApprover从而实现动态路由。复杂的网关条件sequenceFlow sourceRefgateway targetRefpathA conditionExpression xsi:typetFormalExpression ![CDATA[${leaveDays 3 leaveType 病假}]] /conditionExpression /sequenceFlow sequenceFlow sourceRefgateway targetRefpathB conditionExpression xsi:typetFormalExpression ![CDATA[${leaveDays 3}]] /conditionExpression /sequenceFlow条件表达式支持EL表达式可以组合多个变量进行复杂逻辑判断。但要注意表达式不宜过于复杂否则难以维护和调试。对于极其复杂的路由逻辑建议在网关前用一个服务任务来计算一个简单的路由键如nextStep然后网关根据这个路由键做简单判断。6. 用户任务与身份管理谁来做Activiti自带一套简单的用户-组关系表ACT_ID_USER,ACT_ID_GROUP,ACT_ID_MEMBERSHIP但在实际企业应用中99%的情况都不会直接使用它而是与公司现有的LDAP、AD或自建用户系统集成。6.1 集成外部身份系统关键在于实现Activiti的IdentityService接口背后的UserIdentityManager和GroupIdentityManager。更常见的做法是完全绕过Activiti的身份表。实践方案任务查询时过滤在调用TaskService.createTaskQuery()时你手头有当前登录用户的ID和所属角色列表。你可以这样查询// 查询指派给我的任务 taskService.createTaskQuery().taskAssignee(currentUserId).list(); // 查询我所属角色能看到的候选组任务 ListString myGroupIds getCurrentUserGroupIds(); // 从你的用户系统获取 taskService.createTaskQuery().taskCandidateGroupIn(myGroupIds).list();这样任务的指派和候选组信息虽然存储在Activiti中但用户和组的验证逻辑完全由你的外部系统控制。在流程中使用角色名而非具体人在BPMN中任务的candidateGroups永远填写角色名如dept_manager,hr_bp。在启动流程或流转时通过一个监听器或服务任务根据业务规则和当前用户上下文动态地将角色解析为具体的用户ID并设置到assignee或candidateUsers中。6.2 任务监听器与指派activiti:assignee${applicant}这种静态表达式有时不够用。我们可以使用任务监听器在任务创建时动态指派。userTask idhrApprove nameHR备案 extensionElements activiti:taskListener eventcreate classcom.yourcompany.listener.HrTaskAssignmentListener/ /extensionElements /userTaskpublic class HrTaskAssignmentListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { // 从流程变量或业务规则中计算负责人 String processInstanceId delegateTask.getProcessInstanceId(); String businessKey (String) delegateTask.getVariable(businessKey); // 根据业务键查询业务数据决定HR负责人 String hrPersonInCharge yourService.findHrByBusinessKey(businessKey); // 动态指派 delegateTask.setAssignee(hrPersonInCharge); // 或者设置候选组/人 // delegateTask.addCandidateGroup(hr_team); // delegateTask.addCandidateUser(user1); } }监听器非常强大除了create事件还有assignment任务被指派时、complete任务完成时等事件可以用于自动设置变量、发送通知、记录日志等。7. 历史数据与流程监控事后如何复盘流程跑起来不是终点运维和业务分析更需要关注流程的运行情况。HistoryService是你的得力工具。7.1 历史数据查询Activiti默认会将运行时的数据ACT_RU_*在流程结束后归档到历史表ACT_HI_*。你可以查询历史流程实例createHistoricProcessInstanceQuery()历史活动createHistoricActivityInstanceQuery()—— 可以看到流程走过的每一个节点、开始和结束时间。历史任务createHistoricTaskInstanceQuery()—— 每个用户任务的详细信息、办理人、耗时。历史变量createHistoricVariableInstanceQuery()—— 流程变量的历史值。// 查询某个业务键对应的已结束的流程实例 HistoricProcessInstance historicInstance historyService .createHistoricProcessInstanceQuery() .processInstanceBusinessKey(LEAVE_20231026001) .finished() // 只查已结束的 .singleResult(); if (historicInstance ! null) { // 查询这个实例的所有活动记录 ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(historicInstance.getId()) .orderByHistoricActivityInstanceStartTime().asc() .list(); // 可以据此绘制出实际的流程轨迹图 }7.2 流程性能分析与瓶颈定位通过历史数据我们可以做很多分析节点耗时分析计算每个userTask从创建到完成的平均耗时找出审批瓶颈。流程超时分析结合ACT_HI_ACTINST表的DURATION_字段找出长时间卡住的节点。流程吞吐量统计按天/周统计流程实例的启动和完成数量。一个实用的技巧在流程启动和每个关键任务节点通过监听器或服务任务向一张自定义的业务统计表插入时间戳和节点信息。这样你可以完全根据自己的业务维度部门、流程类型进行高效的聚合查询而不必每次都去解析复杂的历史表结构。7.3 历史级别配置历史数据不是越多越好记录太多会影响性能。在ProcessEngineConfiguration中可以配置history属性none: 不保存任何历史数据。性能最好但无法审计。activity: 只保存流程实例和活动节点信息。这是最常用的平衡选择知道流程走过哪些路。audit: 在activity基础上增加任务、变量等信息。满足大部分审计需求。full: 保存所有细节包括流程变量的每次变更。性能开销最大仅用于调试或极高审计要求的场景。# 在Spring Boot配置中 spring.activiti.history-levelaudit对于超高频的流程我通常在生产环境使用activity级别并将历史数据定期归档到独立的分析库以减轻主业务库的压力。