从零构建:基于Activiti 7.x的企业级流程自动化实战指南
你是否曾为繁琐的报销审批流程而等待数日?是否经历过因流程节点不清导致的任务卡顿?在数字化转型浪潮中,企业运营效率的核心往往不在于某个炫酷的功能,而在于那些支撑日常业务流转的“隐形骨架”——业务流程。对于Java开发者而言,掌握一套成熟、稳定的流程引擎技术,就如同为业务系统装上了智能导航,能够将复杂的规则逻辑转化为清晰、自动化的执行路径。
今天,我们将深入探索业界广泛使用的开源流程引擎Activiti的最新版本——7.x系列。与网络上泛泛而谈的概念介绍不同,本文将以一名一线架构师的视角,带你从环境搭建、流程设计、表单绑定,到高级特性与生产环境避坑,完成一次完整的、可落地的实战之旅。无论你是希望为现有系统引入工作流能力,还是正在评估技术选型,这篇文章都将提供一套经过验证的实践方案。
1. 环境准备与项目初始化
在开始编写第一行流程代码之前,一个稳定、兼容的开发环境是成功的基石。Activiti 7.x 与 Spring Boot 2.x+ 进行了深度整合,这极大地简化了传统工作流引擎繁重的配置工作。然而,版本间的细微差异往往是新手遇到的第一个“暗礁”。
首先,我们通过Spring Initializr或手动创建一个标准的Spring Boot项目。核心依赖除了Spring Boot Starter Web、JPA等常规组件外,关键在于引入Activiti Spring Boot Starter。这里有一个常见的版本匹配陷阱需要注意:Activiti 7.1.x 通常与 Spring Boot 2.3.x ~ 2.7.x 兼容性最佳。盲目使用最新版本可能导致意料之外的冲突。
Maven核心依赖配置示例:
<properties>
<activiti.version>7.1.0.M6</activiti.version>
<spring-boot.version>2.7.18</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- 使用H2数据库作为快速开始的嵌入式数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 生产环境推荐使用MySQL或PostgreSQL -->
<!-- <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency> -->
</dependencies>
注意:Activiti 7.x 默认使用了新的
activiti-spring-boot-starter,它内部整合了流程引擎、Spring Security(用于任务分配和权限)等。如果你的项目已有自己的安全框架,需要仔细处理配置,避免冲突。
数据库是流程引擎的“记忆中枢”。Activiti 在启动时会自动检查数据库表结构,若不存在则根据依赖的数据库方言(如MySQL, PostgreSQL, H2)自动创建约25张核心表。这些表主要分为五大类:
| 表前缀 | 全称 | 存储内容说明 |
|---|---|---|
| ACT_RE_ | Repository | 流程定义、模型等静态部署资源。 |
| ACT_RU_ | Runtime | 运行时数据,如流程实例、执行流、任务、变量。流程结束时这部分数据会被清除。 |
| ACT_HI_ | History | 历史数据,包括已完成的流程实例、任务、变量详情,用于审计与分析。 |
| ACT_ID_ | Identity | 身份信息,如用户、用户组及其关系。 |
| ACT_GE_ | General | 通用数据,如存储流程定义图片、部署文件等二进制资源。 |
完成依赖配置后,一个最简单的 application.yml 配置文件可能如下所示:
spring:
datasource:
url: jdbc:h2:mem:activitidb;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
# Activiti 配置
activiti:
database-schema-update: true # 自动更新数据库表结构
db-history-used: true # 使用历史数据
history-level: audit # 历史记录级别:none, activity, audit, full
check-process-definitions: false # 启动时不强制检查流程定义文件
async-executor-activate: false # 开发环境可关闭异步执行器以简化调试
启动你的Spring Boot应用,如果控制台没有报错,并且能在H2控制台(默认地址 http://localhost:8080/h2-console,JDBC URL填写 jdbc:h2:mem:activitidb)中看到一系列 ACT_ 开头的表,那么恭喜你,流程引擎的基础环境已经就绪。
2. 第一个流程:请假审批的设计与实现
理论铺垫完毕,现在让我们动手创建一个实实在在的流程。我们将以实现一个经典的“员工请假审批”流程为例,它包含了用户任务(User Task)、网关(Gateway)等核心元素。
第一步:使用BPMN 2.0设计流程 Activiti遵循BPMN 2.0(业务流程模型与标注)国际标准。你可以使用官方Eclipse插件、在线工具如bpmn.io,或者IntelliJ IDEA的Activiti BPMN插件来绘制流程图。这里我们直接编写BPMN 2.0 XML文件,这有助于理解其本质。
在 src/main/resources/processes/ 目录下创建 leave-request.bpmn20.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:activiti="http://activiti.org/bpmn"
targetNamespace="http://www.activiti.org/processdef">
<!-- 定义一个流程,id用于代码中引用,name用于显示 -->
<process id="leaveProcess" name="员工请假流程" isExecutable="true">
<!-- 开始事件 -->
<startEvent id="startEvent" name="开始申请">
<extensionElements>
<!-- 定义启动时需要填写的表单字段 -->
<activiti:formProperty id="applicant" name="申请人" type="string" required="true"/>
<activiti:formProperty id="leaveType" name="请假类型" type="enum" required="true">
<activiti:value id="annual" name="年假"/>
<activiti:value id="sick" name="病假"/>
<activiti:value id="personal" name="事假"/>
</activiti:formProperty>
<activiti:formProperty id="days" name="请假天数" type="long" required="true"/>
<activiti:formProperty id="reason" name="事由" type="string"/>
</extensionElements>
</startEvent>
<!-- 第一个用户任务:员工提交申请 -->
<userTask id="submitRequest" name="提交请假申请" activiti:assignee="${applicant}">
<documentation>
员工 ${applicant} 提交了 ${days} 天 ${leaveType} 申请,事由:${reason}。
</documentation>
</userTask>
<!-- 顺序流:从开始事件指向提交任务 -->
<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="submitRequest"/>
<!-- 第二个用户任务:部门经理审批 -->
<userTask id="deptManagerApprove" name="部门经理审批" activiti:candidateGroups="dept_manager">
<extensionElements>
<!-- 审批动作和意见 -->
<activiti:formProperty id="managerApproved" name="是否批准" type="enum" required="true">
<activiti:value id="true" name="批准"/>
<activiti:value id="false" name="拒绝"/>
</activiti:formProperty>
<activiti:formProperty id="managerComment" name="审批意见" type="string"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow2" sourceRef="submitRequest" targetRef="deptManagerApprove"/>
<!-- 排他网关:根据经理审批结果决定流向 -->
<exclusiveGateway id="decisionGateway" name="审批决定"/>
<sequenceFlow id="flowToHR" sourceRef="deptManagerApprove" targetRef="decisionGateway"/>
<!-- 条件顺序流:如果批准且天数>3天,需HR审批 -->
<sequenceFlow id="flow3" sourceRef="decisionGateway" targetRef="hrApprove">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${managerApproved == 'true' && days > 3}]]>
</conditionExpression>
</sequenceFlow>
<!-- HR审批任务 -->
<userTask id="hrApprove" name="人力资源部审批" activiti:candidateGroups="hr">
<extensionElements>
<activiti:formProperty id="hrApproved" name="HR审批结果" type="enum" required="true">
<activiti:value id="true" name="最终批准"/>
<activiti:value id="false" name="驳回"/>
</activiti:formProperty>
</extensionElements>
</userTask>
<sequenceFlow id="flow4" sourceRef="hrApprove" targetRef="gatewayAfterHR"/>
<!-- 另一个网关,处理HR审批后的逻辑 -->
<exclusiveGateway id="gatewayAfterHR" name="HR审批后决定"/>
<sequenceFlow id="flow5" sourceRef="gatewayAfterHR" targetRef="sendApprovalNotification">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${hrApproved == 'true'}]]>
</conditionExpression>
</sequenceFlow>
<!-- 条件顺序流:如果经理直接拒绝,或HR驳回,流程结束 -->
<sequenceFlow id="flowToReject" sourceRef="decisionGateway" targetRef="rejectEnd">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${managerApproved == 'false'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flowToRejectByHR" sourceRef="gatewayAfterHR" targetRef="rejectEnd">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${hrApproved == 'false'}]]>
</conditionExpression>
</sequenceFlow>
<!-- 条件顺序流:如果批准且天数<=3天,直接进入通知环节 -->
<sequenceFlow id="flowToNotify" sourceRef="decisionGateway" targetRef="sendApprovalNotification">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${managerApproved == 'true' && days <= 3}]]>
</conditionExpression>
</sequenceFlow>
<!-- 服务任务:模拟发送通知邮件或消息 -->
<serviceTask id="sendApprovalNotification" name="发送批准通知" activiti:class="org.example.workflow.service.SendNotificationDelegate"/>
<sequenceFlow id="flow6" sourceRef="sendApprovalNotification" targetRef="endEvent"/>
<!-- 结束事件 -->
<endEvent id="endEvent" name="请假流程结束"/>
<endEvent id="rejectEnd" name="申请被拒绝"/>
</process>
</definitions>
这个XML定义了一个相对完整的流程:员工发起申请 -> 部门经理审批 -> 根据结果和天数判断是否需HR审批 -> 最终通知或拒绝。其中使用了 activiti:assignee(指定处理人)和 activiti:candidateGroups(指定候选用户组)来分配任务。
第二步:部署流程定义
将BPMN文件放入 resources/processes/ 目录后,Activiti会在应用启动时自动部署。你也可以通过API动态部署:
@Service
public class ProcessDeploymentService {
@Autowired
private RepositoryService repositoryService;
public Deployment deployProcess(String bpmnFileName) {
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("processes/" + bpmnFileName)
.name("请假流程部署")
.deploy();
System.out.println("流程部署成功,部署ID: " + deployment.getId());
return deployment;
}
}
第三步:启动流程实例并完成任务 流程定义部署后,只是一个“模板”。真正的流程运行始于一个“流程实例”。我们通过一个简单的REST控制器来模拟前端操作:
@RestController
@RequestMapping("/api/leave")
public class LeaveProcessController {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private IdentityService identityService;
/**
* 发起一个请假流程
*/
@PostMapping("/start")
public String startLeaveProcess(@RequestBody LeaveRequest request) {
// 设置流程发起人(通常从安全上下文获取)
identityService.setAuthenticatedUserId(request.getApplicant());
// 设置流程变量
Map<String, Object> variables = new HashMap<>();
variables.put("applicant", request.getApplicant());
variables.put("leaveType", request.getLeaveType());
variables.put("days", request.getDays());
variables.put("reason", request.getReason());
// 使用流程定义的Key启动实例
ProcessInstance instance = runtimeService.startProcessInstanceByKey("leaveProcess", variables);
return "流程启动成功,实例ID: " + instance.getId();
}
/**
* 查询某个用户的待办任务
*/
@GetMapping("/tasks")
public List<TaskRepresentation> getTasks(@RequestParam String userId) {
// 查询分配给该用户的任务
List<Task> tasks = taskService.createTaskQuery().taskAssignee(userId).list();
// 查询该用户所在候选组的任务
List<Task> candidateTasks = taskService.createTaskQuery().taskCandidateUser(userId).list();
tasks.addAll(candidateTasks);
return tasks.stream().map(task -> {
TaskRepresentation tr = new TaskRepresentation();
tr.setTaskId(task.getId());
tr.setTaskName(task.getName());
tr.setProcessInstanceId(task.getProcessInstanceId());
// 可以进一步查询流程变量,显示更详细信息
Map<String, Object> vars = taskService.getVariables(task.getId());
tr.setVariables(vars);
return tr;
}).collect(Collectors.toList());
}
/**
* 办理任务(如经理审批)
*/
@PostMapping("/complete/{taskId}")
public String completeTask(@PathVariable String taskId,
@RequestParam String action,
@RequestParam(required = false) String comment) {
Map<String, Object> taskVariables = new HashMap<>();
taskVariables.put("managerApproved", "approve".equals(action));
taskVariables.put("managerComment", comment);
// 完成任务,并传递变量驱动流程走向
taskService.complete(taskId, taskVariables);
return "任务处理完成";
}
}
至此,一个具备核心流转功能的请假流程已经可以运行。你可以通过调用 /api/leave/start 发起流程,然后以经理身份登录,调用 /api/leave/tasks 查看待办,并处理任务。
3. 核心概念深化:任务分配、变量与监听器
掌握了基础流程搭建后,我们需要深入几个关键机制,它们决定了流程的灵活性与健壮性。
任务分配策略 Activiti提供了多种任务分配方式,适应不同业务场景:
- 固定分配:在BPMN中直接指定
activiti:assignee="zhangsan"。简单但不灵活。 - 表达式分配:使用UEL(Unified Expression Language)表达式,如
activiti:assignee="${applicant}",动态指定处理人为流程变量中的申请人。 - 候选用户/组:使用
activiti:candidateUsers或activiti:candidateGroups。任务会出现在这些用户或组成员的待办列表中,需要先“认领”(claim)才能成为处理人。 - 监听器分配:通过实现
TaskListener接口,在任务创建时动态计算处理人。这是最灵活的方式,可以集成组织架构服务。
@Component
public class DynamicTaskAssignmentListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
String eventName = delegateTask.getEventName();
if (EVENTNAME_CREATE.equals(eventName)) {
// 根据复杂业务规则,从外部系统查询负责人
String processDefinitionKey = delegateTask.getProcessDefinitionId();
String department = (String) delegateTask.getVariable("department");
String assignee = findAssigneeByRule(processDefinitionKey, department);
delegateTask.setAssignee(assignee);
}
}
private String findAssigneeByRule(String key, String dept) {
// 模拟从数据库或规则引擎查询
return "manager_of_" + dept;
}
}
在BPMN中配置监听器:
<userTask id="someTask" name="动态分配任务">
<extensionElements>
<activiti:taskListener event="create" class="com.yourcompany.DynamicTaskAssignmentListener"/>
</extensionElements>
</userTask>
流程变量(Variables)的作用域与生命周期 流程变量是流程运行时携带数据的载体。理解其作用域至关重要:
- 全局变量:通过
runtimeService.setVariable(executionId, key, value)设置,在整个流程实例内可见。 - 本地变量:通过
runtimeService.setVariableLocal(taskId, key, value)设置,仅在当前执行分支或任务内有效。 - 瞬态变量:通过
runtimeService.setVariable(executionId, key, value)设置,但标记为瞬态,不保存到历史表,适用于中间计算结果。
一个常见的坑是混淆变量作用域,导致在网关条件表达式中读取不到预期的变量。务必在设置和读取时明确作用域。
事件监听器(Event Listeners) 监听器允许你在流程执行的特定节点注入自定义逻辑,实现业务解耦。
- 执行监听器(Execution Listener):监听流程实例或活动(如开始事件、结束事件、网关)的生命周期事件。
- 任务监听器(Task Listener):监听用户任务的生命周期事件(创建、分配、完成、删除)。
例如,我们可以在流程结束时,自动调用一个服务来更新业务状态:
@Component
public class ProcessEndListener implements ExecutionListener {
@Autowired
private BusinessStatusService statusService;
@Override
public void notify(DelegateExecution execution) {
String processInstanceId = execution.getProcessInstanceId();
String businessKey = execution.getProcessBusinessKey(); // 通常关联业务ID
String status = "COMPLETED"; // 可根据流程结果细化
statusService.updateStatus(businessKey, status);
// 可以在这里发送通知、记录审计日志等
}
}
4. 高级特性与生产环境避坑指南
当流程从Demo走向生产环境,你会遇到更复杂的需求和挑战。本节聚焦于那些容易被忽略却至关重要的高级特性和实践。
异步执行器与事务管理 Activiti默认使用异步执行器处理作业(如定时器事件、异步延续)。这能提升吞吐量,但也引入了复杂性。
activiti:
async-executor-activate: true # 生产环境建议开启
async-executor-core-pool-size: 10
async-executor-max-pool-size: 50
async-executor-queue-capacity: 100
关键提醒:异步执行器在一个独立线程池中运行,与触发它的主事务是分离的。这意味着,如果在服务任务(Service Task)中抛出的异常被捕获并处理,主事务可能提交成功,但异步作业却因异常而失败重试,导致数据不一致。务必确保你的服务任务逻辑是幂等的,并妥善处理异常。
历史数据配置与性能优化 Activiti的历史记录非常详尽,但也会导致历史表(ACT_HI_*)快速增长。需要根据业务需求调整历史级别:
- none:不保存任何历史。
- activity:保存流程实例和活动实例信息。
- audit(默认):在activity基础上,保存所有变量、表单属性。
- full:最详细,额外保存所有流程变量的更新细节。
对于高并发、长生命周期的流程,建议将历史数据定期归档或清理。Activiti提供了 HistoryService 的API进行手动清理,也可以结合业务定制清理策略。
集成自定义表单 虽然Activiti支持内嵌表单定义,但企业级应用通常使用自研或第三方UI表单设计器。此时,流程引擎只负责流转和任务分配,表单渲染与数据收集由前端负责。集成关键在于:
- 表单定义分离:将表单的字段、校验规则、布局等信息存储在独立的表或JSON文件中,与流程定义关联。
- 数据绑定:任务办理时,前端根据任务ID获取对应的表单定义和初始数据(流程变量),提交时再将表单数据作为流程变量传回引擎。
@Service
public class FormIntegrationService {
public FormDefinition getStartForm(String processDefinitionKey) {
// 从你的表单库中查询该流程的启动表单定义
return customFormRepository.findByProcessKey(processDefinitionKey);
}
public FormDefinition getTaskForm(String taskId) {
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
String formKey = task.getFormKey(); // 可以在BPMN中为任务设置formKey
if (formKey != null) {
return customFormRepository.findByKey(formKey);
}
// 或者根据任务定义Key来查找
return customFormRepository.findByTaskDefKey(task.getTaskDefinitionKey());
}
public void submitTaskForm(String taskId, Map<String, Object> formData) {
// 数据校验...
validateFormData(formData);
// 将表单数据转换为流程变量
Map<String, Object> variables = convertFormToVariables(formData);
// 完成任务并提交变量
taskService.complete(taskId, variables);
}
}
常见“坑点”与解决方案
-
流程版本管理:当你重新部署一个同ID的流程定义时,Activiti会创建新版本。默认情况下,新发起的流程实例会使用最新版本,但正在运行的老版本实例不受影响。这可能导致同一流程存在多个版本实例同时运行。如果业务要求强制升级,需要编写迁移脚本或使用
runtimeService.updateProcessInstanceBusinessKey等操作进行干预。 -
网关条件表达式:在排他网关(Exclusive Gateway)的条件表达式中,确保引用的流程变量已存在且类型正确。表达式语言默认是JUEL,对于复杂逻辑,建议将判断逻辑封装在Spring Bean中,通过
${myBean.checkCondition(execution)}调用。 -
用户与组的管理:Activiti自带的
IdentityService功能简单,生产环境通常需要与公司现有的LDAP、AD或用户中心集成。你可以实现UserEntityManager和GroupEntityManager接口,替换默认的身份管理实现。 -
高并发下的乐观锁:Activiti的运行时数据更新使用乐观锁。在高并发场景下(如多人同时抢签一个任务),可能抛出
ActivitiOptimisticLockingException。处理方式通常是在业务层进行重试,或者调整业务设计,避免对同一任务资源的激烈竞争。 -
流程图的动态生成与预览:很多场景需要展示实时流程图,并高亮当前节点。这需要结合流程实例ID和BPMN XML,使用GraphicGenerator生成图片,并根据当前活动节点信息进行渲染。社区有一些开源工具库可以简化此工作。
从环境搭建到第一个流程运行,再到深入核心机制与生产实践,我们完成了一次完整的Activiti 7.x入门之旅。记住,流程引擎的引入不仅仅是技术选型,更是对业务逻辑的一次重新梳理和标准化。初期建议从简单的、核心的流程入手,逐步迭代,在实战中积累对引擎特性的理解。当你熟悉了这些模式后,会发现Activiti提供的远不止是任务流转,它是一套完整的、用于构建可观测、可管理、可优化的业务流程自动化的强大框架。
1万+



被折叠的 条评论
为什么被折叠?



