Java案例如何实现工作流?

wen python案例 3

Java案例如何实现工作流?从零搭建完整项目实战指南

📖 目录导读

  1. 工作流核心概念与选型分析
  2. 项目环境搭建与依赖配置
  3. 数据库表设计(核心三板斧)
  4. 核心代码实现:流程引擎与状态机
  5. Web接口设计与前端联动
  6. 业务案例:请假审批流程完整实现
  7. 常见问题与性能优化问答

工作流核心概念与选型分析

什么是工作流?
工作流(Workflow)是将业务规则、角色权限和任务流转抽象为可配置的流程模型,在Java生态中,实现工作流主要有三种方式:

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:可以使用mxGraphvue-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案例,你可以看到实现工作流本质上就是状态机+节点配置+权限校验+并发处理的组合,无论是简单的请假审批还是复杂的工单系统,核心都在于解耦流程定义与实例运行,希望这个实战指南能帮助你快速搭建属于自己的工作流引擎,如果有其他问题,欢迎在评论区交流。

抱歉,评论功能暂时关闭!