Spring Boot集成Flowable,三个注解实现请假审批流程
在众多工作流引擎中,Flowable 的使用率非常高。然而,许多团队在集成之后,普遍反映流程引擎与业务代码容易出现紧密耦合,导致项目结构变得越来越混乱。
常见的问题代码示例如下:
// 业务 Service 里手动调 Flowable RuntimeService@Service
public class Lea veService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
public void submit(Lea veApply lea ve) {
// 1. 存业务表
lea veMapper.insert(lea ve);
// 2. 手动发起流程
Map<String, Object> vars = new HashMap<>();
vars.put("days", lea ve.getDays());
vars.put("applicant", lea ve.getUserId());
ProcessInstance pi = runtimeService.startProcessInstanceByKey(
"lea ve_process", "lea ve:" + lea ve.getId(), vars);
// 3. 把 instanceId 存回业务表
lea ve.setProcessInstanceId(pi.getId());
lea veMapper.updateById(lea ve);
}
}
这种做法的弊端在哪里?
- 业务代码直接入侵了流程引擎的 API,例如
RuntimeService、TaskService等被散落在各个业务 Service 中,显得杂乱无章。 - 流程引擎与业务数据库高度耦合:未来若打算将流程引擎拆分为独立服务,现有代码将面临大规模重写。
- 流程回调逻辑颇为繁琐:流程结束后如需更新业务状态,不得不编写监听器、发布事件并单独处理,过程迂回曲折。
- 每个业务流程都需要重复执行这一套操作:无论是请假、报销还是采购,每个 Service 中都需要注入 Flowable API,不仅代码量翻倍,而且容易出错。
Forge Admin 提供的解决方案是:将流程引擎拆分为一个独立的服务。在业务侧,仅需使用 3 个注解和 SpEL 表达式便可完成流程集成。本文将通过构建一个完整的请假审批流程,向大家演示这套机制。
一、整体架构:业务与流程引擎实现解耦
首先,让我们理清 Forge 的流程架构,这样后续阅读代码时会更清晰:
┌─────────────────────────┐ ┌─────────────────────────┐
│ 业务服务(admin) │ │ 流程服务(flow) │
│ │ │ │
│ Lea veService │ │ Flowable 引擎 │
│ @FlowBind │ HTTP │ BPMN 流程模型 │
│ @FlowStart ────────────┼───────►│ 任务流转 │
│ @FlowCallback ◄───────┼───────┤ 事件推送(Redis/Webhook)│
│ │ │ │
└─────────────────────────┘ └─────────────────────────┘
核心设计理念:
- 流程引擎作为独立服务(
forge-flow)存在,不与业务服务混合部署。 - 业务侧不再直接调用 Flowable API,而是通过
FlowClient发送 HTTP 请求来与流程服务通信。 - 仅需 3 个注解,即可完成流程的发起、绑定与回调,对业务代码几乎实现零侵入。
- 采用 SpEL 表达式传递参数,彻底告别手动拼装 Map 的繁琐操作。
二、三个注解:流程接入所需的全部 API
整套机制的核心仅包含三个注解,首先请记住它们各自的职责:
| 注解 | 位置 | 作用 |
|---|---|---|
@FlowBind | 类上 | 声明此 Service 绑定的具体流程模型 |
@FlowStart | 方法上 | 在方法执行成功后,自动发起流程 |
@FlowCallback | 方法上 | 用于接收流程事件回调(如通过、驳回、取消等) |
仅此三个。不再需要 RuntimeService 和 TaskService,也无需手动调用 startProcessInstanceByKey。代码简洁利落。
三、第一步:定义业务实体
首先创建一个请假申请实体,它是一个标准的业务表,与流程引擎不存在任何关联:
@Data
@TableName("biz_lea ve_apply")
public class Lea veApply {
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 接入流程发起
这是最关键的环节,直接查看完整代码:
@FlowBind(modelKey = "lea ve_process", businessType = "lea ve")
@Service
public class Lea veService {
@FlowStart(
businessKeySpEl = "'lea ve:' + #lea ve.id",
titleSpEl = "#lea ve.applicantName + ' 的请假申请'",
userIdSpEl = "#lea ve.applicantId",
userNameSpEl = "#lea ve.applicantName",
variablesSpEl = "{'days': #lea ve.days, 'reason': #lea ve.reason}"
)
public Lea veApply submit(Lea veApply lea ve) {
// 1. 保存业务表 —— 这是你唯一需要关心的业务逻辑
lea veMapper.insert(lea ve);
lea ve.setStatus("PENDING");
lea veMapper.updateById(lea ve);
return lea ve;
// 2. 流程发起由 @FlowStart 切面自动完成,无需编写任何 Flowable 代码
}
}
这段代码实现了什么?
@FlowBind(modelKey = "lea ve_process"):声明当前 Service 绑定的是lea ve_process这个流程模型。@FlowStart:当submit()方法执行成功后,切面会自动触发流程。businessKeySpEl = "'lea ve:' + #lea ve.id":通过 SpEL 从参数中提取值,生成业务唯一标识。titleSpEl:定义了流程标题(即审批人看到的标题)。userIdSpEl/userNameSpEl:用于传递发起人相关信息。variablesSpEl:设置流程变量(例如请假天数、原因),后续审批节点可根据这些变量进行条件判断。
关键提醒:submit() 方法内部没有出现任何 Flowable API。它只专注于业务逻辑——数据持久化。流程的发起由切面在方法返回后自动执行,实现了完全的解耦。
SpEL 上下文支持哪些变量
@FlowStart 中的所有 SpEL 表达式都可以访问以下内容:
| 变量 | 含义 |
|---|---|
#参数名 | 方法参数(按名称引用,例如 #lea ve) |
#p0、#p1 | 方法参数(按位置引用) |
#result | 方法的返回值 |
这意味着你可以从业务对象中灵活提取任意字段,组合生成流程标题、业务键或流程变量。无需手动构建 Map,显著提升了开发效率。
五、第三步:使用 @FlowCallback 接收流程结果
当流程审批结束后,通常需要更新业务状态(例如审批通过后改为 APPROVED)。利用 @FlowCallback 可以轻松实现:
@FlowBind(modelKey = "lea ve_process", businessType = "lea ve")
@Service
public class Lea veService {
// ... 包含上文的 submit 方法
@FlowCallback(on = {FlowCallback.ON_COMPLETED, FlowCallback.ON_REJECTED})
public void onFlowResult(FlowEventContext ctx) {
// 从流程事件上下文中获取业务键
String businessKey = ctx.getBusinessKey(); // "lea ve:123"
Long lea veId = Long.parseLong(businessKey.split(":")[1]);
// 根据事件类型更新业务状态
if (FlowCallback.ON_COMPLETED.equals(ctx.getEventType())) {
lea veMapper.updateStatus(lea veId, "APPROVED");
} else if (FlowCallback.ON_REJECTED.equals(ctx.getEventType())) {
lea veMapper.updateStatus(lea veId, "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 variables = eval(flowStart.variablesSpEl(), ctx, Map.class);
// 5. 通过 FlowClient 发送 HTTP 请求调用流程服务
flowClient.startProcess(modelKey, businessKey, businessType, title, variables, userId, userName, deptId, deptName);
return result;
}
总共包含 5 个步骤:
- 首先,业务方法正常执行。
- 解析出流程模型的 Key。
- 利用方法的参数和返回值构建 SpEL 上下文。
- 解析所有的 SpEL 表达式,提取出业务键、标题以及流程变量。
- 通过
FlowClient向流程服务发起 HTTP 请求,启动流程。
整个过程中,业务方法完全感知不到流程引擎的存在。它只需负责数据存储,流程的发起由切面在后台默默完成。
七、与传统 Flowable 接入方式对比
| 对比项 | 传统接入方式 | Forge 注解接入方式 |
|---|---|---|
| 业务代码入侵性 | 直接注入 RuntimeService | 零入侵,仅需添加注解 |
| 流程发起方式 | 手动调用 startProcessInstanceByKey | @FlowStart 自动触发 |
| 流程变量传递 | 手动构建 Map | 通过 SpEL 表达式完成 |
| 流程回调实现 | 编写 ExecutionListener/TaskListener | 使用 @FlowCallback 注解 |
| 引擎耦合程度 | 业务与引擎共用数据库 | 引擎独立部署,通过 HTTP 通信 |
| 引擎更换难度 | 涉及大面积代码修改 | 仅需修改 FlowClient 的实现即可 |
| 新增流程工作量 | 需要复制粘贴大量 Flowable API 代码 | 只需添加 @FlowBind + @FlowStart |
两者最大的差异在于:传统的做法是“业务代码直接调用流程引擎”,而 Forge 的做法是“将流程引擎服务化,业务通过注解声明其流程意图”。这一思路的根本转变,也带来了代码量的天壤之别。
八、流程模型在哪里配置?
在代码层面,你只需要负责“发起和回调”。至于流程具体如何流转(例如几级审批、由谁审批、依据什么条件分支),是通过后台的可视化界面来配置的:
- 进入“流程模型管理”模块,创建一个新模型,并将 Key 设置为
lea ve_process。 - 使用 BPMN 设计器或钉钉风格的审批流设计器来绘制流程图。
- 配置审批节点:例如部门主管 → 财务 → 老板(当请假超过 3 天时才会流转到老板)。
- 设定条件分支:例如
days > 3时走老板审批路径,否则只需主管审批即可结束。 - 发布该模型。
在整个过程中,业务代码无需任何改动。流程怎么走,完全由模型配置决定。今天需要改为三级审批,明天又想改成两级,只需在后台拖拽配置即可,业务代码不会受到任何影响。
九、几个实操要点
1. businessKey 必须保证唯一性
businessKeySpEl 是连接流程实例与业务数据的关键纽带。建议采用 "业务类型:" + 业务ID 的格式,例如 "lea ve:123"。在回调阶段,可以依靠它来反向查询业务数据,简单且可靠。
2. 流程变量需要提前规划
variablesSpEl 中传递的变量,会被流程模型中的条件分支所引用。例如 days 这个变量,流程配置中可能会判断 days > 3 来决定走哪条路线。请务必确保变量名与模型中的定义保持一致,否则条件判断会失效。
3. 回调逻辑需要实现幂等性
@FlowCallback 可能因为网络重试等原因被重复调用。在更新业务状态时,务必进行幂等检查:
@FlowCallback(on = FlowCallback.ON_COMPLETED)
public void onCompleted(FlowEventContext ctx) {
Lea veApply lea ve = lea veMapper.selectById(extractId(ctx));
if ("APPROVED".equals(lea ve.getStatus())) {
return; // 已经处理过,跳过
}
lea veMapper.updateStatus(lea ve.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="lea ve_process") |
| 发起流程 | runtimeService.startProcessInstanceByKey(...) | @FlowStart(businessKeySpEl=...) |
| 传递流程变量 | 手动构建 Map | variablesSpEl = "{'days': #lea ve.days}" |
| 接收审批结果 | 编写 Listener | @FlowCallback(on=ON_COMPLETED) |
| 接收驳回通知 | 编写 Listener | @FlowCallback(on=ON_REJECTED) |
这并非否定 Flowable API 的价值,而是强调业务代码不应直接与流程引擎 API 打交道。通过将流程引擎服务化、将接入过程注解化,业务代码才能变得简洁、易于维护且具备可移植性。
无论是请假、报销、采购还是合同——每个业务流程的接入工作,都简化为添加 @FlowBind + @FlowStart + @FlowCallback 这三行注解。这才是工作流应有的理想集成方式。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
阿里云Qoder CN灵码AI助手免费版及credits计费指南
阿里云QoderCN(原通义灵码)是一款AI智能编码助手,提供IDE插件、独立IDE等形态,覆盖编码及日常办公场景。产品分个人社区版(免费)、个人专业版、企业标准版和企业VPC版,采用Credits计费模式,支持多种AI模型。
基于大模型的城市文旅知识图谱构建与内容分发
大模型构建城市知识图谱时优先采信权威信源。贵港西江传媒联合《度假旅游》杂志,通过本地采编、期刊发布、阿里云多平台分发模式,产出产业文旅等结构化内容,提升AI知识库收录权重,为城市品牌长效传播提供可复制路径。
贵州文旅AIGEO内容运营本地媒体落地实践
针对贵州文旅行业在阿里云平台的内容运营痛点,总结合规发文规则,包括弱化营销、避免引流信息与极限词。以《度假旅游杂志》在贵阳设立本地化运营站点为例,为黔域文旅商家产出合规原创内容,提升AI平台收录权重。同时指出商家常踩的审核红线,强调以干货分享获取自然流量。
阿里内部禁用Claude Code OpenCode成替代方案
ClaudeCode对国内用户定向封禁,网传阿里内部已全面禁用。OpenCode作为开源替代,支持接入多种AI模型,通过CCSwitch或手动配置可无缝迁移原有MCP与AgentSkill,规避账号风险。
年实测Homebrew安装配置国内源多种安装方式一篇搞定
Homebrew是macOS上流行的包管理工具,提供pkg安装包、脚本安装和Git克隆等多种安装方式。国内用户推荐使用镜像脚本安装并配置中科大或清华大学镜像源以加速下载。常用命令包括brewinstall安装工具、brewinstall--cask安装图形应用、brewupdate更新及brewdoctor诊断环境。
- 日榜
- 周榜
- 月榜
相关攻略
2026-07-06 16:30
2026-07-06 16:30
2026-07-06 16:29
2026-07-06 16:29
2026-07-06 16:29
2026-07-06 16:29
2026-07-06 16:29
2026-07-06 16:29
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

