找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1531

积分

0

好友

225

主题
发表于 昨天 18:54 | 查看: 4| 回复: 0

工作流介绍

1.1 为什么使用工作流

在开发过程中,我们经常会遇到各类审批流程需求。一个典型的请假审批可能包含“开始 -> 员工申请 -> 领导审批 -> 老板审批 -> 结束”等多个环节。如果单纯使用数据库字段(如 status: 0->开始, 1->申请...)和代码逻辑来硬编码流转过程,虽然能够实现功能,但会导致业务代码异常复杂。如果再考虑驳回到上一节点等操作,维护成本将急剧上升。为了解决代码硬编码带来的高成本和低可维护性问题,工作流引擎应运而生。

1.2 工作流是什么

工作流是对业务步骤与规则进行抽象和概括性描述的一种方法。它使用特定的建模语言为业务流程建模,使其能在计算机上运行,并由计算机进行推动和计算。本质上,工作流是一个复杂版本的状态机。

简单状态
下图展示了工作流退化为简单状态机的例子:角色“路人乙”的状态在“站起来 -> 走起来 -> 跑起来 -> 慢下来 -> 站起来”之间循环。实现这种简单的状态切换,仅需一个字段记录当前状态即可。

图片

然而,当状态维度增加、流转条件变得复杂时,单纯用字段记录状态的方式就会捉襟见肘。

复杂状态
下图描述了“路人甲”发工资后的决策流程,其选择取决于余额大小。这个流程引入了条件判断(网关)和并行任务,已经无法用一个简单的状态字段来清晰描述和管理。

图片

工作流的核心价值在于解耦业务宏观流程与微观逻辑。它允许熟悉业务的人通过可视化工具设计整体流程,而开发人员只需关注单个节点(如一个审批任务或一个自动调用)的具体实现。这就像建造体育场,架构师负责整体蓝图,而工人只需专注于砌好面前的砖。

1.3 工作流不能解决的问题

工作流适用于有明确前后关联和顺序的事务。对于彼此独立、没有流转顺序的任务,工作流并非最佳选择。

图片

例如,“路人丙”每天要完成学习、谈恋爱、玩游戏这三件独立的事。它们之间没有必然的顺序,但每项任务的完成状态共同决定了“今日任务”是否完结。这类场景更适合使用CMMN(案例管理模型和符号)来建模。本文主要介绍基于BPMN协议的工作流。

BPMN2.0协议

2.1 什么是BPMN2.0协议

图片
BPMN(业务流程模型与符号)2.0是一种国际通用的业务流程建模语言标准。它就像业务领域的“普通话”,既能让人直观理解,也能被计算机准确解析和执行,极大地降低了业务与技术之间的沟通成本。

2.2 BPMN2.0协议元素介绍

协议中的元素主要分为四大类:事件(Event)、任务(Task)、连线(Sequence Flow)、网关(Gateway)。一个有效的流程必须包含一个开始事件和至少一个结束事件。网关用于控制流程的分支与合并逻辑,各类任务节点则承担具体的业务操作,所有节点通过连线连接。

下面简要介绍几种核心元素的作用:

网关:

  • 互斥网关 (Exclusive Gateway):又称排他网关,其行为类似于 if...else if...else,在多个出口路径中,有且仅有一条满足条件的路径会被执行。
  • 并行网关 (Parallel Gateway):其所有出口路径都会同时被执行,类似于开启多个线程并行处理任务,通常需要成对使用以开启和同步并行分支。
  • 包容性网关 (Inclusive Gateway):只要满足条件的出口路径都会被执行,类似于多个独立的 if 判断,条件之间并非互斥。

任务:
所有BPMN任务都源自一个抽象任务,其核心行为是:1) 当流程到达该任务时做什么? 2) 当任务收到“信号(Trigger)”时,是否能继续流转?

  • 人工任务 (User Task):最常见的任务类型,通常需要人工干预。它包含“签收人(Assignee)”等变量,流程会在此等待指定人员处理完毕并触发完成信号后,才继续向下流转。
  • 服务任务 (Service Task):自动执行的任务节点。到达该节点时,引擎会自动执行一段预定义逻辑(如计算、调用RPC或HTTP接口),执行完毕后自动流转。这在构建微服务架构的应用集成时非常有用。
  • 接受任务 (Receive Task):一种被动等待的任务。到达该节点后,流程会暂停,不做任何操作,直到接收到一个外部触发信号才会继续。适用于需要人为判断后方可推进的阻塞点。
  • 调用活动 (Call Activity):类似于程序中的函数调用,用于引用和执行另一个流程定义,实现流程的模块化和复用。

