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 groupIdorg.flowable/groupId artifactIdflowable-spring-boot-starter/artifactId version6.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?characterEncodingUTF-8serverTimezoneAsia/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 iddeptManagerAudit name部门经理审批 flowable:candidateUsers${deptManagerId} extensionElements flowable:formProperty idauditResult name审批结果 typeenum requiredtrue flowable:value idagree name同意 / flowable:value idreject name驳回 / /flowable:formProperty flowable:formProperty idcomment name审批意见 typestring / /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 启动流程注入RuntimeServiceFlowable的RuntimeService是管理流程运行时的核心服务负责启动流程实例、设置变量、触发信号等。我们通过Spring的依赖注入轻松获得它。RestController RequestMapping(/api/process) public class ProcessController { Autowired private RuntimeService runtimeService; Autowired private IdentityService identityService; // 用于设置流程发起人 PostMapping(/start-leave) public ResponseEntityString startLeaveProcess(RequestBody LeaveRequest request) { // 1. 设置流程发起人可选但对任务查询和权限控制有用 identityService.setAuthenticatedUserId(request.getApplicantUserId()); // 2. 准备流程变量 MapString, 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”是你的流程定义KeyBPMN XML中process idleave-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(); // 查询历史活动记录了解流程走过的路径 ListHistoricActivityInstance 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 iddeptManagerAudit name部门经理审批 flowable:candidateUsers${deptManagerId} extensionElements !-- 之前定义的表单属性 -- flowable:formProperty ... / !-- 引用Spring Bean作为任务监听器 -- flowable:taskListener eventcreate class${notificationTaskListener} / flowable:taskListener eventcomplete 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. 准备完成任务所需的流程变量 MapString, 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 ResponseEntityString 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 ListTaskInfo getMyTasks(String userId) { // 查询分配给该用户的任务 ListTask assignedTasks taskService.createTaskQuery() .taskAssignee(userId) // 指定办理人 .orderByTaskCreateTime().desc() .list(); // 查询该用户作为候选人的任务尚未签收 ListTask 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生态融合的深度。它不是一个需要你顶礼膜拜的庞然大物而是一个可以逐步融入你现有项目的工具。从一个简单的请假审批开始尝试用它来管理你项目里那些复杂的、多变的状态流转逻辑你会发现把“流程”交给专业的引擎你的业务代码会变得前所未有的清晰和稳定。