Activiti工作流引擎实战:从核心概念到企业级流程开发

📅 2026/8/1 13:55:58
Activiti工作流引擎实战:从核心概念到企业级流程开发
1. 项目概述为什么我们需要一个工作流引擎如果你在开发企业级应用尤其是涉及审批、报销、请假这类流程化业务时肯定遇到过这样的场景业务逻辑和流程逻辑高度耦合改一个审批节点代码要动一大片流程状态全靠数据库字段手动维护今天加个“加签”明天加个“驳回”后天产品经理又说要支持“并行审批”代码越写越乱维护成本指数级上升。这时候一个成熟的工作流引擎就显得至关重要了。Activiti作为一款轻量级、开源且功能强大的工作流与业务流程管理BPM引擎就是为了解决这些问题而生的。它基于BPMN 2.0业务流程模型与标注标准允许我们通过可视化的流程图来定义复杂的业务流程然后由引擎来驱动这些流程的自动执行。简单来说你把业务流程图画出来剩下的流转、任务分配、状态管理、历史追踪这些“脏活累活”都交给Activiti。开发者只需要关注业务节点的具体实现比如“部门经理审批”这个节点要做什么操作而不用再操心“这个任务完成后该流转给谁”、“如何记录每一步的操作日志”这类流程控制问题。我最早接触Activiti是在一个大型的OA系统重构项目中当时手动维护的审批状态机已经变成了一个近千行的“屎山”switch-case。引入Activiti后流程定义变得清晰可视代码职责分离开发和运维效率都得到了质的提升。这个系列我就结合自己踩过的坑和积累的经验带你从零开始彻底搞懂Activiti的核心概念、实战用法以及那些官方文档里不会写的“潜规则”。2. 核心概念与架构拆解理解Activiti的“世界观”在动手写代码之前我们必须先理解Activiti的几个核心概念。这些概念是理解其运行机制的基础很多初学者遇到的困惑根源都在于对这些概念的理解偏差。2.1 BPMN 2.0统一的流程“语言”Activiti的核心是BPMN 2.0。你可以把它理解为流程图的“国际通用语言”。就像建筑工程师用CAD图纸沟通一样BPMN 2.0提供了一套标准化的图形符号和XML格式来描述业务流程。事件Event流程中发生的事情用圆圈表示。例如开始事件一个细圆圈、结束事件一个粗圆圈、中间捕获事件比如定时器事件。活动Activity流程中需要执行的工作用圆角矩形表示。这是最常用的元素比如“用户任务”、“服务任务”。网关Gateway控制流程的分支与合并用菱形表示。例如排他网关XOR带“X”的菱形用于决策if-else并行网关AND带“”的菱形用于同时发起多个分支。顺序流Sequence Flow连接其他元素的箭头表示执行顺序。泳道Swimlane用池Pool和道Lane来区分流程的不同参与者比如不同的部门或角色。使用标准BPMN的好处是业务分析师可以用专业的流程设计工具如Activiti官方提供的Activiti Modeler或者开源的bpmn-js画出流程图生成标准的.bpmn20.xml文件。开发者拿到这个文件几乎不需要修改就能直接部署到Activiti引擎中运行。这种“设计即开发”的模式极大地提升了业务与研发的协作效率。2.2 Activiti的核心服务与数据库表Activiti通过一组定义清晰的服务接口与数据库交互驱动流程运行。理解这些服务是进行API编程的关键。Activiti 7.x之后更推荐使用ProcessRuntime、TaskRuntime等更现代的API但其底层思想与传统的服务接口一脉相承。我们先从经典的服务模型入手它更利于理解原理。Activiti引擎在启动时会创建一系列服务实例我们可以通过ProcessEngine对象获取它们RepositoryService流程仓库服务。负责管理流程定义即BPMN XML文件的部署、查询、删除。你可以把它想象成流程定义的“版本管理器”。RuntimeService运行时服务。负责启动流程实例、管理流程变量、查询和执行流程实例。一个流程定义模板可以启动多个流程实例具体的审批单。TaskService任务服务。这是最常用的服务负责管理“用户任务”User Task例如查询待办任务、完成任务、拾取任务、设置任务代理人等。HistoryService历史服务。查询已经执行完毕的流程实例、任务、活动节点等信息用于生成报表或追溯历史。IdentityService身份服务。管理用户、组以及它们之间的关系。但在实际项目中我们通常会将Activiti与公司现有的用户体系如LDAP、自研用户中心集成所以这个服务使用频率不高。FormService表单服务。用于处理动态表单但实际项目中前后端分离架构下前端表单通常自研此服务也较少直接使用。ManagementService引擎管理服务。主要用于作业管理和数据库维护日常开发接触较少。这些服务背后对应着Activiti自动创建的数十张数据库表表名都以ACT_为前缀后跟两位分类标识ACT_RE_*:RE表示repository存储流程定义和部署信息的静态资源。ACT_RU_*:RU表示runtime存储运行时的流程实例、任务、变量等数据。这些表是引擎运行的核心记录当前正在进行的流程。流程结束后相应的记录会被删除。ACT_HI_*:HI表示history存储所有历史数据包括已结束的流程实例、任务、变量详情等。ACT_GE_*:GE表示general通用数据如二进制资源流程图片。ACT_ID_*:ID表示identity身份信息如果使用自带身份服务的话。实操心得排查生产环境流程卡住的问题时ACT_RU_TASK运行时任务表和ACT_RU_EXECUTION运行时执行流表是你最需要关注的两张表。通过它们你可以清晰地看到当前所有活跃的任务停留在哪个节点以及流程的精确执行路径。2.3 流程实例、执行流与任务的关系这是最容易混淆的一组概念我用一个简单的请假流程来解释流程定义你画好的“请假流程图.bpmn”定义了从“提交申请”到“审批结束”的完整规则。这是一个模板。流程实例张三今天提交了一张请假单引擎根据“请假流程图”这个模板创建了一个具体的流程。这个具体的请假审批过程就是一个流程实例。一个定义可以产生无数个实例。执行流一个流程实例在运行时引擎会创建一个或多个执行流来推进流程。通常开始事件会创建一个主执行流。当流程走到并行网关时一个执行流会分裂成多个子执行流分别走不同的分支。执行流是引擎驱动流程前进的“指针”。任务当执行流到达一个“用户任务”节点时引擎会在ACT_RU_TASK表中创建一条任务记录。这个任务会分配给具体的用户或组如“部门经理”。张三的部门经理李四登录系统就能在待办列表里看到这个任务。简单比喻流程定义是乐高图纸流程实例是按图纸拼好的一艘具体飞船执行流是拼装工人的手可能有多只任务则是图纸上标注的“现在请把这块积木某个具体操作拼上去”的指令。3. 环境搭建与第一个流程实战理论讲得再多不如动手跑一遍。我们从一个最简单的“员工请假流程”开始搭建环境并实现流程的完整生命周期部署 - 启动 - 办理任务 - 结束。3.1 项目初始化与依赖引入我们创建一个Spring Boot项目。现在Activiti 7.x已深度集成Spring Boot使用起来非常方便。在pom.xml中添加依赖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 dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency在application.yml中配置基本属性spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true # 开启H2控制台方便查看数据库 activiti: database-schema-update: true # 自动更新数据库结构 db-history-used: true # 使用历史表 history-level: audit # 历史记录级别audit记录所有节点信息 check-process-definitions: false # 不自动部署resources/processes下的bpmn文件注意database-schema-update: true在开发环境很方便引擎启动时会自动检查并创建缺失的表。但在生产环境务必设置为false并建议使用Flyway或Liquibase等工具来严格管理数据库脚本的版本避免不可预知的表结构变更。3.2 绘制并部署第一个BPMN流程图在src/main/resources/processes目录下新建一个leave-application.bpmn20.xml文件。你可以用任何文本编辑器写XML但更推荐使用可视化设计器。这里为了理解我们先看XML结构?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:xsdhttp://www.w3.org/2001/XMLSchema xmlns:activitihttp://activiti.org/bpmn targetNamespacehttp://www.activiti.org/processdef !-- 一个简单的请假流程 -- process idleaveApplication name员工请假流程 isExecutabletrue !-- 1. 开始事件 -- startEvent idstartEvent name开始请假/startEvent !-- 2. 员工提交申请用户任务 -- userTask idsubmitApply name提交请假申请 activiti:assignee${applicant} documentation员工填写请假单并提交/documentation /userTask !-- 3. 部门经理审批用户任务 -- userTask iddeptManagerAudit name部门经理审批 activiti:candidateGroupsdeptManager documentation部门经理审批请假申请/documentation /userTask !-- 4. 排他网关根据审批结果决定流向 -- exclusiveGateway iddecisionGateway name审批决定/exclusiveGateway !-- 5. 网关出口1审批通过 -- sequenceFlow idflowToApprove sourceRefdecisionGateway targetRefapproveEnd conditionExpression xsi:typetFormalExpression !-- 这里判断流程变量 auditResult 是否为 approve -- ![CDATA[${auditResult approve}]] /conditionExpression /sequenceFlow !-- 6. 网关出口2审批驳回 -- sequenceFlow idflowToReject sourceRefdecisionGateway targetRefmodifyApply conditionExpression xsi:typetFormalExpression ![CDATA[${auditResult reject}]] /conditionExpression /sequenceFlow !-- 7. 驳回后回到提交节点修改申请 -- userTask idmodifyApply name修改申请 activiti:assignee${applicant} documentation根据经理意见修改请假申请/documentation /userTask !-- 8. 修改后再次流向经理审批 -- sequenceFlow idflowToReAudit sourceRefmodifyApply targetRefdeptManagerAudit/sequenceFlow !-- 9. 审批通过流程结束 -- endEvent idapproveEnd name审批通过结束/endEvent !-- 连接线 -- sequenceFlow idflow1 sourceRefstartEvent targetRefsubmitApply/sequenceFlow sequenceFlow idflow2 sourceRefsubmitApply targetRefdeptManagerAudit/sequenceFlow sequenceFlow idflow3 sourceRefdeptManagerAudit targetRefdecisionGateway/sequenceFlow /process /definitions关键点解析process id流程的唯一标识在代码中通过这个ID来启动流程。userTask用户任务节点需要人工处理。activiti:assignee${applicant}任务指派给具体的人${}表示这是一个表达式运行时从流程变量applicant中取值。activiti:candidateGroupsdeptManager任务指派给一个组角色组内任何人都可以拾取并处理。exclusiveGateway排他网关相当于if-else。出口的顺序流上可以定义条件表达式。conditionExpression条件表达式使用JUELJava Unified Expression Language语法。这里判断流程变量auditResult的值。接下来我们编写一个简单的REST Controller来部署这个流程定义RestController RequestMapping(/process) public class ProcessController { Autowired private RepositoryService repositoryService; PostMapping(/deploy) public String deploy() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leave-application.bpmn20.xml) .name(员工请假流程部署) .deploy(); // 执行部署 return 流程部署成功部署ID: deployment.getId(); } }启动Spring Boot应用调用这个接口流程定义就部署到数据库ACT_RE_*表中了。你可以通过http://localhost:8080/h2-console登录H2数据库查看ACT_RE_PROCDEF表里面应该有一条leaveApplication的记录。3.3 启动流程实例并办理任务流程部署好了现在模拟员工“张三”发起一个请假申请。RestController RequestMapping(/runtime) public class RuntimeController { Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; /** * 张三发起请假 */ PostMapping(/start-leave) public String startLeaveProcess() { // 设置流程变量申请人 MapString, Object variables new HashMap(); variables.put(applicant, zhangsan); // 通过流程定义KEY启动一个流程实例 ProcessInstance instance runtimeService.startProcessInstanceByKey(leaveApplication, variables); return 流程实例启动成功实例ID: instance.getId(); } /** * 查询张三的待办任务 */ GetMapping(/tasks/{assignee}) public ListMapString, Object getTasks(PathVariable String assignee) { ListTask tasks taskService.createTaskQuery() .taskAssignee(assignee) // 根据办理人查询 .orderByTaskCreateTime().desc() .list(); return tasks.stream().map(task - { MapString, Object map new HashMap(); map.put(taskId, task.getId()); map.put(taskName, task.getName()); map.put(processInstanceId, task.getProcessInstanceId()); map.put(createTime, task.getCreateTime()); return map; }).collect(Collectors.toList()); } /** * 办理任务提交申请 */ PostMapping(/complete-submit) public String completeSubmitTask(RequestParam String taskId) { // 完成任务并可以设置下一步需要的流程变量 MapString, Object variables new HashMap(); variables.put(leaveDays, 3); // 请假天数 variables.put(reason, 回家探亲); // 请假原因 taskService.complete(taskId, variables); return 任务[提交请假申请]已完成流程已流转至[部门经理审批]; } }操作步骤调用/runtime/start-leave启动流程。此时第一个节点“提交请假申请”的任务已经产生并且分配给了zhangsan。调用/runtime/tasks/zhangsan可以查询到张三的待办任务。张三填写表单后调用/runtime/complete-submit?taskIdxxx完成“提交申请”任务。引擎会自动推进到下一个节点“部门经理审批”。此时任务到了“部门经理审批”节点并且这个节点是候选组deptManager。我们需要模拟部门经理“李四”来拾取并处理这个任务。这里涉及到一个重要概念拾取Claim。对于候选组任务需要组内成员先“认领”到自己名下才能办理。/** * 李四查询组任务并拾取 */ GetMapping(/group-tasks/{groupId}) public ListMapString, Object getGroupTasks(PathVariable String groupId) { ListTask tasks taskService.createTaskQuery() .taskCandidateGroup(groupId) // 查询候选组任务 .list(); // ... 返回任务列表同上 } PostMapping(/claim-task) public String claimTask(RequestParam String taskId, RequestParam String userId) { // 李四拾取任务将组任务变为他的个人待办 taskService.claim(taskId, userId); return 用户[ userId ]已成功拾取任务[ taskId ]; } /** * 李四审批通过或驳回 */ PostMapping(/complete-audit) public String completeAuditTask(RequestParam String taskId, RequestParam String result, // approve or reject RequestParam(required false) String comment) { MapString, Object variables new HashMap(); variables.put(auditResult, result); // 设置审批结果变量用于网关判断 // 添加审批意见会保存到历史记录中 if (StringUtils.hasText(comment)) { taskService.addComment(taskId, null, comment); } taskService.complete(taskId, variables); return 审批任务完成结果: result; }操作步骤李四调用/runtime/group-tasks/deptManager看到待审批的组任务。李四决定处理其中一个调用/runtime/claim-task?taskIdxxxuserIdlisi将任务认领。李四审批后调用/runtime/complete-audit传入result为approve或reject。如果resultapprove流程变量auditResult被设置为approve流经网关时匹配flowToApprove线直接到达结束事件流程结束。如果resultreject流程变量auditResult被设置为reject匹配flowToReject线流转到“修改申请”节点任务再次分配给申请人zhangsan。这就形成了一个驳回循环。至此一个包含基本元素开始事件、用户任务、排他网关、条件分支、循环的流程就完整地跑通了。你可以通过HistoryService查询这个流程实例的完整执行轨迹。4. 流程变量详解流程的“记忆”与“血液”流程变量Process Variable是Activiti中极其重要的概念它是流程在流转过程中携带和传递数据的载体决定了流程的走向也承载了业务数据。你可以把它理解为流程的“记忆”和“血液”。4.1 变量的作用域与生命周期变量是有作用域的主要分为两种流程实例变量Process Instance Variable作用域是整个流程实例在实例启动时或运行中设置所有该实例下的执行流和任务都能访问。通常用于存储全局业务数据如applicant申请人、leaveDays请假天数。任务变量Task Variable作用域仅限于单个任务。任务创建时设置任务完成后如果未显式提升为流程变量则通常会被清除。适用于该任务特有的临时数据。变量设置的最佳实践启动时注入在runtimeService.startProcessInstanceByKey()时通过Map传入初始变量。任务办理时传递在taskService.complete(taskId, variables)时传入变量这些变量默认会提升为流程实例变量除非指定作用域。这是最常用的传递数据的方式。运行时显式设置通过runtimeService.setVariable()或taskService.setVariable()在流程中间任何需要的地方设置。// 设置流程实例变量 runtimeService.setVariable(processInstanceId, key, value); // 设置任务局部变量仅该任务可见 taskService.setVariableLocal(taskId, localKey, localValue); // 获取变量 Object value runtimeService.getVariable(processInstanceId, key);4.2 变量的序列化与存储Activiti会将变量值序列化后存入ACT_RU_VARIABLE运行时和ACT_HI_VARINST历史表。支持的类型包括基本类型及包装类String, Integer, Long, Double, Boolean, Date, Short, Byte...可序列化的对象任何实现了java.io.Serializable接口的POJO。集合类型List, Map等其元素也需可序列化。重要避坑指南存储自定义对象如LeaveInfo时务必确保该类实现了Serializable接口并且保持serialVersionUID的一致性。如果流程实例运行期间你修改了实体类并重新部署了应用可能会导致反序列化失败流程无法继续。一种更稳健的做法是流程变量中只存储实体ID如leaveId: 123业务数据通过ID从业务表中实时查询。4.3 在表达式中使用变量变量最强大的地方在于可以在BPMN元素的表达式EL表达式中直接使用实现动态逻辑。任务分配activiti:assignee${applicant}网关条件${auditResult approve}服务任务调用在JavaDelegate实现类中可以通过DelegateExecution对象获取变量。监听器在事件监听器中可以读取和修改变量。表达式语言默认是JUEL功能强大但也需注意安全避免在表达式中执行危险操作。5. 用户任务User Task的深入配置用户任务是流程与“人”交互的核心节点其配置灵活性直接决定了流程的易用性。5.1 任务分配策略除了上面用到的assignee指定具体用户和candidateGroups候选组还有更多分配方式候选用户Candidate Usersactiviti:candidateUsersuser1, user2。多个用户都可以看到并拾取该任务。表达式动态分配userTask idhrAudit nameHR备案 activiti:assignee${hrManager}/在流程变量中设置hrManager为lisi或wangwu。通过监听器分配实现org.activiti.engine.delegate.TaskListener接口在create事件中编程式分配。public class ManagerTaskListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { String dept (String) delegateTask.getVariable(department); // 根据部门信息从数据库或缓存查询部门经理 String manager findManagerByDept(dept); delegateTask.setAssignee(manager); } }然后在XML中配置userTask iddeptAudit name部门审批 extensionElements activiti:taskListener eventcreate classcom.example.listener.ManagerTaskListener/ /extensionElements /userTask这种方式最灵活可以实现复杂的分配逻辑如轮询、负载均衡、根据职务动态查找等。5.2 任务办理人与候选人的查询在业务系统中我们经常需要构建“我的待办”、“组待办”列表。Activiti提供了强大的TaskQueryAPI。// 1. 查询个人待办指定办理人 ListTask myTasks taskService.createTaskQuery() .taskAssignee(currentUserId) .orderByTaskCreateTime().desc() .list(); // 2. 查询候选组待办 ListTask groupTasks taskService.createTaskQuery() .taskCandidateGroup(deptManager) // 精确组 // .taskCandidateGroupIn(Arrays.asList(deptManager, hr)) // 多组 .list(); // 3. 查询候选用户待办用户可能在多个候选组中或直接被设为候选人 ListTask candidateTasks taskService.createTaskQuery() .taskCandidateUser(currentUserId) // 这个API会查询用户作为候选人用户或组的所有任务 .list(); // 4. 复杂查询结合流程变量 ListTask tasks taskService.createTaskQuery() .processVariableValueEquals(applicant, zhangsan) // 流程变量applicant等于zhangsan .taskCandidateOrAssigned(currentUserId) // 当前用户是办理人或候选人 .list();性能提示taskCandidateUser查询虽然方便但在用户组关系复杂时可能性能不佳。如果系统用户量大建议在业务层维护用户-组关系然后使用taskCandidateGroupIn进行查询或者对Activiti的身份表ACT_ID_*进行定制化改造和缓存。5.3 任务附加操作委托、转办、加签实际业务中任务处理人可能无法及时处理需要委托或转交给他人。Activiti原生支持部分操作但更复杂的逻辑通常需要自己基于API封装。委托Delegate将任务暂时交给他人处理处理完后任务会回到原负责人这里。使用taskService.delegateTask(taskId, delegateUserId)。转办Transfer将任务完全转交给他人原负责人不再参与。使用taskService.setAssignee(taskId, newUserId)。加签这不是一个原生操作而是一种业务模式。例如部门经理审批时觉得需要财务会签。实现方式通常有两种动态增加候选人通过taskService.addCandidateUser(taskId, extraUserId)为当前任务添加一个候选人。这样原经理和财务都能看到并处理这个任务谁先完成即结束需业务规则约定。动态创建子任务通过编程方式复制或创建一个新的任务给财务并建立父子关系。这更复杂但能实现更精细的控制如并行加签、顺序加签。这些操作都会产生相应的历史记录便于审计。在设计流程时需要提前考虑这些业务场景并在流程节点上预留足够的灵活性或者通过额外的“管理功能”来支持。