Spring Boot 整合 Flowable 工作流:用三个注解搭一个请假审批流程

时间:2026-07-07 08:51:41 来源:互联网

在众多工作流引擎中,Flowable的应用最为广泛。然而,许多团队在集成后常感到流程引擎与业务代码严重耦合,导致系统结构愈发混乱。

典型的乱象长这样:

// 业务 Service 里手动调 Flowable RuntimeService@Servicepublic class LeaveService {@Autowiredprivate RuntimeService runtimeService;@Autowiredprivate TaskService taskService;@Autowiredprivate HistoryService historyService;public void submit(LeaveApply leave) {// 1. 存业务表leaveMapper.insert(leave);// 2. 手动发起流程Map<String, Object> 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);}}

问题在哪?

  1. 业务代码侵入流程引擎 API:RuntimeServiceTaskService散落在业务 Service 里
  2. 流程引擎和业务库耦合:如果流程引擎拆成独立服务,这些代码全要改
  3. 流程回调难写:流程结束后要更新业务状态,得写监听器、发事件、再处理
  4. 每个业务流程都要重复这一套:请假、报销、采购,每个 Service 都注入一遍 Flowable API

Forge Admin 的做法是:将流程引擎分离为独立服务,业务侧仅需通过3个注解与SpEL表达式即可完成流程对接。本文将以完整的请假审批流程为例,详细介绍这一机制。

一、整体架构:业务和流程引擎解耦

先看清楚 Forge 的流程架构,再看代码就不会懵:

┌─────────────────────────┐┌─────────────────────────┐│ 业务服务(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

三、第一步:定义业务实体

先建一个请假申请实体,这就是普通业务表,和流程引擎无关:

@Data@TableName("biz_leave_apply")public class LeaveApply {private Long id;private String applicantId;// 申请人IDprivate String applicantName;// 申请人姓名private Integer days;// 请假天数private String reason; // 请假原因private String status; // 业务状态:PENDING/APPROVED/REJECTEDprivate String processInstanceId;// 流程实例ID(可选,用于关联)}

注意:业务表不需要包含任何 Flowable 的字段(act_*那些表由流程引擎自己管)。业务表只存业务数据,流程数据在流程服务那边。

四、第二步:用 @FlowBind + @FlowStart 接入发起流程

这是最关键的一步。看完整代码:

@FlowBind(modelKey = "leave_process", businessType = "leave")@Servicepublic 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. @FlowStartsubmit()方法执行成功后,切面自动发起流程
  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

@FlowBind(modelKey = "leave_process", businessType = "leave")@Servicepublic 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的核心逻辑(源码精简版):

@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<String, Object> 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 MapSpEL 表达式
流程回调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可能因为网络重试被调多次。更新业务状态时要做幂等:

@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 调流程服务,配置好地址:

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 MapvariablesSpEl = "{'days': #leave.days}"
接收审批结果写 Listener@FlowCallback(on=ON_COMPLETED)
接收驳回写 Listener@FlowCallback(on=ON_REJECTED)

不是说 Flowable 的 API 没用,而是业务代码不该直接碰流程引擎 API。把流程引擎服务化、把接入注解化,业务代码才能干净、可维护、可移植。

综上,通过使用三个注解而非直接调用Flowable API,可有效隔离流程引擎与业务代码,提升系统的可维护性与可移植性。这种将引擎服务化、接入注解化的设计,是工作流集成的理想方案。