Java案例如何实现工作流?从零搭建完整项目实战指南
📖 目录导读
工作流核心概念与选型分析
什么是工作流?
工作流(Workflow)是将业务规则、角色权限和任务流转抽象为可配置的流程模型,在Java生态中,实现工作流主要有三种方式:

- 开源框架:Activiti、Flowable、Camunda(功能完整但较重)
- 轻量级状态机:Spring Statemachine、Squirrel(适合简单线性流程)
- 自定义实现:基于职责链+状态模式(灵活可控,适合复杂业务)
选型建议:
如果你的业务需要可视化画布、BPMN2.0规范支持,选择Flowable(Activiti的进化版);如果是内部小流程,推荐用状态模式+数据库驱动的自定义方案,本文将以该方案为主线演示。
项目环境搭建与依赖配置
技术栈:Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis(可选)
关键pom依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
application.yml配置(关键部分):
spring:
datasource:
url: jdbc:mysql://localhost:3306/workflow_db?useUnicode=true&characterEncoding=utf-8
username: root
password: your_password
redis:
host: localhost
port: 6379
小提示:Redis用于缓存流程定义和待办任务计数,非必须但强烈推荐。
数据库表设计(核心三板斧)
工作流数据模型必须包含以下三张核心表:
1 流程定义表(wf_process_define)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| process_key | varchar(50) | 唯一标识(如"leave_approve") |
| process_name | varchar(100) | 流程名称 |
| version | int | 版本号 |
| status | tinyint | 0=草稿 1=启用 2=停用 |
2 流程节点表(wf_node)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| process_id | bigint | 关联流程定义ID |
| node_name | varchar(50) | 节点名称(如"部门经理审批") |
| node_type | varchar(20) | START/APPROVE/REJECT/END |
| assignee_type | varchar(10) | ROLE/USER/EXPRESSION |
| assignee_value | varchar(100) | 具体审批人ID或角色ID |
3 流程实例表(wf_process_instance)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| process_id | bigint | 关联流程定义ID |
| business_key | varchar(100) | 业务主键(如请假单ID) |
| current_node_id | bigint | 当前所在节点ID |
| status | tinyint | 0=进行中 1=通过 2=驳回 3=撤销 |
| creator | bigint | 发起人ID |
| create_time | datetime | 创建时间 |
✅ 核心设计原则:流程定义与实例分离,实现流程配置化。
核心代码实现:流程引擎与状态机
1 流程引擎接口定义
public interface WorkflowEngine {
// 启动流程
ProcessInstance startProcess(String processKey, String businessKey, Map<String, Object> variables);
// 提交审批
boolean approve(Long instanceId, Long userId, String comment);
// 驳回
boolean reject(Long instanceId, Long userId, String comment);
// 获取当前待办
List<Task> getPendingTasks(Long userId);
}
2 状态流转核心逻辑(状态模式)
public class ApproveState implements NodeState {
@Override
public ProcessResult handle(ProcessInstance instance, User operator, String action) {
// 1. 校验操作人是否有权限
checkAssignee(instance.getCurrentNode(), operator);
// 2. 根据action执行不同策略
if ("APPROVE".equals(action)) {
// 获取下一个节点
Node nextNode = nodeMapper.getNextNode(instance.getCurrentNode().getId());
if (nextNode.getNodeType().equals("END")) {
instance.setStatus(1); // 流程完成
} else {
instance.setCurrentNodeId(nextNode.getId());
}
} else if ("REJECT".equals(action)) {
// 回到上一个节点或发起人
Node prevNode = nodeMapper.getPreviousNode(instance.getCurrentNode().getId());
instance.setCurrentNodeId(prevNode.getId());
}
// 3. 保存日志
saveHistoryLog(instance, operator, action);
return new ProcessResult(true, "操作成功");
}
}
3 并发控制:基于Redis分布式锁
public boolean approveWithLock(Long instanceId, Long userId, String comment) {
String lockKey = "workflow:" + instanceId;
boolean locked = redisLock.tryLock(lockKey, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("流程正在处理,请稍后再试");
}
try {
ProcessInstance instance = instanceMapper.selectById(instanceId);
// 乐观锁检查版本
int result = instanceMapper.updateStatus(instanceId, instance.getStatus(), instance.getVersion());
if (result == 0) {
throw new ConcurrentModificationException("并发冲突,请重试");
}
return doApprove(instance, userId, comment);
} finally {
redisLock.unlock(lockKey);
}
}
Web接口设计与前端联动
1 核心API端点
POST /api/workflow/start # 启动流程 POST /api/workflow/approve # 审批通过 POST /api/workflow/reject # 驳回 GET /api/workflow/tasks # 我的待办 GET /api/workflow/history # 流程轨迹
2 前端Vue组件示例(简化)
<template>
<div>
<el-table :data="pendingTasks">
<el-table-column prop="processName" label="流程名称" />
<el-table-column prop="createTime" label="到达时间" />
<el-table-column label="操作">
<template #default="scope">
<el-button @click="handleApprove(scope.row)">通过</el-button>
<el-button @click="handleReject(scope.row)">驳回</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
交互原则:前端仅做展示和事件触发,所有业务逻辑和状态流转由后端统一控制。
业务案例:请假审批流程完整实现
1 流程定义配置
@PostConstruct
public void initProcess() {
// 创建请假流程定义
ProcessDefine define = new ProcessDefine();
define.setProcessKey("leave_approve");
define.setProcessName("员工请假审批");
define.setVersion(1);
// 创建节点链
Node startNode = Node.builder().nodeName("发起申请").nodeType("START").build();
Node managerNode = Node.builder().nodeName("部门经理审批").nodeType("APPROVE")
.assigneeType("ROLE").assigneeValue("MANAGER").build();
Node hrNode = Node.builder().nodeName("HR审批").nodeType("APPROVE")
.assigneeType("ROLE").assigneeValue("HR").build();
Node endNode = Node.builder().nodeName("结束").nodeType("END").build();
// 设置顺序
startNode.setNextNode(managerNode);
managerNode.setNextNode(hrNode);
hrNode.setNextNode(endNode);
processDefineService.save(define, Arrays.asList(startNode, managerNode, hrNode, endNode));
}
2 启动流程并生成待办
public ProcessInstance startLeaveFlow(LeaveForm leaveForm) {
// 1. 保存业务表单
leaveFormMapper.insert(leaveForm);
// 2. 启动流程
ProcessInstance instance = workflowEngine.startProcess(
"leave_approve",
leaveForm.getId().toString(),
Map.of("days", leaveForm.getDays(), "reason", leaveForm.getReason())
);
// 3. 生成第一个待办任务
Task task = new Task();
task.setInstanceId(instance.getId());
task.setNodeId(instance.getCurrentNodeId());
task.setAssignee(Role.MANAGER.getUserIds()); // 批量分配给所有经理角色人员
taskMapper.insert(task);
return instance;
}
3 待办任务轮询与通知
@Scheduled(fixedRate = 5000)
public void pollPendingTasks() {
// 从Redis或数据库查询超时未处理的待办
List<Task> timeoutTasks = taskMapper.selectOverdueTasks(2, TimeUnit.HOURS);
timeoutTasks.forEach(task -> {
// 发送WebSocket通知或钉钉消息
notificationService.sendReminder(task.getAssignee(), "您有一个待办即将超时");
});
}
常见问题与性能优化问答
Q1:流程节点多时性能如何优化?
A:采用路径缓存——启动时将节点顺序关系存入Redis(如NEXT_NODE:节点ID),避免每次流转都查数据库,同时使用MyBatis-Plus的流式查询处理批量任务。
Q2:如何避免重复审批?
A:数据库层面加instance_id + node_id + status联合唯一索引;业务层面用Redis锁保证同一时间只有一个线程操作该实例。
Q3:支持动态驳回(退回指定节点)吗?
A:可以在流程定义表中加allow_jump_to_nodes字段(JSON数组),前端展示可跳转的节点列表,后端通过NodeJumpHandler校验后直接修改current_node_id,同时清理后续所有待办。
Q4:如何处理会签(多人审批)?
A:引入countersign_node表,记录会签配置(如“需2人通过”),发起任务时生成N个子任务,只有当approved_count >= threshold时才流向下一节点。
Q5:流程定义升级后如何处理存量实例?
A:提供两种策略:①迁移模式——将存量实例的节点与新版本节点映射;②兼容模式——旧实例按旧版本执行,新流程使用新版本,通常推荐后者,通过process_key + version隔离。
Q6:前端如何实时获取待办变化?
A:使用WebSocket订阅主题/topic/tasks/{userId},后端在任务创建、审批时推送WorkflowTaskEvent,前端监听后刷新列表。
Q7:如果项目需要部署到公司内网,无法联网,怎么获取工作流可视化工具?
A:可以使用mxGraph或vue-flow(开源)嵌入到管理后台,自己实现拖拽建模,后端只需提供节点CRUD和连线关系接口即可。
Q8:如何保证工作流日志的不可篡改性?
A:采用区块链式日志表,每条日志包含previous_hash,同时定期计算哈希值并存储到不可变存储(如ClickHouse),或者简单点用事务日志+数据库只读权限。
Q9:微服务架构下如何共享工作流引擎?
A:将工作流引擎独立为workflow-service,通过Feign接口对外提供REST API,使用分布式事务(Seata)保证跨服务的数据一致性,启动流程”和“扣减库存”需要同时成功。
Q10:节点权限如何做到细粒度?
A:支持SPEL表达式,例如#employee.department == #node.dept,可以在assignee_value字段存储表达式字符串,运行时通过ExpressionEvaluator动态解析用户上下文。
通过本文的Java案例,你可以看到实现工作流本质上就是状态机+节点配置+权限校验+并发处理的组合,无论是简单的请假审批还是复杂的工单系统,核心都在于解耦流程定义与实例运行,希望这个实战指南能帮助你快速搭建属于自己的工作流引擎,如果有其他问题,欢迎在评论区交流。