Spring Boot集成Flowable工作流引擎:三注解搞定请假审批流程
1. 从零到一:为什么要在Spring Boot里集成工作流引擎?
如果你是一个后端开发者,最近可能经常听到“低代码”、“流程自动化”这些词。但说实话,很多项目里所谓的“流程”,其实就是一堆if-else加上数据库里的几个状态字段。比如一个请假审批,代码里写满了“如果部门经理审批通过,则状态改为‘总监审批中’;如果总监驳回,则状态回退到‘申请人修改’”。这种硬编码的流程逻辑,维护起来简直是噩梦——业务规则一变,就得通读代码,小心翼翼地修改那些嵌套的判断,生怕改漏了哪个分支。
这就是工作流引擎要解决的问题。它把业务流程从你的业务代码中抽离出来,变成一种可以可视化设计、独立部署和动态调整的“数据”。而Flowable,作为Activiti的一个分支,是目前Java生态中最活跃、功能最全面的开源工作流引擎之一。它轻量、性能好,并且与Spring Boot的集成堪称无缝。
那么,在Spring Boot项目里接上Flowable,到底能带来什么?最直接的,就是你不用再当“流程状态管理员”了。请假、报销、采购、工单,这些有固定步骤的业务,都可以通过Flowable的BPMN 2.0标准流程图来定义。流程怎么走,由流程图说了算,你的代码只需要关心在每个节点上“做什么业务操作”(比如计算金额、发送通知),而不用操心“下一步该去哪”。当业务方说“我们想在部门经理和总监之间加一个财务审核环节”,你只需要拖拽修改流程图并重新部署,代码可能一行都不用动。
今天,我就带你绕过那些复杂的官方文档和庞大的Demo,直击核心:如何用最少的代码,在Spring Boot中快速搭建一个可运行的请假审批流程。我们不会面面俱到,而是聚焦于三个核心注解,它们就像三根支柱,能撑起一个流程应用的基本骨架。理解了它们,你就能自己探索Flowable更强大的功能了。
2. 环境搭建与核心依赖:别在起步阶段踩坑
开始之前,我们得先把场子搭起来。这里有几个关键选择,直接决定了你后续开发的顺畅程度。
2.1 依赖选型:spring-boot-starter才是正道
首先看Maven依赖。Flowable为Spring Boot提供了官方的Starter,这是最推荐的方式。
<dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>6.8.0</version> <!-- 请使用当前最新稳定版 --> </dependency>为什么一定要用Starter?因为它帮你自动配置了几乎所有东西:数据源、事务管理器、Flowable的各种服务Bean(如RepositoryService,RuntimeService,TaskService)。如果你手动引入flowable-engine然后自己写@Bean配置,会陷入一堆繁琐的配置文件中,极易出错。Starter让集成变得像使用spring-boot-starter-data-jpa一样简单。
引入这个依赖后,Flowable会自动在你的项目资源路径下寻找流程定义文件(.bpmn20.xml或.bpmn)。默认情况下,它会扫描src/main/resources/processes/目录。我建议你严格遵守这个约定,新建这个文件夹,把所有的流程图文件都放进去,避免不必要的配置。
2.2 数据库配置:它比你想象的要“重”
Flowable需要数据库来存储流程定义、运行时数据、历史数据等。它会自动创建数十张表(常见的有60张左右)。所以,第一个注意事项来了:
注意:切勿在生产环境中使用内嵌的H2数据库。
flowable-spring-boot-starter默认可能配了H2,方便测试。但在真实项目中,请务必在application.yml中配置你自己的MySQL或PostgreSQL等数据库。
spring: datasource: url: jdbc:mysql://localhost:3306/flowable_db?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver # Flowable相关配置(非必须,有默认值) flowable: async-executor-activate: false # 初学者可以先关闭异步执行器,避免复杂化 database-schema-update: true # 设置为 true,启动时自动创建或更新表结构关键配置是flowable.database-schema-update。设置为true,应用启动时,Flowable会检查数据库,如果表不存在就创建,存在则对比模型版本进行更新。这在开发阶段非常方便。但在生产环境,更安全的做法是设置为false,并使用Flowable提供的数据库脚本手动初始化表结构,以避免自动升级带来的意外风险。
2.3 绘制第一个流程图:请假审批的BPMN模型
一切就绪,我们来定义流程。在src/main/resources/processes/下新建一个文件,比如leave-approval.bpmn20.xml。BPMN 2.0标准是XML格式,但我们可以用图形化工具来画。推荐使用Flowable官方提供的Eclipse插件,或者在线工具如bpmn-js。这里我用文字描述一个最简单的请假审批流程:
- 开始事件(Start Event):流程起点。
- 用户任务(User Task)- 提交申请:申请人填写请假单。任务分配候选人(Candidate Users)设为申请人自己(实际中由业务系统动态指定)。
- 用户任务(User Task)- 部门经理审批:经理审批。候选人设为“部门经理”这个角色(或具体用户)。
- 排他网关(Exclusive Gateway):根据审批结果做判断。
- 顺序流(Sequence Flow):
- 条件为“同意”,流向“结束事件(End Event)”。
- 条件为“驳回”,流回“提交申请”节点,让申请人修改。
- 结束事件(End Event):流程结束。
在XML中,最关键的是每个<userTask>节点的id和name属性,以及如何分配任务。我们可以使用表达式(Expression)来动态分配。例如,部门经理审批任务可以这样定义:
<userTask id="deptManagerAudit" name="部门经理审批" flowable:candidateUsers="${deptManagerId}"> <extensionElements> <flowable:formProperty id="auditResult" name="审批结果" type="enum" required="true"> <flowable:value id="agree" name="同意" /> <flowable:value id="reject" name="驳回" /> </flowable:formProperty> <flowable:formProperty id="comment" name="审批意见" type="string" /> </extensionElements> </userTask>这里我们通过flowable:candidateUsers="${deptManagerId}"来动态指定候选人。deptManagerId是一个流程变量(Process Variable),在流程启动或流经时由我们的Java代码注入。同时,我们使用<extensionElements>定义了这个任务的自定义表单属性,包括一个枚举类型的审批结果和一个字符串类型的审批意见。这样,审批人在处理任务时,就有了明确的输入项。
把画好的流程图保存为XML文件,放在processes目录下。启动Spring Boot应用,如果控制台没有报错,并且能看到类似“Deployed processes: [leave-approval (v1)]”的日志,恭喜你,流程定义已经成功部署到数据库了!
3. 第一个核心注解:@RestController与流程的启停控制
流程部署好了,它就像一套模具,静静躺在数据库里。现在我们需要一个入口,来根据这套模具“生产”出一个具体的流程实例(Process Instance)。比如,张三今天要请假,我们就为他启动一个“请假审批流程实例”。
这个入口,通常就是一个Spring MVC的@RestController。
3.1 启动流程:注入RuntimeService
Flowable的RuntimeService是管理流程运行时的核心服务,负责启动流程实例、设置变量、触发信号等。我们通过Spring的依赖注入轻松获得它。
@RestController @RequestMapping("/api/process") public class ProcessController { @Autowired private RuntimeService runtimeService; @Autowired private IdentityService identityService; // 用于设置流程发起人 @PostMapping("/start-leave") public ResponseEntity<String> startLeaveProcess(@RequestBody LeaveRequest request) { // 1. 设置流程发起人(可选,但对任务查询和权限控制有用) identityService.setAuthenticatedUserId(request.getApplicantUserId()); // 2. 准备流程变量 Map<String, Object> variables = new HashMap<>(); variables.put("applicantName", request.getApplicantName()); variables.put("leaveType", request.getLeaveType()); variables.put("leaveDays", request.getLeaveDays()); variables.put("reason", request.getReason()); // 动态查找部门经理ID,这里简化为从请求体获取 variables.put("deptManagerId", request.getDeptManagerId()); // 3. 启动流程实例 // “leave-approval”是流程定义的Key,即BPMN文件中的id属性,不是文件名。 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave-approval", variables); // 4. 清理认证信息(良好习惯) identityService.setAuthenticatedUserId(null); return ResponseEntity.ok("请假流程已启动,流程实例ID: " + processInstance.getId()); } }关键点解析:
identityService.setAuthenticatedUserId:这行代码设置了当前操作的“认证用户”。它非常重要,因为Flowable会将这个用户记录为流程实例的“发起人”(Initiator)。后续查询“我发起的流程”时,就需要用到这个信息。务必记得在使用后置为null,尤其是在Web请求上下文中,避免用户信息泄露到其他线程。startProcessInstanceByKey:这是最常用的启动方式。参数“leave-approval”是你的流程定义Key(BPMN XML中<process id="leave-approval" ...>的id),而不是文件名。你也可以用startProcessInstanceById,参数是流程定义在数据库中的唯一ID(通常包含版本号),但用Key启动默认会使用最新版本的流程定义,更符合直觉。- 流程变量(Variables):我们通过Map传递的
variables,会成为这个流程实例的全局变量。在流程流转过程中,任何节点都可以读取或修改这些变量。我们在之前BPMN中定义的${deptManagerId},就是从这里获取值的。流程变量是连接业务流程(流程图)和业务数据(你的Java对象)的桥梁。
3.2 查询与监控:获取流程状态
启动后,你可能需要查看流程当前走到哪了。这需要用到HistoryService来查询历史活动,或者用RuntimeService和TaskService查询当前活动节点。
@GetMapping("/instance/{instanceId}") public ProcessInstanceDetail getInstanceDetail(@PathVariable String instanceId) { // 查询运行时信息 ProcessInstance instance = runtimeService.createProcessInstanceQuery() .processInstanceId(instanceId) .singleResult(); // 查询历史活动记录,了解流程走过的路径 List<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery() .processInstanceId(instanceId) .orderByHistoricActivityInstanceStartTime().asc() .list(); // 组装返回结果... return detail; }到这里,我们已经可以通过HTTP API来启动和查看一个流程了。但这只是让流程“跑起来”,流程中的任务(比如“部门经理审批”)还需要有人去“完成”它。这就引出了我们的第二个核心注解。
4. 第二个核心注解:@Component与任务监听器的妙用
用户任务(User Task)是流程中需要人工干预的节点。处理任务最常见的方式是通过TaskService查询任务列表,然后调用taskService.complete(taskId, variables)来完成任务。但今天我想介绍一种更优雅、更解耦的方式:任务监听器(Task Listener)。
监听器允许你在任务生命周期的特定事件(如创建create、分配assignment、完成complete)发生时,执行一段Java代码。这非常适合处理一些与核心业务逻辑解耦的“边缘逻辑”,比如发送通知、记录日志、更新业务表冗余字段等。
4.1 创建全局任务监听器
我们可以创建一个Spring Bean,并实现Flowable的TaskListener接口。
@Component public class NotificationTaskListener implements TaskListener { private static final Logger logger = LoggerFactory.getLogger(NotificationTaskListener.class); @Autowired private MessageService messageService; // 假设你有一个发送消息的服务 @Override public void notify(DelegateTask delegateTask) { String eventName = delegateTask.getEventName(); String taskName = delegateTask.getName(); String assignee = delegateTask.getAssignee(); // 当前任务的处理人 String processInstanceId = delegateTask.getProcessInstanceId(); logger.info("任务事件触发: 流程实例[{}], 任务[{}], 事件[{}], 处理人[{}]", processInstanceId, taskName, eventName, assignee); // 根据不同事件类型执行不同操作 if (TaskListener.EVENTNAME_CREATE.equals(eventName)) { // 任务刚创建,通常发送“待办”通知 if (assignee != null) { String message = String.format("您有一个新的待办任务【%s】需要处理,请及时查看。", taskName); messageService.sendToUser(assignee, message); } } else if (TaskListener.EVENTNAME_COMPLETE.equals(eventName)) { // 任务完成,可以发送结果通知给相关人员 String auditResult = (String) delegateTask.getVariable("auditResult"); String comment = (String) delegateTask.getVariable("comment"); // 例如,通知申请人审批结果 String applicant = (String) delegateTask.getVariable("applicantName"); String resultMsg = "同意".equals(auditResult) ? "已通过" : "被驳回,原因:" + comment; messageService.sendToUser(applicant, String.format("您的请假申请%s", resultMsg)); } // 还可以处理 ASSIGNMENT(任务分配给人时)等事件 } }4.2 将监听器绑定到流程图
光有Bean还不够,我们需要在BPMN中指定哪个任务使用这个监听器。这需要修改XML,在<userTask>的<extensionElements>中添加。
<userTask id="deptManagerAudit" name="部门经理审批" flowable:candidateUsers="${deptManagerId}"> <extensionElements> <!-- 之前定义的表单属性 --> <flowable:formProperty ... /> <!-- 引用Spring Bean作为任务监听器 --> <flowable:taskListener event="create" class="${notificationTaskListener}" /> <flowable:taskListener event="complete" class="${notificationTaskListener}" /> </extensionElements> </userTask>注意这里的class属性值:${notificationTaskListener}。这不是一个全限定类名,而是一个Spring Bean的名字。Flowable与Spring集成时,支持这种表达式,它会从Spring上下文中查找名为notificationTaskListener的Bean(即我们@Component注解的类,默认类名首字母小写就是Bean名),并将其作为监听器实例。
这样做的好处是什么?
- 解耦:发送通知的逻辑从你的业务Service中剥离出来,业务Service只关心审批的核心逻辑(如更新请假单状态)。
- 可维护:所有通知逻辑集中在一个地方管理。
- 灵活:在流程图上配置监听关系,非开发人员(如流程管理员)通过修改流程图也能调整通知策略(当然,需要知道Bean的名字)。
一个常见的坑:确保你的监听器Bean是单例(Singleton)并且是线程安全的。因为Flowable引擎会并发执行多个流程实例,同一个监听器实例可能被多个线程同时调用。我们的例子中,MessageService如果被正确注入(Spring默认单例且通常线程安全),一般没问题。但要避免在监听器中修改共享的非线程安全状态。
5. 第三个核心注解:@Service与业务逻辑的完美融合
流程引擎负责流转,但节点上的具体业务操作,比如“部门经理审批”时,到底要更新数据库里的哪张表、计算什么数据,这仍然是你的业务系统职责。我们需要把Flowable的任务和我们的业务Service连接起来。通常,我们会在TaskService.complete()的前后,调用业务Service。
5.1 在业务Service中完成任务
创建一个LeaveService,它负责协调流程引擎和业务数据。
@Service public class LeaveService { @Autowired private TaskService taskService; @Autowired private RuntimeService runtimeService; @Autowired private LeaveApplicationRepository leaveAppRepo; // 假设的JPA Repository @Transactional // 确保业务和流程操作在同一个事务里 public void completeManagerAudit(String taskId, ManagerAuditRequest request) { // 1. 根据taskId查询任务,并做基础校验(如任务是否存在、当前用户是否有权限) Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); if (task == null) { throw new RuntimeException("任务不存在或已完成"); } // 这里可以添加更复杂的权限校验,比如当前登录用户是否是任务的候选人或办理人 // 2. 执行核心业务逻辑:更新请假申请单状态 String processInstanceId = task.getProcessInstanceId(); // 通过流程实例ID,可以关联到业务数据。这里假设我们把业务主键存在流程变量里。 String businessKey = (String) runtimeService.getVariable(processInstanceId, "businessKey"); LeaveApplication app = leaveAppRepo.findById(businessKey) .orElseThrow(() -> new RuntimeException("请假申请不存在")); app.setAuditResult(request.getResult()); app.setAuditComment(request.getComment()); app.setStatus("MANAGER_AUDITED"); leaveAppRepo.save(app); // 3. 准备完成任务所需的流程变量 Map<String, Object> taskVariables = new HashMap<>(); taskVariables.put("auditResult", request.getResult()); // 这个变量会被排他网关使用 taskVariables.put("comment", request.getComment()); // 如果需要,还可以设置其他变量,决定流程下一步走向 // 例如:taskVariables.put("nextApprover", someUserId); // 4. 完成任务!这是驱动流程向下流转的关键一步。 taskService.complete(taskId, taskVariables); // 5. (可选)完成任务后的其他业务操作,如记录操作日志等。 log.info("部门经理审批完成,任务ID: {}, 流程实例ID: {}, 结果: {}", taskId, processInstanceId, request.getResult()); } }关键点与避坑指南:
- 事务管理(
@Transactional):这个方法上加@Transactional注解至关重要。它保证了更新业务表(leaveAppRepo.save)和完成流程任务(taskService.complete)这两个操作在同一个数据库事务中。要么都成功,要么都回滚。否则,可能出现业务数据更新了,但流程卡住不动,或者流程走完了,业务数据没更新的数据不一致状态。 - 业务键(Business Key):如何通过流程实例找到对应的业务数据?最佳实践是使用Business Key。在启动流程实例时,可以调用
runtimeService.startProcessInstanceByKey(processDefinitionKey, businessKey, variables),将你的业务主键(如请假单号“LEAVE-2023-001”)作为businessKey传入。之后,就可以通过runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey)来查询流程实例。上面代码中,我们将其存为流程变量也是一种方式,但使用正式的Business Key更规范,Flowable也为其提供了专门的API和索引优化。 - 权限校验:
taskService.complete本身不检查权限,只要taskId有效就能完成。所以必须在调用complete之前,自己做好严格的权限校验。通常是根据当前登录用户ID,去查询他是否有权限处理这个任务(是否是候选人、办理人,或者拥有某个能处理所有任务的管理角色)。 - 变量作用域:
taskService.complete(taskId, variables)中传入的变量,默认是任务局部变量,会在任务完成后提升为流程变量。这意味着,在完成任务时设置的变量(如auditResult),可以在后续的网关判断、任务分配表达式中使用。如果你希望变量只在本任务范围内有效,不传递到流程级别,需要使用taskService.setVariableLocal。
5.2 驱动流程的控制器
最后,我们提供一个REST接口给前端,用于提交审批操作。
@RestController @RequestMapping("/api/task") public class TaskController { @Autowired private LeaveService leaveService; @PostMapping("/complete/manager-audit") public ResponseEntity<String> completeManagerAudit(@RequestBody CompleteTaskRequest request) { // 这里应该从安全上下文(如JWT)中获取当前用户,并验证该用户是否有权操作此任务 // 为简化示例,假设请求体中包含了已校验的用户信息 leaveService.completeManagerAudit(request.getTaskId(), request.getAuditData()); return ResponseEntity.ok("审批操作提交成功"); } }至此,一个完整的“发起流程 -> 自动创建任务 -> 处理任务(执行业务逻辑+驱动流程) -> 流程根据结果流转”的闭环就形成了。我们通过@RestController、@Component、@Service这三个注解,清晰地划分了流程控制、事件监听、业务逻辑这三个层次,代码结构清晰,职责分明。
6. 流程的查询、管理与进阶思考
基础功能跑通后,你肯定会需要更多功能。这里提供一些关键查询和管理操作的思路。
6.1 如何查询“我的待办”?
这是最常见的需求。结合当前登录用户ID,使用TaskService进行查询。
@Service public class MyTaskService { @Autowired private TaskService taskService; public List<TaskInfo> getMyTasks(String userId) { // 查询分配给该用户的任务 List<Task> assignedTasks = taskService.createTaskQuery() .taskAssignee(userId) // 指定办理人 .orderByTaskCreateTime().desc() .list(); // 查询该用户作为候选人的任务(尚未签收) List<Task> candidateTasks = taskService.createTaskQuery() .taskCandidateUser(userId) // 候选人 .orderByTaskCreateTime().desc() .list(); // 合并、转换并返回... return allTasks; } }注意:在实际系统中,任务分配可能非常复杂,涉及候选组、通过表达式动态计算候选人等。这里的查询需要根据你流程图中的任务分配方式(assignee、candidateUsers、candidateGroups)来灵活组合。
6.2 流程管理:挂起、激活与跳转
有时需要对流程进行干预。
- 挂起/激活流程实例:
runtimeService.suspendProcessInstanceById(instanceId)和runtimeService.activateProcessInstanceById(instanceId)。挂起后,该实例下的所有任务都无法完成。 - 跳转(非标准功能):Flowable官方不推荐随意跳转节点,这会破坏流程的完整性和历史记录。但在极端调试或补救场景下,可以通过
runtimeService.createChangeActivityStateBuilder()来修改当前活动节点。务必谨慎使用,并充分理解其影响。
6.3 历史数据与报表
所有流程运行的历史痕迹都保存在历史表中(以ACT_HI_开头)。HistoryService提供了强大的查询API,可以用于生成报表,如:
- 平均任务处理时长。
- 各环节的通过率、驳回率。
- 流程实例的总体耗时分布。
6.4 你可能遇到的坑与优化建议
- 异步执行器(Async Executor):对于定时事件、异步任务等,Flowable默认会使用异步执行器。在开发环境如果不想复杂化,可以像我们之前配置的那样关闭它(
flowable.async-executor-activate: false)。生产环境开启时,需要理解其原理,并合理配置线程池。 - 变量序列化:流程变量会被序列化后存入数据库。存入复杂的Java对象(如一个大实体类)会导致性能问题和版本兼容性问题。最佳实践是只存基本类型、String、Map、List或实现了
Serializable的简单值对象。业务主键用String或Long,通过主键去业务库查询完整数据。 - 流程定义版本控制:当你修改BPMN文件并重新部署时,Flowable会创建一个新版本(Version),旧版本的流程实例会继续以旧版本运行,新启动的实例则使用新版本。这是非常棒的特性。你需要管理好不同版本的流程定义,并在查询时注意版本筛选。
- 高并发与性能:对于任务量非常大的系统,直接查询
ACT_RU_TASK(运行时任务表)可能会有压力。考虑对任务表进行分库分表,或者建立只读副本用于查询。Flowable的表结构设计对于常见操作是有索引的,但复杂的自定义查询可能需要你自己优化。
回过头看,我们用三个Spring Boot中最常见的注解——@RestController、@Component、@Service,就串联起了Flowable工作流的核心生命周期。这证明了Flowable与Spring生态融合的深度。它不是一个需要你顶礼膜拜的庞然大物,而是一个可以逐步融入你现有项目的工具。从一个简单的请假审批开始,尝试用它来管理你项目里那些复杂的、多变的状态流转逻辑,你会发现,把“流程”交给专业的引擎,你的业务代码会变得前所未有的清晰和稳定。