下图展示了一个覆盖上述节点的手机生产流程模型示例:
图片

2.3 如何使用BPMN2.0协议

从使用者视角,主要关注三点:

  1. 流程设计:如何将业务逻辑转化为可视化的流程图(需要友好的绘图工具)。
  2. 流程驱动:如何通过API启动和推动流程实例。
  3. 任务处理:引擎如何通知我当前需要处理什么任务(需要清晰的事件或查询机制)。

下图概括了流程图从设计到运行的生命周期:
图片

Flowable简介

3.1 Flowable是什么

Flowable是一个基于BPMN 2.0协议的、用Java编写的轻量级开源业务流程引擎。它可用于部署BPMN 2.0流程定义,创建和管理流程实例,并访问运行中或历史的数据。Flowable可以嵌入到任何Java应用中,也可以作为独立的服务运行。

3.2 Flowable与Activiti

Flowable项目最初是从Activiti 6.0.0.Beta4分支衍生而来。目前,Flowable修复了Activiti 6中的许多问题,并进行了大量功能和性能优化,可以实现从Activiti到Flowable的低成本迁移。

Flowable实战

假设需要为一个公司内部系统实现员工请假审批流程。我们将使用以下步骤实现:

  1. 画图:创建SpringBoot项目(flowable-ui),启动Flowable UI设计器,在线绘制请假流程图并导出XML文件。
  2. 部署:创建另一个SpringBoot项目(flowable),将XML文件部署到流程引擎中。
  3. 启动:启动SpringBoot应用。
  4. 流转:通过编写代码调用API,实现流程的启动、任务查询与审批。
4.1 本地安装,启动flowable-ui

1. 创建SpringBoot项目
项目结构如下图所示,用于承载Flowable UI设计器。
图片

2. 关键代码与配置
主启动类 JavaSkillPointApplication.java:

package com.weige.javaskillpoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JavaSkillPointApplication {
    public static void main(String[] args) {
        SpringApplication.run(JavaSkillPointApplication.class, args);
    }
}

配置文件 application.yml:

server:
  port: 8088
flowable:
  idm:
    app:
      admin:
        # 登录的用户名
        user-id: admin
        # 登录的密码
        password: admin
        # 用户的名字
        first-name: wei
        last-name: kai
spring:
  # mysql连接信息
  datasource:
    # mysql8之后
    driver-class-name: com.mysql.cj.jdbc.Driver
    # mysql8之前
    # driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://43.143.132.109:3306/flowable?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT&nullCatalogMeansCurrent=true
    username: root
    password: ******
  jpa:
    properties:
      hibernate:
        hbm2ddl:
          auto: update
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
    open-in-view: true

注意

  • &nullCatalogMeansCurrent=true 参数对于Flowable自动建表至关重要。
  • 需提前在MySQL中创建名为 flowable 的数据库,项目启动时会自动创建所需表。
  • 启动后,通过 http://localhost:8088 访问,使用配置的用户名(admin)和密码(admin)登录。

3. 项目依赖 (pom.xml)
关键依赖包括Flowable UI模块、SpringBoot Web、JPA及MySQL驱动。

<dependencies>
    <!-- Springboot Web项目 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- idm依赖提供身份认证 -->
    <dependency>
        <groupId>org.flowable</groupId>
        <artifactId>flowable-spring-boot-starter-ui-idm</artifactId>
        <version>6.7.1</version>
    </dependency>
    <!-- modeler绘制流程图 -->
    <dependency>
        <groupId>org.flowable</groupId>
        <artifactId>flowable-spring-boot-starter-ui-modeler</artifactId>
        <version>6.7.1</version>
    </dependency>
    <!-- jpa -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- flowable 引擎 -->
    <dependency>
        <groupId>org.flowable</groupId>
        <artifactId>flowable-spring-boot-starter</artifactId>
        <version>6.7.1</version>
    </dependency>
    <!-- 其他依赖:lombok, test等 -->
