# Spring Boot 接 Flowable 工作流:用 3 个注解搭一个请假审批流程 > 很多项目接 Flowable,最后写成了"Flowable 教程翻译"。这篇不翻译 Flowable 文档,只讲一件事:怎么在 Spring Boot 业务代码里,用最少的侵入接一个能跑的审批流程。 工作流引擎里 Flowable 用得最多,但很多团队接完后都有一个感受:**流程引擎和业务代码搅在一起,越写越乱。** 典型的乱象长这样: ```java // 业务 Service 里手动调 Flowable RuntimeService @Service public class LeaveService { @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; @Autowired private HistoryService historyService; public void submit(LeaveApply leave) { // 1. 存业务表 leaveMapper.insert(leave); // 2. 手动发起流程 Map vars = new HashMap<>(); vars.put("days", leave.getDays()); vars.put("applicant", leave.getUserId()); ProcessInstance pi = runtimeService.startProcessInstanceByKey( "leave_process", "leave:" + leave.getId(), vars); // 3. 把 instanceId 存回业务表 leave.setProcessInstanceId(pi.getId()); leaveMapper.updateById(leave); } } ``` 问题在哪? - **业务代码侵入流程引擎 API**:`RuntimeService`、`TaskService` 散落在业务 Service 里 - **流程引擎和业务库耦合**:如果流程引擎拆成独立服务,这些代码全要改 - **流程回调难写**:流程结束后要更新业务状态,得写监听器、发事件、再处理 - **每个业务流程都要重复这一套**:请假、报销、采购,每个 Service 都注入一遍 Flowable API Forge Admin 的做法是:**把流程引擎拆成独立服务,业务侧只用 3 个注解 + SpEL 就能接流程**。这篇就讲怎么用这套机制搭一个完整的请假审批流程。 --- ## 一、整体架构:业务和流程引擎解耦 先看清楚 Forge 的流程架构,再看代码就不会懵: ```text ┌─────────────────────────┐ ┌─────────────────────────┐ │ 业务服务(admin) │ │ 流程服务(flow) │ │ │ │ │ │ LeaveService │ │ Flowable 引擎 │ │ @FlowBind │ HTTP │ BPMN 流程模型 │ │ @FlowStart ───────────┼───────►│ 任务流转 │ │ @FlowCallback ◄───────┼───────►│ 事件推送(Redis/Webhook)│ │ │ │ │ └─────────────────────────┘ └─────────────────────────┘ ``` 关键点: 1. **流程引擎是独立服务**(`forge-flow`),不和业务服务混部署 2. **业务侧不直接调 Flowable API**,通过 `FlowClient` 发 HTTP 调流程服务 3. **3 个注解搞定发起、绑定、回调**,业务代码几乎无侵入 4. **SpEL 表达式传参**,不用手动拼 Map --- ## 二、三个注解:流程接入的全部 API 整套机制的核心就三个注解,先记住它们的职责: | 注解 | 位置 | 作用 | |------|------|------| | `@FlowBind` | 类上 | 声明这个 Service 绑定哪个流程模型 | | `@FlowStart` | 方法上 | 方法执行成功后自动发起流程 | | `@FlowCallback` | 方法上 | 流程事件回调(通过、驳回、取消等) | 就这三个。没有 `RuntimeService`,没有 `TaskService`,没有手动 `startProcessInstanceByKey`。 --- ## 三、第一步:定义业务实体 先建一个请假申请实体,这就是普通业务表,和流程引擎无关: ```java @Data @TableName("biz_leave_apply") public class LeaveApply { private Long id; private String applicantId; // 申请人ID private String applicantName; // 申请人姓名 private Integer days; // 请假天数 private String reason; // 请假原因 private String status; // 业务状态:PENDING/APPROVED/REJECTED private String processInstanceId;// 流程实例ID(可选,用于关联) } ``` 注意:业务表不需要包含任何 Flowable 的字段(`act_*` 那些表由流程引擎自己管)。业务表只存业务数据,流程数据在流程服务那边。 --- ## 四、第二步:用 @FlowBind + @FlowStart 接入发起流程 这是最关键的一步。看完整代码: ```java @FlowBind(modelKey = "leave_process", businessType = "leave") @Service public class LeaveService { @FlowStart( businessKeySpEl = "'leave:' + #leave.id", titleSpEl = "#leave.applicantName + ' 的请假申请'", userIdSpEl = "#leave.applicantId", userNameSpEl = "#leave.applicantName", variablesSpEl = "{'days': #leave.days, 'reason': #leave.reason}" ) public LeaveApply submit(LeaveApply leave) { // 1. 存业务表 —— 这是你唯一的业务逻辑 leaveMapper.insert(leave); leave.setStatus("PENDING"); leaveMapper.updateById(leave); return leave; // 2. 流程发起由 @FlowStart 切面自动完成,不用写 } } ``` **这段代码做了什么?** 1. `@FlowBind(modelKey = "leave_process")`:声明这个 Service 绑定 `leave_process` 流程模型 2. `@FlowStart`:`submit()` 方法执行成功后,切面自动发起流程 3. `businessKeySpEl = "'leave:' + #leave.id"`:业务唯一标识,用 SpEL 从参数取值 4. `titleSpEl`:流程标题(审批人看到的标题) 5. `userIdSpEl` / `userNameSpEl`:发起人信息 6. `variablesSpEl`:流程变量(天数、原因),审批节点可以用这些变量做条件判断 **注意:`submit()` 方法里没有任何 Flowable API。** 它只做业务的事——存数据。流程发起是切面在方法返回后自动做的。 ### SpEL 上下文能取什么 `@FlowStart` 的所有 SpEL 表达式都能访问: | 变量 | 含义 | |------|------| | `#参数名` | 方法参数(按名称,如 `#leave`) | | `#p0`、`#p1` | 方法参数(按位置) | | `#result` | 方法返回值 | 这意味着你可以从业务对象里取任何字段,拼成流程标题、业务键、流程变量。不用手动 build Map。 --- ## 五、第三步:用 @FlowCallback 接收流程结果 流程审批结束后,要更新业务状态(比如审批通过 → 改成 APPROVED)。用 `@FlowCallback`: ```java @FlowBind(modelKey = "leave_process", businessType = "leave") @Service public class LeaveService { // ... 上面的 submit 方法 @FlowCallback(on = {FlowCallback.ON_COMPLETED, FlowCallback.ON_REJECTED}) public void onFlowResult(FlowEventContext ctx) { // 从流程事件上下文取业务键 String businessKey = ctx.getBusinessKey(); // "leave:123" Long leaveId = Long.parseLong(businessKey.split(":")[1]); // 根据事件类型更新业务状态 if (FlowCallback.ON_COMPLETED.equals(ctx.getEventType())) { leaveMapper.updateStatus(leaveId, "APPROVED"); } else if (FlowCallback.ON_REJECTED.equals(ctx.getEventType())) { leaveMapper.updateStatus(leaveId, "REJECTED"); } } } ``` `@FlowCallback` 支持的事件类型: | 常量 | 含义 | |------|------| | `ON_COMPLETED` | 流程通过(全部审批完成) | | `ON_REJECTED` | 流程驳回 | | `ON_CANCELED` | 流程撤回/取消 | | `ON_TASK_CREATED` | 新待办产生(某节点待审批) | | `ON_TASK_COMPLETED` | 某个审批节点处理完 | | `ON_TASK_ASSIGNED` | 任务分配/签收 | **回调怎么触发的?** 流程服务通过 Redis Pub/Sub 或 Webhook 推事件,业务侧的 `FlowEventSubscriber` 自动路由到标了 `@FlowCallback` 的方法。不依赖流程引擎直连。 --- ## 六、底层原理:FlowStartAspect 做了什么 如果你好奇"切面到底干了什么",看一下 `FlowStartAspect` 的核心逻辑(源码精简版): ```java @Around("@annotation(flowStart)") public Object around(ProceedingJoinPoint pjp, FlowStart flowStart) throws Throwable { // 1. 先执行业务方法 Object result = pjp.proceed(); // 2. 解析 modelKey(先取 @FlowStart,没有就取 @FlowBind) String modelKey = flowStart.modelKey(); if (!StringUtils.hasText(modelKey)) { modelKey = resolveFlowBind(pjp).modelKey(); } // 3. 构建 SpEL 上下文(方法参数 + 返回值) EvaluationContext ctx = buildSpelContext(pjp, result); // 4. 解析所有 SpEL 表达式 String businessKey = eval(flowStart.businessKeySpEl(), ctx, String.class); String title = eval(flowStart.titleSpEl(), ctx, String.class); String userId = eval(flowStart.userIdSpEl(), ctx, String.class); Map variables = eval(flowStart.variablesSpEl(), ctx, Map.class); // 5. 通过 FlowClient 发 HTTP 调流程服务 flowClient.startProcess(modelKey, businessKey, businessType, title, variables, userId, userName, deptId, deptName); return result; } ``` 就 5 步: 1. 业务方法先正常执行 2. 解析流程模型 Key 3. 用方法参数和返回值构建 SpEL 上下文 4. 解析所有 SpEL 表达式,取出业务键、标题、变量 5. 通过 `FlowClient` 发 HTTP 请求到流程服务,发起流程 **业务方法完全不知道流程引擎的存在。** 它只管存数据,流程发起是切面在背后做的。 --- ## 七、和传统 Flowable 接入对比 | 对比项 | 传统接入 | Forge 注解接入 | |--------|----------|----------------| | 业务代码侵入 | 直接注入 `RuntimeService` | 零侵入,只加注解 | | 发起流程 | 手动 `startProcessInstanceByKey` | `@FlowStart` 自动发起 | | 传流程变量 | 手动 build Map | SpEL 表达式 | | 流程回调 | 写 `ExecutionListener` / `TaskListener` | `@FlowCallback` 注解 | | 引擎耦合 | 业务和引擎同库 | 引擎独立服务,HTTP 通信 | | 换引擎 | 大面积改代码 | 改 `FlowClient` 实现 | | 新增流程 | 复制粘贴一套 Flowable API | 加 `@FlowBind` + `@FlowStart` | 最大的区别是:**传统接入是"业务调流程引擎",Forge 是"流程引擎服务化,业务通过注解声明流程意图"。** --- ## 八、流程模型在哪里配? 代码侧只管"发起和回调",流程怎么流转(几级审批、谁来批、按什么条件走)是在后台可视化配的: 1. 在"流程模型管理"里创建模型,Key 填 `leave_process` 2. 用 BPMN 设计器或钉钉风格审批流设计器画流程 3. 配审批节点:部门主管 → 财务 → 老板(超 3 天才到老板) 4. 配条件分支:`days > 3` 走老板审批,否则到主管就结束 5. 发布模型 业务代码不用改。流程怎么走,全在模型配置里。今天改三级审批,明天改两级,后台拖一下就行。 --- ## 九、几个实操要点 ### 1. businessKey 一定要唯一 `businessKeySpEl` 是流程实例和业务数据的关联纽带。建议用 `"业务类型:" + 业务ID` 的格式,比如 `"leave:123"`。回调时靠它反查业务数据。 ### 2. 流程变量要提前想好 `variablesSpEl` 里传的变量,流程模型里的条件分支会用到。比如 `days` 这个变量,流程里会判断 `days > 3` 决定走哪条线。变量名要和模型里一致。 ### 3. 回调要做幂等 `@FlowCallback` 可能因为网络重试被调多次。更新业务状态时要做幂等: ```java @FlowCallback(on = FlowCallback.ON_COMPLETED) public void onCompleted(FlowEventContext ctx) { LeaveApply leave = leaveMapper.selectById(extractId(ctx)); if ("APPROVED".equals(leave.getStatus())) { return; // 已经处理过,跳过 } leaveMapper.updateStatus(leave.getId(), "APPROVED"); } ``` ### 4. skipOnError 默认 true `@FlowStart` 有个 `skipOnError` 属性,默认 `true`:业务方法抛异常时不发起流程。这很合理——数据都没存成功,不该发起流程。如果你希望即使业务异常也发起(少见),设成 `false`。 ### 5. 流程服务和业务服务要能互通 `forge-flow-client` 通过 HTTP 调流程服务,配置好地址: ```yaml forge: flow: client: url: http://localhost:8581 # 流程服务地址 ``` 事件回调走 Redis Pub/Sub 或 Webhook,确保两边能通信。 --- ## 十、总结:3 个注解替代一堆 Flowable API | 你想做的事 | 传统写法 | Forge 写法 | |-----------|----------|------------| | 绑定流程模型 | 硬编码 modelKey | `@FlowBind(modelKey="leave_process")` | | 发起流程 | `runtimeService.startProcessInstanceByKey(...)` | `@FlowStart(businessKeySpEl=...)` | | 传流程变量 | 手动 build Map | `variablesSpEl = "{'days': #leave.days}"` | | 接收审批结果 | 写 Listener | `@FlowCallback(on=ON_COMPLETED)` | | 接收驳回 | 写 Listener | `@FlowCallback(on=ON_REJECTED)` | 不是说 Flowable 的 API 没用,而是**业务代码不该直接碰流程引擎 API**。把流程引擎服务化、把接入注解化,业务代码才能干净、可维护、可移植。 请假、报销、采购、合同——每个业务流程接进来,都是加 `@FlowBind` + `@FlowStart` + `@FlowCallback` 这三行注解的事。这才是工作流该有的接入方式。 --- **源码自取**: - Gitee:https://gitee.com/ForgeLab/forge-admin - GitHub:https://github.com/yaomindong1996/forge-admin - 在线演示:http://www.dlforgelab.com:8084/forge/login (admin / 123456) > 你们项目接 Flowable 是直接注入 RuntimeService,还是做了封装?评论区聊聊,看看大家的接入方式。