1. 项目概述为什么我们需要一个工作流引擎如果你在开发企业级应用尤其是涉及审批、报销、请假这类流程化业务时一定遇到过这样的场景业务逻辑像面条一样缠绕在代码里今天销售说要加一个总监审批节点明天财务说报销金额超过5000要走特殊流程。每次改动你都得小心翼翼地在一堆if-else和状态判断里穿梭生怕改出个Bug。更头疼的是流程的流转状态、当前处理人、历史记录这些数据管理起来也是一团乱麻。这时候一个专门负责流程编排和执行的“引擎”就显得至关重要了。Activiti就是这样一个在Java世界里经久不衰的开源工作流引擎。简单来说Activiti把业务流程从你的业务代码中抽离出来用一种可视化的流程图BPMN 2.0标准来定义。你只需要告诉它流程怎么走画图谁来处理配置处理人引擎就会自动帮你驱动流程实例管理任务分配记录每一步的痕迹。这带来的好处是显而易见的业务变更只需修改流程图无需动核心代码流程状态清晰可追溯而且它天然支持高并发和分布式部署为复杂的企业流程提供了坚实的底盘。我最早接触Activiti是在一个OA系统项目里当时手动撸了一套审批状态机后期维护简直是一场噩梦。引入Activiti后开发效率提升了产品经理甚至能自己用设计器调整流程那种解放生产力的感觉至今记忆犹新。接下来我会从一个实践者的角度带你彻底搞懂Activiti的核心概念、环境搭建和第一个流程的落地避开我当年踩过的那些坑。2. 核心概念全景图理解引擎的“零件”在动手写代码之前我们必须先理解Activiti里那些核心“零件”是什么以及它们是如何协作的。很多人一上来就照着例子跑通了一个请假流程但稍微变点需求就懵了根本原因就是概念没吃透。2.1 流程定义与流程实例蓝图与房子的关系这是最基础也最重要的一对概念。流程定义Process Definition就是你用BPMN 2.0画出来的那张流程图它是对业务流程的静态描述好比一张建筑的蓝图。这张图里定义了流程有哪些步骤、步骤之间的连线、每个步骤由谁处理等等。在Activiti中你通常会将一个.bpmn20.xml文件部署到引擎中这就创建了一个流程定义。而流程实例Process Instance则是根据这张蓝图具体盖起来的一栋房子。当某个员工发起一个请假申请时Activiti就会依据“请假流程”这个定义创建一个具体的流程实例。一个流程定义可以对应无数个流程实例就像一张蓝图可以盖出无数栋结构相同的房子但每栋房子里的住户、装修业务数据都是独立的。理解这个区别至关重要。你修改了流程定义比如增加一个审批节点通常不会影响已经正在运行的流程实例它们会按照旧的蓝图继续走下去。除非你特意做了流程定义的版本控制和新实例的迁移这涉及到更高级的用法。2.2 任务与执行流引擎驱动的核心单元流程实例启动后是如何一步步向前推进的呢这依赖于任务Task和执行流Execution这两个核心驱动机制。任务是流程中需要由“人”或“系统”去完成的一个工作单元。最常见的就是“用户任务”比如“部门经理审批”。当流程流转到这样一个节点时Activiti会在数据库中创建一条任务记录并将它分配给指定的处理人或候选组。处理人通过任务列表看到它完成审批后流程才会继续向下走。执行流则是一个更为底层的概念你可以把它理解为流程运行时的一个“指针”或“执行线程”。它沿着流程定义中的连线移动经过哪个节点就触发哪个节点的行为如创建任务、调用Java类、发送消息。一个流程实例在启动时会创建一个主执行流。当遇到并行网关Parallel Gateway时一个执行流会分裂成多个同时推进多条分支当分支在汇聚点汇合时多个执行流又会合并成一个。我曾经在排查一个并行审批流程卡住的问题时就是通过查询数据库中的ACT_RU_EXECUTION表发现有一条分支的执行流没有到达汇聚点从而定位到是某个节点的“完成任务”逻辑没有正确被调用。2.3 数据与上下文流程的“记忆”流程在运行过程中需要携带和传递数据。Activiti提供了多种作用域的变量来充当流程的“记忆”。流程变量Process Variables作用域是整个流程实例。比如请假单的applicant申请人、leaveDays请假天数这些信息在整个流程生命周期中都需要被访问和修改。它是最常用的一种变量。任务变量Task Variables作用域仅限于单个任务。通常用于存储该任务处理过程中产生的临时数据或者任务表单的数据。任务一旦完成这些变量默认不会自动提升为流程变量。本地变量Local Variables作用域是某个执行流。它比流程变量范围更小通常用于一些复杂的网关条件判断或脚本任务中的中间计算。变量是连接工作流引擎和业务系统的桥梁。业务数据通过变量传入引擎驱动流程分支判断比如leaveDays 3需要总监审批流程中的处理结果也通过变量传出用于更新业务状态。这里一个常见的坑是混淆变量作用域在任务中设置了变量却期望在流程其他地方能读到结果发现是null。3. 环境搭建与核心配置实战理论说得再多不如动手搭一个。这里我以Spring Boot 2.7.x Activiti 7为例带你走一遍最简化的集成流程。选择Activiti 7是因为它原生支持Spring Boot自动化配置程度高对新手更友好。3.1 依赖引入与基础配置首先在你的pom.xml中引入关键依赖。注意Activiti 7的Spring Boot Starter已经包含了引擎、MyBatisActiviti的持久层框架等所有必需组件。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引入依赖后Spring Boot的自动配置会为你准备好几乎一切。但有几个关键配置我建议你在application.yml中显式地设置一下以便更好地理解和控制引擎行为。spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1 username: sa password: driver-class-name: org.h2.Driver activiti: # 是否检查流程定义文件开发环境建议true生产环境可设为false提升启动速度 check-process-definitions: true # 数据库架构更新策略false-启动时不检查创建表true-启动时检查不存在则创建create-drop-启动创建关闭删除 database-schema-update: true # 是否部署classpath下的流程定义文件 deployment-mode: default # 历史数据记录级别none-不记录activity-记录节点实例audit-记录节点实例任务实例变量full-最全记录默认 history-level: audit重点说一下database-schema-update和history-level。第一次启动时务必设为true让Activiti自动创建那28张对你没看错就是28张运行时、历史、身份等表。history-level我推荐用audit它记录了流程实例、任务、变量足以满足绝大部分审计和查询需求又比full级别轻量。full级别会记录所有细节包括所有变量的每次变更数据量巨大除非有严格的合规要求否则慎用。3.2 数据库表结构初窥启动应用后你可以连接到H2控制台如果用了H2查看自动生成的表。这些表都以ACT_为前缀并按照功能分为几大类ACT_RE_*:RE表示repository存储。主要是静态的部署信息如ACT_RE_PROCDEF流程定义表、ACT_RE_DEPLOYMENT部署单元表。ACT_RU_*:RU表示runtime运行时。存储正在运行的流程实例、任务、变量等。这些数据是引擎运行的核心流程结束后相应的记录会被删除除非历史级别要求保留。ACT_HI_*:HI表示history历史。存储所有已经结束的流程实例的历史数据用于查询和审计。ACT_ID_*:ID表示identity身份。存储用户、组以及它们之间的关系。但很多项目会使用自己的用户体系而不用Activiti自带的这套。ACT_GE_*:GE表示general通用。如ACT_GE_BYTEARRAY存储流程定义图、流程实例图等二进制资源。实操心得刚开始不必死记每张表的字段但要知道这几大类表是干什么的。以后排查问题比如“流程为什么卡住了”就去查ACT_RU_TASK和ACT_RU_EXECUTION“想查上个月的所有审批”就去查ACT_HI_PROCINST和ACT_HI_TASKINST。这个分类思维能帮你快速定位问题。3.3 核心服务Bean注入Activiti的核心功能通过一系列Service接口提供在Spring Boot中它们已经被自动配置为Bean你可以直接Autowired注入使用。最常用的有以下几个RepositoryService: 流程仓库服务。负责管理部署、删除、查询流程定义。RuntimeService: 运行时服务。负责启动流程实例、管理流程变量、触发信号等。TaskService: 任务服务。这是前端交互最频繁的服务负责查询用户任务、完成任务、管理任务变量、设置任务处理人等。HistoryService: 历史服务。查询所有已经结束的流程和任务的历史数据。IdentityService: 身份服务。管理用户和组但通常我们集成自己的用户系统。在你的Service或Controller中可以这样注入并使用它们Service public class LeaveApplyService { Autowired private RepositoryService repositoryService; Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; // ... 你的业务方法 }环境搭好核心概念和组件也清楚了我们终于可以开始设计并运行第一个真正的工作流了。4. 第一个工作流从流程图到代码实现让我们用一个最经典的“员工请假流程”作为入门示例。流程很简单员工提交申请 → 部门经理审批 → 如果请假天数大于3天需要总监审批 → 最后流程结束。我们将分三步走画图、部署、写代码驱动。4.1 使用BPMN 2.0绘制流程图虽然可以用XML直接写.bpmn20.xml文件但我强烈建议使用可视化设计器。Eclipse有Activiti Designer插件在线工具如https://bpmn.io也非常好用。这里我描述一下关键节点开始事件Start Event: 圆形表示流程开始。用户任务User Task: 圆角矩形代表需要人工处理的任务。“提交请假申请”设置处理人为#{applicant}一个流程变量动态指定。“部门经理审批”设置处理人为deptManager可以是一个固定ID或从变量中获取。“总监审批”设置处理人为director。排他网关Exclusive Gateway: 菱形。用于做决策只有一条路径会被选择。我们用在“部门经理审批”之后判断leaveDays 3。序列流Sequence Flow: 带箭头的线连接各个节点。从排他网关引出的线需要设置条件表达式Condition Expression例如${leaveDays 3}和${leaveDays 3}。结束事件End Event: 粗边圆形表示流程结束。将画好的图保存为leave-process.bpmn20.xml并放到Spring Boot项目的src/main/resources/processes/目录下。Activiti会自动扫描并部署它。4.2 部署流程定义与启动实例部署操作一般只在流程有变更时才需要。由于我们配置了spring.activiti.check-process-definitions: true且文件放对了位置应用启动时就已经自动部署了。但我们也可以通过代码手动部署更可控public String deployProcess() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leave-process.bpmn20.xml) .name(员工请假流程V1) .deploy(); // 执行部署 System.out.println(部署成功部署ID: deployment.getId()); // 通常我们需要根据部署ID查询到具体的流程定义 ProcessDefinition processDefinition repositoryService.createProcessDefinitionQuery() .deploymentId(deployment.getId()) .singleResult(); return processDefinition.getId(); // 返回流程定义ID }部署成功后就可以启动流程实例了。启动时必须传入业务数据也就是流程变量。public String startLeaveProcess(String applicant, Integer leaveDays, String businessKey) { // 准备流程变量 MapString, Object variables new HashMap(); variables.put(applicant, applicant); variables.put(leaveDays, leaveDays); // 用流程定义的Key来启动实例businessKey通常关联你的业务主键如请假单ID ProcessInstance instance runtimeService.startProcessInstanceByKey(leaveProcess, businessKey, variables); System.out.println(流程实例启动成功实例ID: instance.getId()); return instance.getId(); }这里有几个关键点startProcessInstanceByKey中的leaveProcess是你BPMN文件中process idleaveProcess ...的id属性不是文件名。businessKey非常重要它是连接工作流实例和你的业务数据的桥梁。比如你的请假单表有一条记录ID为1001那么就把1001作为businessKey传入。之后你就可以用这个Key来查询对应的流程实例。变量applicant会被用于第一个用户任务“提交请假申请”的处理人指派因为我们在图中设置了#{applicant}。4.3 任务查询与完成流程实例启动后第一个任务“提交请假申请”会自动创建并分配给applicant变量指定的用户。现在我们需要模拟用户登录系统后查询并处理自己的任务。// 1. 查询某个用户的任务列表 public ListTask getTasksByAssignee(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) // 指定处理人 .orderByTaskCreateTime().desc() // 按创建时间排序 .list(); } // 2. 完成任务并可能传递新的变量 public void completeTask(String taskId, MapString, Object taskVariables) { // 完成任务时可以传入新的变量这些变量默认是任务变量 // 如果需要设置为流程变量可以在完成任务前通过runtimeService.setVariable设置 if (taskVariables ! null !taskVariables.isEmpty()) { // 方式一作为任务变量完成任务后如需保留需手动提升 // taskService.complete(taskId, taskVariables); // 方式二推荐直接设置为流程变量 for (Map.EntryString, Object entry : taskVariables.entrySet()) { runtimeService.setVariable(taskService.createTaskQuery().taskId(taskId).singleResult().getProcessInstanceId(), entry.getKey(), entry.getValue()); } } taskService.complete(taskId); System.out.println(任务 taskId 已完成。); }例如员工zhangsan提交申请后部门经理lisi登录查询自己的任务进行审批// 部门经理查询任务 ListTask deptManagerTasks taskService.createTaskQuery().taskAssignee(lisi).list(); for (Task task : deptManagerTasks) { System.out.println(待审批任务 task.getName() , 流程实例ID: task.getProcessInstanceId()); // 可以在此处通过runtimeService.getVariables获取流程变量如请假天数用于前端展示 MapString, Object processVars runtimeService.getVariables(task.getProcessInstanceId()); Integer days (Integer) processVars.get(leaveDays); System.out.println(请假天数 days); } // 假设任务ID是”task123“部门经理同意并填写审批意见 MapString, Object approveVars new HashMap(); approveVars.put(deptManagerComment, 情况属实同意。); // 注意这里完成的是“部门经理审批”这个任务 taskService.complete(task123, approveVars);当taskService.complete(“task123”)被调用后Activiti引擎会自动驱动流程向下执行经过排他网关根据leaveDays变量的值判断走向如果大于3天就会自动创建下一个“总监审批”任务并分配给director用户。至此一个完整的工作流从定义、部署、启动到流转我们就全部跑通了。你可以通过H2控制台观察ACT_RU_TASK和ACT_HI_ACTINST等表的数据变化直观地理解引擎的内部运作。5. 避坑指南与高级特性初探在基本流程跑通的基础上分享几个初期最容易踩的坑以及两个能立刻提升效率的高级特性。5.1 常见问题排查实录问题一流程启动后第一个任务没创建检查点1确认BPMN文件中的process的id和启动时代码中的processDefinitionKey是否完全一致大小写敏感。检查点2查看ACT_RU_EXECUTION表是否有你刚启动的流程实例记录如果没有可能是启动失败。如果有再看ACT_RU_TASK表看任务是否创建。如果执行流存在但任务没创建大概率是流程图第一个节点不是“用户任务”或者该用户任务的assignee或candidateUsers表达式解析为空。检查点3打开历史日志级别在application.yml中设置logging.level.org.activiti: DEBUG查看引擎执行的详细日志通常会明确报错。问题二任务完成了但流程没往下走卡住了检查点1最可能的原因是排他网关的条件表达式写错了。检查从网关出去的每一条序列流上的条件。确保使用的变量名和流程变量中的一致表达式语法正确如${leaveDays 3}。一个技巧是总设置一条“默认流”不设条件用于兜底。检查点2是否在完成任务后还有异步作业如Service Task没执行完可以查看ACT_RU_JOB表。检查点3用runtimeService.createExecutionQuery().processInstanceId(instanceId).list()查询当前所有执行流看它们停在了哪个节点ID上再对照流程图分析。问题三流程变量取不到或值为null检查点1分清变量作用域。在任务中通过taskService.setVariableLocal设置的是任务变量只在当前任务有效。如果要在流程范围内使用请用runtimeService.setVariable。检查点2变量类型要匹配。在Java代码里你放进去的是一个Integer但在网关条件表达式${leaveDays 3}中引擎会进行类型转换。如果变量是字符串“5”这个表达式可能会出错。尽量保持类型一致。检查点3变量是否被误删了在某些节点如子流程结束可能会自动删除局部变量。5.2 监听器在流程关键点注入业务逻辑你肯定不想把所有的业务代码都写在完成任务的那一瞬间。比如无论流程是正常结束还是被取消都需要给申请人发送一条通知。这时执行监听器Execution Listener和任务监听器Task Listener就派上用场了。你可以在BPMN图的节点事件、任务、网关或连线上附加监听器指定在事件发生如start,end,take时触发一段Java代码或表达式。例如在流程结束时发送通知在结束事件上添加一个执行监听器end事件。endEvent idendEvent1 name请假结束 extensionElements activiti:executionListener eventend classcom.yourcompany.listener.ProcessEndNotificationListener/ /extensionElements /endEvent对应的Java类public class ProcessEndNotificationListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { String processInstanceId execution.getProcessInstanceId(); String applicant (String) execution.getVariable(applicant); // 调用你的消息服务通知申请人流程已结束 messageService.sendNotify(applicant, “您的请假流程[“ processInstanceId “]已处理完毕。”); } }监听器让你能以“非侵入”的方式将业务逻辑钩入流程的生命周期保持核心流程的纯净这是设计清晰工作流应用的关键。5.3 动态指派与候选人模式我们之前的例子中处理人assignee都是硬编码或通过简单表达式指定的。现实中更常见的是动态指派比如“部门经理”是一个角色具体是谁要根据申请人的部门来定。1. 在表达式中调用Spring Bean你可以在BPMN的assignee属性中使用UEL表达式调用一个Spring容器中的Bean方法。userTask iddeptManagerApprove name部门经理审批 activiti:assignee${userService.findDeptManager(applicant)}/确保你的UserService是一个Spring Bean并且方法返回一个用户ID字符串。2. 候选人模式有时候一个任务可能需要多个人中的任意一个来处理如“技术支持组”中的任何一位工程师。这时可以用candidateUsers或candidateGroups。userTask idtechSupport name技术支持 activiti:candidateGroupstech_support_group/这样属于“tech_support_group”这个组的所有用户都能在任务列表中看到这个任务。其中一个人“认领”claim了任务后该任务就变成他的个人任务其他人就看不到了。// 查询候选组任务 ListTask candidateTasks taskService.createTaskQuery().taskCandidateGroup(“tech_support_group”).list(); // 某个用户认领任务 taskService.claim(taskId, currentUserId); // 完成任务... taskService.complete(taskId);候选人模式非常适合处理团队协作、抢单这类场景。6. 历史数据查询与流程状态追踪流程跑起来之后管理和监控就变得非常重要。谁发起了多少流程某个流程当前卡在哪个环节平均处理时间是多长这些都需要通过查询历史数据来回答。HistoryService是你的主要工具。6.1 核心历史查询APIHistoryService提供了丰富的查询接口可以构建非常复杂的查询条件。查询已结束的流程实例ListHistoricProcessInstance finishedProcesses historyService.createHistoricProcessInstanceQuery() .finished() // 只查已结束的 .processDefinitionKey(leaveProcess) .variableValueEquals(applicant, zhangsan) // 按流程变量查询 .orderByProcessInstanceEndTime().desc() .listPage(0, 10);这个查询非常强大它允许你根据流程变量来过滤历史实例这对于业务查询至关重要。比如产品经理想查“张三”的所有请假记录你不需要自己去关联业务表直接通过变量applicant就能查到。查询某个流程实例的所有活动历史ListHistoricActivityInstance activities historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime().asc() .list(); for (HistoricActivityInstance activity : activities) { System.out.println(节点: activity.getActivityName() , 开始: activity.getStartTime() , 结束: activity.getEndTime() , 耗时(ms): activity.getDurationInMillis()); }这个列表能完整还原一个流程实例的生命轨迹哪个任务由谁在什么时间处理了多久一目了然。这是做流程效率分析和优化的基础数据。查询历史任务ListHistoricTaskInstance hisTasks historyService.createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId) .taskAssignee(lisi) .finished() .list();6.2 可视化流程状态追踪光有数据还不够如果能给用户展示一张流程图并高亮显示当前流程已经走过的节点和当前停留的节点体验会好很多。Activiti提供了生成流程实例当前状态图的能力。Service public class ProcessImageService { Autowired private ProcessEngine processEngine; public InputStream generateProcessDiagram(String processInstanceId) throws IOException { // 1. 获取流程实例 ProcessInstance processInstance runtimeService.createProcessInstanceQuery() .processInstanceId(processInstanceId).singleResult(); if (processInstance null) { return null; // 流程可能已结束 } // 2. 获取流程定义ID String processDefinitionId processInstance.getProcessDefinitionId(); // 3. 获取BPMN模型 BpmnModel bpmnModel repositoryService.getBpmnModel(processDefinitionId); // 4. 获取当前活动的节点ID ListString activeActivityIds runtimeService.getActiveActivityIds(processInstanceId); // 5. 使用默认流程图生成器 ProcessDiagramGenerator diagramGenerator processEngine.getProcessEngineConfiguration() .getProcessDiagramGenerator(); // 生成图片流高亮当前活动节点红色高亮已流转的序列流绿色 InputStream imageStream diagramGenerator.generateDiagram(bpmnModel, png, activeActivityIds, Collections.emptyList(), // 高亮已流转的序列流需要额外计算这里先留空 processEngine.getProcessEngineConfiguration().getActivityFontName(), processEngine.getProcessEngineConfiguration().getLabelFontName(), processEngine.getProcessEngineConfiguration().getClassLoader(), 1.0, true); // 缩放比例是否绘制非活动节点 return imageStream; } }你可以将这个InputStream写入HTTP响应前端以图片形式展示。用户就能看到一张清晰的、带高亮状态的流程图这对于理解复杂流程的当前进展有巨大帮助。注意事项生成流程图是一个相对耗资源的操作尤其对于复杂流程图。在生产环境中要考虑对生成的图片进行缓存避免每次查看都重新生成。另外getActiveActivityIds方法对于并行网关的多分支场景返回的是所有并行分支上的活动节点这正好符合我们的高亮需求。7. 集成实践与你的用户系统对接很少有项目会直接使用Activiti自带的ACT_ID_*身份表。通常我们都有自己的用户、部门、角色体系。如何让Activiti的任务查询、候选人指派能对接我们自己的用户系统是落地时必须解决的问题。核心思路是实现Activiti的IdentityService相关接口或者更常见的在任务查询时进行“翻译”。7.1 任务查询的“翻译”策略假设你的系统用户表是sys_user部门表是sys_dept。当部门经理lisi登录后要查询他的待办任务你不能直接用taskService.createTaskQuery().taskAssignee(“lisi”)因为Activiti不认识你系统中的lisi。解决方案使用流程变量或扩展字段。在指派任务时存入实际用户ID。我们之前用${userService.findDeptManager(applicant)}这个表达式的结果就应该直接是你业务系统里的用户ID比如”U1002″。查询时获取当前登录用户的业务ID然后用这个ID去查询。public ListTaskDTO getMyTasks(String currentUserId) { // 1. 用业务用户ID直接查询Activiti任务 ListTask activitiTasks taskService.createTaskQuery() .taskAssignee(currentUserId) // 这里currentUserId就是”U1002″ .orderByTaskCreateTime().desc() .list(); // 2. 将Activiti任务转换为前端需要的DTO并丰富业务信息 ListTaskDTO taskDTOList new ArrayList(); for (Task task : activitiTasks) { TaskDTO dto convertToDTO(task); // 根据task.getProcessInstanceId()查询流程变量获取业务数据如请假单号、标题等 String businessKey runtimeService.createProcessInstanceQuery() .processInstanceId(task.getProcessInstanceId()) .singleResult() .getBusinessKey(); // 再根据businessKey去你的业务表查询详细信息填充到dto中 LeaveApply leaveApply leaveApplyService.getById(businessKey); dto.setBusinessTitle(leaveApply.getTitle()); // ... 其他信息 taskDTOList.add(dto); } return taskDTOList; }这种方式简单直接是最常用的集成方法。它要求你在流程设计时就将业务系统的用户ID作为任务的处理人。7.2 候选人组的集成对于候选人组思路类似。比如你有一个角色叫ROLE_DEPT_MANAGER。在流程图中你可以将任务候选人组设置为一个通用的标识符如role:dept_manager。userTask iddeptManagerApprove name部门经理审批 activiti:candidateGroupsrole:dept_manager/当需要查询某个用户的所有候选任务时你需要先根据当前用户ID查出他所属的所有角色如[“role:dept_manager”, “role:project_lead”]。然后用这些角色标识符去Activiti中查询候选组任务。public ListTask getMyCandidateTasks(String currentUserId) { // 1. 从你的系统获取当前用户的所有角色/组标识符列表 ListString myRoleIdentifiers userService.getUserRoles(currentUserId); // 返回 [role:dept_manager, ...] // 2. 构建查询查询这些组下的所有任务 TaskQuery query taskService.createTaskQuery(); if (myRoleIdentifiers ! null !myRoleIdentifiers.isEmpty()) { query.taskCandidateGroupIn(myRoleIdentifiers); // 使用in查询 } // 可能还需要加上个人候选任务taskCandidateUser // query.taskCandidateOrAssigned(currentUserId); 这个方法可以同时查询指派给该用户和该用户候选的任务 return query.list(); }当用户从任务列表“认领”一个候选组任务时调用taskService.claim(taskId, currentUserId)即可。这样就实现了基于你自身角色系统的动态任务分配。7.3 自定义身份管理高级如果你的集成需求非常复杂可以考虑实现Activiti的IdentityService接口完全接管用户和组的管理。但这会涉及更多底层操作如重写UserQuery、GroupQuery等将查询转发到你的业务数据库。对于大部分项目而言上述的“查询翻译”策略已经足够简洁有效侵入性也更低。绕开这些初期常见的“坑”并善用监听器、动态指派和历史查询你的Activiti应用就已经具备了相当的健壮性和实用性。工作流引擎的魅力在于它将流程的控制权从代码中解放出来让业务逻辑的流转变得可视、可管、可追溯。当你熟悉了这些基础组件和模式后便可以进一步探索更复杂的场景如并行会签、子流程、事件驱动、异步任务等以应对更加多变的业务需求。
Activiti工作流引擎入门:核心概念、Spring Boot集成与实战避坑指南
1. 项目概述为什么我们需要一个工作流引擎如果你在开发企业级应用尤其是涉及审批、报销、请假这类流程化业务时一定遇到过这样的场景业务逻辑像面条一样缠绕在代码里今天销售说要加一个总监审批节点明天财务说报销金额超过5000要走特殊流程。每次改动你都得小心翼翼地在一堆if-else和状态判断里穿梭生怕改出个Bug。更头疼的是流程的流转状态、当前处理人、历史记录这些数据管理起来也是一团乱麻。这时候一个专门负责流程编排和执行的“引擎”就显得至关重要了。Activiti就是这样一个在Java世界里经久不衰的开源工作流引擎。简单来说Activiti把业务流程从你的业务代码中抽离出来用一种可视化的流程图BPMN 2.0标准来定义。你只需要告诉它流程怎么走画图谁来处理配置处理人引擎就会自动帮你驱动流程实例管理任务分配记录每一步的痕迹。这带来的好处是显而易见的业务变更只需修改流程图无需动核心代码流程状态清晰可追溯而且它天然支持高并发和分布式部署为复杂的企业流程提供了坚实的底盘。我最早接触Activiti是在一个OA系统项目里当时手动撸了一套审批状态机后期维护简直是一场噩梦。引入Activiti后开发效率提升了产品经理甚至能自己用设计器调整流程那种解放生产力的感觉至今记忆犹新。接下来我会从一个实践者的角度带你彻底搞懂Activiti的核心概念、环境搭建和第一个流程的落地避开我当年踩过的那些坑。2. 核心概念全景图理解引擎的“零件”在动手写代码之前我们必须先理解Activiti里那些核心“零件”是什么以及它们是如何协作的。很多人一上来就照着例子跑通了一个请假流程但稍微变点需求就懵了根本原因就是概念没吃透。2.1 流程定义与流程实例蓝图与房子的关系这是最基础也最重要的一对概念。流程定义Process Definition就是你用BPMN 2.0画出来的那张流程图它是对业务流程的静态描述好比一张建筑的蓝图。这张图里定义了流程有哪些步骤、步骤之间的连线、每个步骤由谁处理等等。在Activiti中你通常会将一个.bpmn20.xml文件部署到引擎中这就创建了一个流程定义。而流程实例Process Instance则是根据这张蓝图具体盖起来的一栋房子。当某个员工发起一个请假申请时Activiti就会依据“请假流程”这个定义创建一个具体的流程实例。一个流程定义可以对应无数个流程实例就像一张蓝图可以盖出无数栋结构相同的房子但每栋房子里的住户、装修业务数据都是独立的。理解这个区别至关重要。你修改了流程定义比如增加一个审批节点通常不会影响已经正在运行的流程实例它们会按照旧的蓝图继续走下去。除非你特意做了流程定义的版本控制和新实例的迁移这涉及到更高级的用法。2.2 任务与执行流引擎驱动的核心单元流程实例启动后是如何一步步向前推进的呢这依赖于任务Task和执行流Execution这两个核心驱动机制。任务是流程中需要由“人”或“系统”去完成的一个工作单元。最常见的就是“用户任务”比如“部门经理审批”。当流程流转到这样一个节点时Activiti会在数据库中创建一条任务记录并将它分配给指定的处理人或候选组。处理人通过任务列表看到它完成审批后流程才会继续向下走。执行流则是一个更为底层的概念你可以把它理解为流程运行时的一个“指针”或“执行线程”。它沿着流程定义中的连线移动经过哪个节点就触发哪个节点的行为如创建任务、调用Java类、发送消息。一个流程实例在启动时会创建一个主执行流。当遇到并行网关Parallel Gateway时一个执行流会分裂成多个同时推进多条分支当分支在汇聚点汇合时多个执行流又会合并成一个。我曾经在排查一个并行审批流程卡住的问题时就是通过查询数据库中的ACT_RU_EXECUTION表发现有一条分支的执行流没有到达汇聚点从而定位到是某个节点的“完成任务”逻辑没有正确被调用。2.3 数据与上下文流程的“记忆”流程在运行过程中需要携带和传递数据。Activiti提供了多种作用域的变量来充当流程的“记忆”。流程变量Process Variables作用域是整个流程实例。比如请假单的applicant申请人、leaveDays请假天数这些信息在整个流程生命周期中都需要被访问和修改。它是最常用的一种变量。任务变量Task Variables作用域仅限于单个任务。通常用于存储该任务处理过程中产生的临时数据或者任务表单的数据。任务一旦完成这些变量默认不会自动提升为流程变量。本地变量Local Variables作用域是某个执行流。它比流程变量范围更小通常用于一些复杂的网关条件判断或脚本任务中的中间计算。变量是连接工作流引擎和业务系统的桥梁。业务数据通过变量传入引擎驱动流程分支判断比如leaveDays 3需要总监审批流程中的处理结果也通过变量传出用于更新业务状态。这里一个常见的坑是混淆变量作用域在任务中设置了变量却期望在流程其他地方能读到结果发现是null。3. 环境搭建与核心配置实战理论说得再多不如动手搭一个。这里我以Spring Boot 2.7.x Activiti 7为例带你走一遍最简化的集成流程。选择Activiti 7是因为它原生支持Spring Boot自动化配置程度高对新手更友好。3.1 依赖引入与基础配置首先在你的pom.xml中引入关键依赖。注意Activiti 7的Spring Boot Starter已经包含了引擎、MyBatisActiviti的持久层框架等所有必需组件。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引入依赖后Spring Boot的自动配置会为你准备好几乎一切。但有几个关键配置我建议你在application.yml中显式地设置一下以便更好地理解和控制引擎行为。spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1 username: sa password: driver-class-name: org.h2.Driver activiti: # 是否检查流程定义文件开发环境建议true生产环境可设为false提升启动速度 check-process-definitions: true # 数据库架构更新策略false-启动时不检查创建表true-启动时检查不存在则创建create-drop-启动创建关闭删除 database-schema-update: true # 是否部署classpath下的流程定义文件 deployment-mode: default # 历史数据记录级别none-不记录activity-记录节点实例audit-记录节点实例任务实例变量full-最全记录默认 history-level: audit重点说一下database-schema-update和history-level。第一次启动时务必设为true让Activiti自动创建那28张对你没看错就是28张运行时、历史、身份等表。history-level我推荐用audit它记录了流程实例、任务、变量足以满足绝大部分审计和查询需求又比full级别轻量。full级别会记录所有细节包括所有变量的每次变更数据量巨大除非有严格的合规要求否则慎用。3.2 数据库表结构初窥启动应用后你可以连接到H2控制台如果用了H2查看自动生成的表。这些表都以ACT_为前缀并按照功能分为几大类ACT_RE_*:RE表示repository存储。主要是静态的部署信息如ACT_RE_PROCDEF流程定义表、ACT_RE_DEPLOYMENT部署单元表。ACT_RU_*:RU表示runtime运行时。存储正在运行的流程实例、任务、变量等。这些数据是引擎运行的核心流程结束后相应的记录会被删除除非历史级别要求保留。ACT_HI_*:HI表示history历史。存储所有已经结束的流程实例的历史数据用于查询和审计。ACT_ID_*:ID表示identity身份。存储用户、组以及它们之间的关系。但很多项目会使用自己的用户体系而不用Activiti自带的这套。ACT_GE_*:GE表示general通用。如ACT_GE_BYTEARRAY存储流程定义图、流程实例图等二进制资源。实操心得刚开始不必死记每张表的字段但要知道这几大类表是干什么的。以后排查问题比如“流程为什么卡住了”就去查ACT_RU_TASK和ACT_RU_EXECUTION“想查上个月的所有审批”就去查ACT_HI_PROCINST和ACT_HI_TASKINST。这个分类思维能帮你快速定位问题。3.3 核心服务Bean注入Activiti的核心功能通过一系列Service接口提供在Spring Boot中它们已经被自动配置为Bean你可以直接Autowired注入使用。最常用的有以下几个RepositoryService: 流程仓库服务。负责管理部署、删除、查询流程定义。RuntimeService: 运行时服务。负责启动流程实例、管理流程变量、触发信号等。TaskService: 任务服务。这是前端交互最频繁的服务负责查询用户任务、完成任务、管理任务变量、设置任务处理人等。HistoryService: 历史服务。查询所有已经结束的流程和任务的历史数据。IdentityService: 身份服务。管理用户和组但通常我们集成自己的用户系统。在你的Service或Controller中可以这样注入并使用它们Service public class LeaveApplyService { Autowired private RepositoryService repositoryService; Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; // ... 你的业务方法 }环境搭好核心概念和组件也清楚了我们终于可以开始设计并运行第一个真正的工作流了。4. 第一个工作流从流程图到代码实现让我们用一个最经典的“员工请假流程”作为入门示例。流程很简单员工提交申请 → 部门经理审批 → 如果请假天数大于3天需要总监审批 → 最后流程结束。我们将分三步走画图、部署、写代码驱动。4.1 使用BPMN 2.0绘制流程图虽然可以用XML直接写.bpmn20.xml文件但我强烈建议使用可视化设计器。Eclipse有Activiti Designer插件在线工具如https://bpmn.io也非常好用。这里我描述一下关键节点开始事件Start Event: 圆形表示流程开始。用户任务User Task: 圆角矩形代表需要人工处理的任务。“提交请假申请”设置处理人为#{applicant}一个流程变量动态指定。“部门经理审批”设置处理人为deptManager可以是一个固定ID或从变量中获取。“总监审批”设置处理人为director。排他网关Exclusive Gateway: 菱形。用于做决策只有一条路径会被选择。我们用在“部门经理审批”之后判断leaveDays 3。序列流Sequence Flow: 带箭头的线连接各个节点。从排他网关引出的线需要设置条件表达式Condition Expression例如${leaveDays 3}和${leaveDays 3}。结束事件End Event: 粗边圆形表示流程结束。将画好的图保存为leave-process.bpmn20.xml并放到Spring Boot项目的src/main/resources/processes/目录下。Activiti会自动扫描并部署它。4.2 部署流程定义与启动实例部署操作一般只在流程有变更时才需要。由于我们配置了spring.activiti.check-process-definitions: true且文件放对了位置应用启动时就已经自动部署了。但我们也可以通过代码手动部署更可控public String deployProcess() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leave-process.bpmn20.xml) .name(员工请假流程V1) .deploy(); // 执行部署 System.out.println(部署成功部署ID: deployment.getId()); // 通常我们需要根据部署ID查询到具体的流程定义 ProcessDefinition processDefinition repositoryService.createProcessDefinitionQuery() .deploymentId(deployment.getId()) .singleResult(); return processDefinition.getId(); // 返回流程定义ID }部署成功后就可以启动流程实例了。启动时必须传入业务数据也就是流程变量。public String startLeaveProcess(String applicant, Integer leaveDays, String businessKey) { // 准备流程变量 MapString, Object variables new HashMap(); variables.put(applicant, applicant); variables.put(leaveDays, leaveDays); // 用流程定义的Key来启动实例businessKey通常关联你的业务主键如请假单ID ProcessInstance instance runtimeService.startProcessInstanceByKey(leaveProcess, businessKey, variables); System.out.println(流程实例启动成功实例ID: instance.getId()); return instance.getId(); }这里有几个关键点startProcessInstanceByKey中的leaveProcess是你BPMN文件中process idleaveProcess ...的id属性不是文件名。businessKey非常重要它是连接工作流实例和你的业务数据的桥梁。比如你的请假单表有一条记录ID为1001那么就把1001作为businessKey传入。之后你就可以用这个Key来查询对应的流程实例。变量applicant会被用于第一个用户任务“提交请假申请”的处理人指派因为我们在图中设置了#{applicant}。4.3 任务查询与完成流程实例启动后第一个任务“提交请假申请”会自动创建并分配给applicant变量指定的用户。现在我们需要模拟用户登录系统后查询并处理自己的任务。// 1. 查询某个用户的任务列表 public ListTask getTasksByAssignee(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) // 指定处理人 .orderByTaskCreateTime().desc() // 按创建时间排序 .list(); } // 2. 完成任务并可能传递新的变量 public void completeTask(String taskId, MapString, Object taskVariables) { // 完成任务时可以传入新的变量这些变量默认是任务变量 // 如果需要设置为流程变量可以在完成任务前通过runtimeService.setVariable设置 if (taskVariables ! null !taskVariables.isEmpty()) { // 方式一作为任务变量完成任务后如需保留需手动提升 // taskService.complete(taskId, taskVariables); // 方式二推荐直接设置为流程变量 for (Map.EntryString, Object entry : taskVariables.entrySet()) { runtimeService.setVariable(taskService.createTaskQuery().taskId(taskId).singleResult().getProcessInstanceId(), entry.getKey(), entry.getValue()); } } taskService.complete(taskId); System.out.println(任务 taskId 已完成。); }例如员工zhangsan提交申请后部门经理lisi登录查询自己的任务进行审批// 部门经理查询任务 ListTask deptManagerTasks taskService.createTaskQuery().taskAssignee(lisi).list(); for (Task task : deptManagerTasks) { System.out.println(待审批任务 task.getName() , 流程实例ID: task.getProcessInstanceId()); // 可以在此处通过runtimeService.getVariables获取流程变量如请假天数用于前端展示 MapString, Object processVars runtimeService.getVariables(task.getProcessInstanceId()); Integer days (Integer) processVars.get(leaveDays); System.out.println(请假天数 days); } // 假设任务ID是”task123“部门经理同意并填写审批意见 MapString, Object approveVars new HashMap(); approveVars.put(deptManagerComment, 情况属实同意。); // 注意这里完成的是“部门经理审批”这个任务 taskService.complete(task123, approveVars);当taskService.complete(“task123”)被调用后Activiti引擎会自动驱动流程向下执行经过排他网关根据leaveDays变量的值判断走向如果大于3天就会自动创建下一个“总监审批”任务并分配给director用户。至此一个完整的工作流从定义、部署、启动到流转我们就全部跑通了。你可以通过H2控制台观察ACT_RU_TASK和ACT_HI_ACTINST等表的数据变化直观地理解引擎的内部运作。5. 避坑指南与高级特性初探在基本流程跑通的基础上分享几个初期最容易踩的坑以及两个能立刻提升效率的高级特性。5.1 常见问题排查实录问题一流程启动后第一个任务没创建检查点1确认BPMN文件中的process的id和启动时代码中的processDefinitionKey是否完全一致大小写敏感。检查点2查看ACT_RU_EXECUTION表是否有你刚启动的流程实例记录如果没有可能是启动失败。如果有再看ACT_RU_TASK表看任务是否创建。如果执行流存在但任务没创建大概率是流程图第一个节点不是“用户任务”或者该用户任务的assignee或candidateUsers表达式解析为空。检查点3打开历史日志级别在application.yml中设置logging.level.org.activiti: DEBUG查看引擎执行的详细日志通常会明确报错。问题二任务完成了但流程没往下走卡住了检查点1最可能的原因是排他网关的条件表达式写错了。检查从网关出去的每一条序列流上的条件。确保使用的变量名和流程变量中的一致表达式语法正确如${leaveDays 3}。一个技巧是总设置一条“默认流”不设条件用于兜底。检查点2是否在完成任务后还有异步作业如Service Task没执行完可以查看ACT_RU_JOB表。检查点3用runtimeService.createExecutionQuery().processInstanceId(instanceId).list()查询当前所有执行流看它们停在了哪个节点ID上再对照流程图分析。问题三流程变量取不到或值为null检查点1分清变量作用域。在任务中通过taskService.setVariableLocal设置的是任务变量只在当前任务有效。如果要在流程范围内使用请用runtimeService.setVariable。检查点2变量类型要匹配。在Java代码里你放进去的是一个Integer但在网关条件表达式${leaveDays 3}中引擎会进行类型转换。如果变量是字符串“5”这个表达式可能会出错。尽量保持类型一致。检查点3变量是否被误删了在某些节点如子流程结束可能会自动删除局部变量。5.2 监听器在流程关键点注入业务逻辑你肯定不想把所有的业务代码都写在完成任务的那一瞬间。比如无论流程是正常结束还是被取消都需要给申请人发送一条通知。这时执行监听器Execution Listener和任务监听器Task Listener就派上用场了。你可以在BPMN图的节点事件、任务、网关或连线上附加监听器指定在事件发生如start,end,take时触发一段Java代码或表达式。例如在流程结束时发送通知在结束事件上添加一个执行监听器end事件。endEvent idendEvent1 name请假结束 extensionElements activiti:executionListener eventend classcom.yourcompany.listener.ProcessEndNotificationListener/ /extensionElements /endEvent对应的Java类public class ProcessEndNotificationListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { String processInstanceId execution.getProcessInstanceId(); String applicant (String) execution.getVariable(applicant); // 调用你的消息服务通知申请人流程已结束 messageService.sendNotify(applicant, “您的请假流程[“ processInstanceId “]已处理完毕。”); } }监听器让你能以“非侵入”的方式将业务逻辑钩入流程的生命周期保持核心流程的纯净这是设计清晰工作流应用的关键。5.3 动态指派与候选人模式我们之前的例子中处理人assignee都是硬编码或通过简单表达式指定的。现实中更常见的是动态指派比如“部门经理”是一个角色具体是谁要根据申请人的部门来定。1. 在表达式中调用Spring Bean你可以在BPMN的assignee属性中使用UEL表达式调用一个Spring容器中的Bean方法。userTask iddeptManagerApprove name部门经理审批 activiti:assignee${userService.findDeptManager(applicant)}/确保你的UserService是一个Spring Bean并且方法返回一个用户ID字符串。2. 候选人模式有时候一个任务可能需要多个人中的任意一个来处理如“技术支持组”中的任何一位工程师。这时可以用candidateUsers或candidateGroups。userTask idtechSupport name技术支持 activiti:candidateGroupstech_support_group/这样属于“tech_support_group”这个组的所有用户都能在任务列表中看到这个任务。其中一个人“认领”claim了任务后该任务就变成他的个人任务其他人就看不到了。// 查询候选组任务 ListTask candidateTasks taskService.createTaskQuery().taskCandidateGroup(“tech_support_group”).list(); // 某个用户认领任务 taskService.claim(taskId, currentUserId); // 完成任务... taskService.complete(taskId);候选人模式非常适合处理团队协作、抢单这类场景。6. 历史数据查询与流程状态追踪流程跑起来之后管理和监控就变得非常重要。谁发起了多少流程某个流程当前卡在哪个环节平均处理时间是多长这些都需要通过查询历史数据来回答。HistoryService是你的主要工具。6.1 核心历史查询APIHistoryService提供了丰富的查询接口可以构建非常复杂的查询条件。查询已结束的流程实例ListHistoricProcessInstance finishedProcesses historyService.createHistoricProcessInstanceQuery() .finished() // 只查已结束的 .processDefinitionKey(leaveProcess) .variableValueEquals(applicant, zhangsan) // 按流程变量查询 .orderByProcessInstanceEndTime().desc() .listPage(0, 10);这个查询非常强大它允许你根据流程变量来过滤历史实例这对于业务查询至关重要。比如产品经理想查“张三”的所有请假记录你不需要自己去关联业务表直接通过变量applicant就能查到。查询某个流程实例的所有活动历史ListHistoricActivityInstance activities historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime().asc() .list(); for (HistoricActivityInstance activity : activities) { System.out.println(节点: activity.getActivityName() , 开始: activity.getStartTime() , 结束: activity.getEndTime() , 耗时(ms): activity.getDurationInMillis()); }这个列表能完整还原一个流程实例的生命轨迹哪个任务由谁在什么时间处理了多久一目了然。这是做流程效率分析和优化的基础数据。查询历史任务ListHistoricTaskInstance hisTasks historyService.createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId) .taskAssignee(lisi) .finished() .list();6.2 可视化流程状态追踪光有数据还不够如果能给用户展示一张流程图并高亮显示当前流程已经走过的节点和当前停留的节点体验会好很多。Activiti提供了生成流程实例当前状态图的能力。Service public class ProcessImageService { Autowired private ProcessEngine processEngine; public InputStream generateProcessDiagram(String processInstanceId) throws IOException { // 1. 获取流程实例 ProcessInstance processInstance runtimeService.createProcessInstanceQuery() .processInstanceId(processInstanceId).singleResult(); if (processInstance null) { return null; // 流程可能已结束 } // 2. 获取流程定义ID String processDefinitionId processInstance.getProcessDefinitionId(); // 3. 获取BPMN模型 BpmnModel bpmnModel repositoryService.getBpmnModel(processDefinitionId); // 4. 获取当前活动的节点ID ListString activeActivityIds runtimeService.getActiveActivityIds(processInstanceId); // 5. 使用默认流程图生成器 ProcessDiagramGenerator diagramGenerator processEngine.getProcessEngineConfiguration() .getProcessDiagramGenerator(); // 生成图片流高亮当前活动节点红色高亮已流转的序列流绿色 InputStream imageStream diagramGenerator.generateDiagram(bpmnModel, png, activeActivityIds, Collections.emptyList(), // 高亮已流转的序列流需要额外计算这里先留空 processEngine.getProcessEngineConfiguration().getActivityFontName(), processEngine.getProcessEngineConfiguration().getLabelFontName(), processEngine.getProcessEngineConfiguration().getClassLoader(), 1.0, true); // 缩放比例是否绘制非活动节点 return imageStream; } }你可以将这个InputStream写入HTTP响应前端以图片形式展示。用户就能看到一张清晰的、带高亮状态的流程图这对于理解复杂流程的当前进展有巨大帮助。注意事项生成流程图是一个相对耗资源的操作尤其对于复杂流程图。在生产环境中要考虑对生成的图片进行缓存避免每次查看都重新生成。另外getActiveActivityIds方法对于并行网关的多分支场景返回的是所有并行分支上的活动节点这正好符合我们的高亮需求。7. 集成实践与你的用户系统对接很少有项目会直接使用Activiti自带的ACT_ID_*身份表。通常我们都有自己的用户、部门、角色体系。如何让Activiti的任务查询、候选人指派能对接我们自己的用户系统是落地时必须解决的问题。核心思路是实现Activiti的IdentityService相关接口或者更常见的在任务查询时进行“翻译”。7.1 任务查询的“翻译”策略假设你的系统用户表是sys_user部门表是sys_dept。当部门经理lisi登录后要查询他的待办任务你不能直接用taskService.createTaskQuery().taskAssignee(“lisi”)因为Activiti不认识你系统中的lisi。解决方案使用流程变量或扩展字段。在指派任务时存入实际用户ID。我们之前用${userService.findDeptManager(applicant)}这个表达式的结果就应该直接是你业务系统里的用户ID比如”U1002″。查询时获取当前登录用户的业务ID然后用这个ID去查询。public ListTaskDTO getMyTasks(String currentUserId) { // 1. 用业务用户ID直接查询Activiti任务 ListTask activitiTasks taskService.createTaskQuery() .taskAssignee(currentUserId) // 这里currentUserId就是”U1002″ .orderByTaskCreateTime().desc() .list(); // 2. 将Activiti任务转换为前端需要的DTO并丰富业务信息 ListTaskDTO taskDTOList new ArrayList(); for (Task task : activitiTasks) { TaskDTO dto convertToDTO(task); // 根据task.getProcessInstanceId()查询流程变量获取业务数据如请假单号、标题等 String businessKey runtimeService.createProcessInstanceQuery() .processInstanceId(task.getProcessInstanceId()) .singleResult() .getBusinessKey(); // 再根据businessKey去你的业务表查询详细信息填充到dto中 LeaveApply leaveApply leaveApplyService.getById(businessKey); dto.setBusinessTitle(leaveApply.getTitle()); // ... 其他信息 taskDTOList.add(dto); } return taskDTOList; }这种方式简单直接是最常用的集成方法。它要求你在流程设计时就将业务系统的用户ID作为任务的处理人。7.2 候选人组的集成对于候选人组思路类似。比如你有一个角色叫ROLE_DEPT_MANAGER。在流程图中你可以将任务候选人组设置为一个通用的标识符如role:dept_manager。userTask iddeptManagerApprove name部门经理审批 activiti:candidateGroupsrole:dept_manager/当需要查询某个用户的所有候选任务时你需要先根据当前用户ID查出他所属的所有角色如[“role:dept_manager”, “role:project_lead”]。然后用这些角色标识符去Activiti中查询候选组任务。public ListTask getMyCandidateTasks(String currentUserId) { // 1. 从你的系统获取当前用户的所有角色/组标识符列表 ListString myRoleIdentifiers userService.getUserRoles(currentUserId); // 返回 [role:dept_manager, ...] // 2. 构建查询查询这些组下的所有任务 TaskQuery query taskService.createTaskQuery(); if (myRoleIdentifiers ! null !myRoleIdentifiers.isEmpty()) { query.taskCandidateGroupIn(myRoleIdentifiers); // 使用in查询 } // 可能还需要加上个人候选任务taskCandidateUser // query.taskCandidateOrAssigned(currentUserId); 这个方法可以同时查询指派给该用户和该用户候选的任务 return query.list(); }当用户从任务列表“认领”一个候选组任务时调用taskService.claim(taskId, currentUserId)即可。这样就实现了基于你自身角色系统的动态任务分配。7.3 自定义身份管理高级如果你的集成需求非常复杂可以考虑实现Activiti的IdentityService接口完全接管用户和组的管理。但这会涉及更多底层操作如重写UserQuery、GroupQuery等将查询转发到你的业务数据库。对于大部分项目而言上述的“查询翻译”策略已经足够简洁有效侵入性也更低。绕开这些初期常见的“坑”并善用监听器、动态指派和历史查询你的Activiti应用就已经具备了相当的健壮性和实用性。工作流引擎的魅力在于它将流程的控制权从代码中解放出来让业务逻辑的流转变得可视、可管、可追溯。当你熟悉了这些基础组件和模式后便可以进一步探索更复杂的场景如并行会签、子流程、事件驱动、异步任务等以应对更加多变的业务需求。