</dependencies>

4. 启动与设计流程
启动项目后,登录Flowable UI,在“模型器”中创建新的BPMN模型,开始绘制“员工请假流程”。设计界面如下图所示:
图片

请假流程核心节点设计如下:

  • 开始事件 -> 请假申请(User Task):申请人变量为 ${studentUser}
  • 领导审批(User Task):候选组为 a
  • 互斥网关:判断请假天数 ${day} 是否大于2天。
  • 老板审批(User Task):候选组为 b (当天数>2时流转至此)。
  • 结束事件
  • 各审批节点均有“通过”和“驳回”两条输出连线,驳回时回到“请假申请”节点。

设计完成后,将模型导出为BPMN 2.0 XML文件(例如 leave.bpmn20.xml),供后续步骤使用。

4.2 创建flowable应用项目 部署并启动

1. 创建SpringBoot应用项目
此项目是实际运行流程的业务应用。
图片

主启动类 JavaSkillPointApplication.java (同上,略)。

2. 部署流程定义
resources/ 目录下创建 processes/ 文件夹,将上一步导出的 leave.bpmn20.xml 文件放入其中。当应用启动时,SpringBoot会自动扫描并部署该文件夹下的所有流程定义。

3. 应用配置
配置文件 application.yml:

server:
  port: 8081
spring:
  datasource:
    # 根据你的MySQL版本选择驱动
    driver-class-name: com.mysql.jdbc.Driver # MySQL 5
    # driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8
    url: jdbc:mysql://43.143.132.109:3306/flowable?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
    username: root
    password: ******

注意:数据库连接信息应与 flowable-ui 项目一致,确保它们操作同一数据库。

4. 编写流程操作测试代码
以下测试类展示了流程的完整操作API:

package com.weige.javaskillpoint;
import org.flowable.engine.*;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@SpringBootTest
class JavaSkillPointApplicationTests {
    @Autowired
    private RuntimeService runtimeService;
    @Autowired
    private TaskService taskService;
    @Autowired
    private HistoryService historyService;

    // 员工提交请假申请
    @Test
    void employeeApply() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("day", 5); // 请假天数
        variables.put("studentUser", "小明"); // 申请人
        // 使用流程定义的Key“leave”启动一个流程实例
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave", variables);
        // 查询并完成申请人的第一个任务(提交申请)
        Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
        taskService.complete(task.getId());
    }

    // 查询领导待审批任务
    @Test
    void queryLeaderTask() {
        // 查询候选组为‘a’(领导组)的所有任务
        List<Task> leaderTasks = taskService.createTaskQuery().taskCandidateGroup("a").list();
        for (Task task : leaderTasks) {
            Map<String, Object> variables = taskService.getVariables(task.getId());
            System.out.println("请假天数:" + variables.get("day") + ", 申请人:" + variables.get("studentUser"));
        }
    }

    // 领导审批(通过)
    @Test
    void leaderApproval() {
        List<Task> leaderTasks = taskService.createTaskQuery().taskCandidateGroup("a").list();
        Map<String, Object> approvalResult = new HashMap<>();
        approvalResult.put("outcome", "通过"); // 审批结果变量,对应流程图中‘通过’连线的条件
        for (Task task : leaderTasks) {
            taskService.complete(task.getId(), approvalResult);
        }
    }

    // 查询老板待审批任务
    @Test
    void queryBossTask() {
        List<Task> bossTasks = taskService.createTaskQuery().taskCandidateGroup("b").list();
        for (Task task : bossTasks) {
            Map<String, Object> variables = taskService.getVariables(task.getId());
            System.out.println("请假天数:" + variables.get("day") + ", 申请人:" + variables.get("studentUser"));
        }
    }

    // 老板审批(通过)
    @Test
    void bossApproval() {
        List<Task> bossTasks = taskService.createTaskQuery().taskCandidateGroup("b").list();
        Map<String, Object> approvalResult = new HashMap<>();
        approvalResult.put("outcome", "通过");
        for (Task task : bossTasks) {
            taskService.complete(task.getId(), approvalResult);
        }
    }

    // 查询流程历史活动
    @Test
    void queryHistory() {
        List<ProcessInstance> instances = runtimeService.createProcessInstanceQuery()
                .processDefinitionKey("leave")
                .orderByStartTime().desc().list();
        if (CollectionUtils.isEmpty(instances)) {
            System.out.println("暂无运行中的流程实例");
            return;
        }
        // 获取最近一个流程实例的历史活动
        List<HistoricActivityInstance> activities = historyService
                .createHistoricActivityInstanceQuery()
                .processInstanceId(instances.get(0).getId())
                .finished()
                .orderByHistoricActivityInstanceEndTime().desc()
                .list();
        activities.forEach(a -> System.out.println(
                "活动:" + a.getActivityName() + ", 耗时:" + a.getDurationInMillis() + "ms"));
    }

    // 完整流程测试:请假2天(无需老板审批)
    @Test
    void testShortLeave() {
        // 1. 员工申请
        Map<String, Object> vars = new HashMap<>();
        vars.put("day", 2);
        vars.put("studentUser", "小明");
        ProcessInstance pi = runtimeService.startProcessInstanceByKey("leave", vars);
        taskService.complete(taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult().getId());

        // 2. 领导审批
        Map<String, Object> outcome = new HashMap<>();
        outcome.put("outcome", "通过");
        taskService.createTaskQuery().taskCandidateGroup("a").list()
                .forEach(task -> taskService.complete(task.getId(), outcome));

        // 3. 查询历史(此时流程应已结束,因为天数<=2,直接走向结束)
        historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(pi.getId())
                .finished()
                .orderByHistoricActivityInstanceEndTime().asc()
                .list()
                .forEach(activity -> System.out.println(activity.getActivityName()));
    }
}
4.3 启动流程代码详解
  • 流程实例:部署后的流程定义相当于“蓝图”,runtimeService.startProcessInstanceByKey("leave", variables) 则根据此蓝图创建一个具体的流程实例。variables 中的参数(如 day, studentUser)会传递给流程。
  • 关键流程变量
    • ${studentUser}: 在“请假申请”节点中,通过 flowable:assignee="${studentUser}" 指定任务的办理人。
    • ${day}: 用于互斥网关判断请假天数。
    • ${outcome}: 用于审批节点的输出连线条件(“通过”或“驳回”)。
  • 任务查询与完成taskService 提供了丰富的API来查询任务(如按候选用户组查询),以及 complete 方法来提交任务,推动流程向下一个节点流转。
4.4 Flowable数据库表说明

Flowable引擎运行时需要数据库支持,所有表名以 ACT_ 开头,第二部分是两字符标识符,表明表的用途:

  • *`ACTRE** (REpository): 存储静态信息,如流程定义(ACT_RE_PROCDEF)、部署单元(ACT_RE_DEPLOYMENT`)。
  • *`ACTRU** (RUntime): 存储运行时数据,如执行实例(ACT_RU_EXECUTION)、任务(ACT_RU_TASK)、变量(ACT_RU_VARIABLE`)。流程实例结束后,这些表的相应记录会被删除,以保持表体积小、性能高。
  • *`ACTHI** (HIstory): 存储历史数据,如已完成的任务(ACT_HI_TASKINST)、流程实例(ACT_HI_PROCINST)、变量(ACT_HI_VARINST`)。
  • *`ACTID** (IDentity): 存储身份信息,如用户(ACT_ID_USER)、组(ACT_ID_GROUP)、关系(ACT_ID_MEMBERSHIP`)。
  • *`ACTGE** (General): 通用数据,如字节数组资源(ACT_GE_BYTEARRAY)、属性(ACT_GE_PROPERTY`)。

启动 flowable-ui 和业务应用后,连接对应的MySQL数据库,即可查看这些自动创建的表结构及其中存储的数据。




上一篇:Gogs任意文件写入漏洞CVE-2025-8110深度分析:符号链接绕过与RCE复现
下一篇:PHP代码审计实战:SEMCMS存储型XSS与SQL注入漏洞挖掘流程
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 17:17 , Processed in 0.231527 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表