Bootstrap

[JavaEE] 工作流- Activiti7 框架详解

目录

1、Activiti介绍

1.1、BPMN设计器

1.2、常见流程符号

1.2.1、事件event

1.2.2、活动activiti

1.2.3、流向flow

2、入门案例

2.1、需求说明

2.2、初始环境

2.2.1、添加依赖

2.2.2、添加配置

2.2.3、添加引导类

2.2.4、启动项目

2.2.5、表结构

2.2.6、常见api

2.3、绘制流程

2.4、部署流程

2.5、操作流程

2.5.1、启动流程

2.5.2、查询待办任务

2.5.3、填写申请单

2.5.4、经理同意

2.5.5、经理不同意

2.5.6、查询历史任务

3、任务分配

3.1、表达式分配

3.1.2、绘制流程

3.1.3、执行流程

3.1.4、备注说明

3.2、候选人

3.2.1、绘制流程

3.2.2、部署并启动流程

3.2.3、拾取任务

3.2.4、归还任务

3.3、候选人组

3.3.1、绘制流程

3.3.2、部署并启动流程

3.3.3、拾取任务

3.3.4、归还/交接任务

4、流程网关

4.1、排他网关

4.2、并行网关

4.3、包容网关

5、附录

5.1、集成安全框架

5.1.1、添加工具类

5.2.2、创建对象

5.2、业务id对接


1、Activiti介绍

目前业界流行的工作流技术有JBPM、Activiti、Flowable、Camunda,其中以Activiti占有率为最高

Activiti是一个开源的轻量级工作流引擎,2010年基于jBPM4实现首次开源。官网地址:https://www.activiti.org

Activiti可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN进行定义

BPMN是一种用于图形化表示和描述业务流程的标准化标记语言,目前主流的版本是2.0

1.1、BPMN设计器

虽然BPMN是一个标记语言,但是在实际中,我们很少直接去用它语法进行开发,而是直接使用流程设计器来画

在前端开源项目中有一个叫bpmn-js的开源项目,现在几乎成了画流程的标准,市面上的流程设计器基本都是基于它改造的

比如Activiti7官网提供的:https://github.com/activiti/activiti-modeling-app设计器,就是基于bpmn-js实现的

需要安装nodejs

使用npm run dev命令启动bpmn-js

运行完自动弹出如下界面

1.2、常见流程符号

1.2.1、事件event

事件是业务流程模型中的重要元素之一,事件可以发生在流程的任何阶段,并且可以影响流程的执行。分为以下几类:

  • 开始事件(Start Event):表示流程的起点,通常用于触发流程的启动

  • 结束事件(End Event):表示流程的结束点,通常用于触发流程的结束

1.2.2、活动activiti

任务(Task)是最基本的活动类型,表示一个简单的、可执行的工作单元。任务通常由人工执行,并且需要指定执行者

用户任务是由人工执行的,需要指定执行的用户或角色,并提供相应的输入

手动任务是由系统自动执行的,不需要指定执行的用户或角色

1.2.3、流向flow

流是连接两个流程节点的连线。常见的流向包含以下几种

2、入门案例

2.1、需求说明

我们通过请假流程审批这样一个流程来学习工作流

在企业中,员工如果有事需要请假,一般都需要向上级请假,得到批准后,方可离开公司。需求如下:

  • 员工:请假的员工需要先填写请假单,填写的字段有:请假人、请假天数、开始请假时间、请假事由

  • 经理:审批员工的请假单,如果不同意,则需要说明不同意的理由

实现步骤

1、搭建环境:使用SpringBoot集成Activiti,把初始化环境做出来

2、绘制流程:按照BPMN的规范,使用流程定义工具,用流程符号把整个流程描述出来

3、部署流程:把画好的流程定义文件,加载到数据库中,生成表的数据

4、操作流程:使用java代码来操作数据库表中的内容

2.2、初始环境

2.2.1、添加依赖

创建一个新的项目 activiti-demo,导入以下依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/>
    </parent>
​
    <dependencies>
        <!--安全框架 spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--springboot与activiti7整合的starter-->
        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-spring-boot-starter</artifactId>
            <version>7.10.0</version>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
​
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- 单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.22</version>
        </dependency>
    </dependencies>
​
    <!--如果activiti依赖下载不了,可以配置如下地址进行下载-->
    <repositories>
        <repository>
            <id>activiti-releases</id>
            <url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url>
        </repository>
    </repositories>

2.2.2、添加配置

配置文件application.yml

server:
  port: 8989
spring:
  datasource:
    username: root
    password: ******
    url: jdbc:mysql://localhost:3306/activiti-db?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC&nullCatalogMeansCurrent=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    #日志的相关配置
  activiti:
    # 记录所有历史数据
    history-level: full
    # 是否需要使用历史表,默认false不使用,而配置true是使用历史表
    db-history-used: true
    # 关闭流程自动部署,需要手动部署流程
    check-process-definitions: false
    # 如果部署过程遇到任何问题,服务不会失败
    deployment-mode: never-fail

注意:需要在自己的MySQL中创建一个新的数据库:activiti-db

2.2.3、添加引导类

准备一个引导类,然后启动项目,启动日志中,我们可以看到,activiti会自动创建表结构

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
​
@SpringBootApplication
public class ActivitiSampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ActivitiSampleApplication.class, args);
    }
}

2.2.4、启动项目

项目启动之后,观察日志信息,会自动创建出Activiti需要的25张数据表

刷新数据库,发现数据库中已经创建了25张表,目前说明Springboot已成功集成了activiti7

2.2.5、表结构

Activiti 的表都以ACT_ 开头。 第二部分是表示表的用途的两个字母标识。 用途也和服务的 API 对应。

  • ACT_GE :GE 表示 general, 通用数据

  • ACT_RE :RE表示 repository,这个前缀的表包含了流程定义信息

  • ACT_RU:RU表示 runtime,这些运行时的表,包含流程实例,任务,变量,异步任务等运行中的数据

  • ACT_HI:HI表示 history, 这些表包含历史数据,比如历史流程实例, 变量,任务等等

具体的表含义,如下

表分类表名解释
一般数据
[ACT_GE_BYTEARRAY]通用的流程定义和流程资源
[ACT_GE_PROPERTY]系统相关属性
流程历史记录
[ACT_HI_ACTINST]历史的活动实例
[ACT_HI_ATTACHMENT]历史的流程附件
[ACT_HI_COMMENT]历史的说明性信息
[ACT_HI_DETAIL]历史的流程运行中的细节信息
[ACT_HI_IDENTITYLINK]历史的流程运行过程中用户关系
[ACT_HI_PROCINST]历史的流程实例
[ACT_HI_TASKINST]历史的任务实例
[ACT_HI_VARINST]历史的流程运行中的变量信息
流程定义表
[ACT_RE_DEPLOYMENT]部署单元信息
[ACT_RE_MODEL]模型信息
[ACT_RE_PROCDEF]已部署的流程定义
运行实例表
[ACT_RU_EVENT_SUBSCR]运行时事件
[ACT_RU_EXECUTION]运行时流程执行实例
[ACT_RU_IDENTITYLINK]运行时用户关系信息,存储任务节点与参与者的相关信息
[ACT_RU_JOB]运行时作业
[ACT_RU_TASK]运行时任务
[ACT_RU_VARIABLE]运行时变量表

2.2.6、常见api

25张表对应MybatisPlus25个Mapper,同样25个Service...

而在activiti7框架内部,已经对25张表的数据操作,已经封装了对应的4个service

  • RepositoryService:用于部署流程定义,可以添加、删除、查询和管理流程定义

  • RuntimeService:用于启动、查询和管理正在运行的流程实例

  • TaskService:用于查询和管理当前用户可以操作的任务,以及完成任务

  • HistoryService:用于查询历史数据,例如已完成的流程实例、已删除的流程实例、用户任务等

因为我们现在使用的是springboot集成了activiti,这些api也被spring容器进行了管理,需要用到以上api的时候,直接注入即可,例如

@Autowired
private RepositoryService repositoryService;

2.3、绘制流程

我们打开bpmn-js,可以直接在页面中画图,步骤如下:

① 定义流程编号(ID)和名称

② 新增一个用户任务,并指定代理人为:张三

③ 新增一个用户任务,并指定代理人为:李四,同时需要结束这个流程,最后需要有一个结束事件

④ 流程图画好之后,在页面的左下角有一个导出,就可以直接导出为bpmn文件(xml文件)

⑤ 把生成后的bpmn文件改名拷贝到idea中备用,存储位置:resource/bpmn/qingjia.bpmn

因为保存的文件都是xml文件,我们为了方便查看这些流程,也可以截个图一起放入bpmn目录下

2.4、部署流程

部署流程就是将生成好流程文件bpmn保存到数据库中,此时需要用到RepositoryService对象

RepositoryService用于部署流程定义,可以添加、删除、查询和管理流程定义

import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
@SpringBootTest
public class ActTest {
    
    //此对象可以完成流程的部署任务
    @Autowired
    private RepositoryService repositoryService;
​
    //流程部署
    @Test
    public void deployProcess() {
        Deployment deployment = repositoryService.createDeployment()//创建流程部署
                .addClasspathResource("bpmn/qingjia.bpmn")//流程定义文件
                .addClasspathResource("bpmn/qingjia.png")//流程定义图片
                .name("qingjia")//指定流程名称
                .deploy();//开始部署
​
        //打印流程部署结果
        System.out.println("流程部署的id:" + deployment.getId());
        System.out.println("流程部署的名称:" + deployment.getName());
    }
}

注意: 图中两个地方需保持一致

上面就进行了一次流程的部署,这个过程中涉及到的数据表有:

  • act_re_deployment:流程部署,记录每次工作流的部署信息,包括部署名称和时间等

  • act_ge_bytearray:流程资源表,系统会将流程定义的两个文件保存到这张表中

  • act_re_procdef:流程定义表,记录每个流程定义的信息,包括流程定义的名称、版本号、部署ID等

注意:act_re_deployment和act_re_procdef一对多关系,一次部署在流程部署表生成一条记录,但一次部署可以部署多个流程定义

 

但是在一般情况下:一次部署一个流程,这样部署表和流程定义表是一对一有关系,方便读取流程部署及流程定义信息

    //查询流程部署信息
    @Test
    public void test2() {
        List<Deployment> list = repositoryService.createDeploymentQuery()//创建流程部署查询
                .deploymentNameLike("qingjia")//设置查询条件
                .list();//查询所有部署的流程
        //打印流程部署结果
        for (Deployment deployment : list) {
            System.out.println("流程部署的名称:" + deployment.getName());
        }
    }
​
    //查询部流程定义
    @Test
    public void test3() {
        //根据name查询流程定义的Key查询
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
                .processDefinitionKeyLike("qingjia")
                .list();
        for (ProcessDefinition processDefinition : list) {
            System.out.println(processDefinition.getId() + "==" + processDefinition.getName());
        }
    }
    

2.5、操作流程

2.5.1、启动流程

流程定义好了之后,相当于定义了一个模板,公司有很多人,每个人都可以按照这个模板填写自己的申请单,每个申请单就相当于是一个流程实例

流程实例的操作要使用RuntimeService来处理,它用于启动、查询和管理正在运行的流程实例

@Autowired
    private RuntimeService runtimeService;
​
    //启动流程实例
    @Test
    public void test4() {
        //根据流程定义key启动流程实例
        ProcessInstance processInstance =
                runtimeService.startProcessInstanceByKey("qingjia");
​
        System.out.println("流程实例Id: " + processInstance.getId());//流程实例id
        System.out.println("流程定义Id: " + processInstance.getProcessDefinitionId());//流程定义id
    }

流程实例启动之后,也会操作相关的表结构

  • act_ru_task(运行时任务表):插入一条新的任务记录,表示流程实例的启动任务

  • act_ru_execution表(运行时流程实例表):插入一条新的流程实例的执行信息,包括流程实例ID、流程定义ID等

  • act_ru_identitylink (运行时身份关联表):插入一条新的身份关联记录,表示流程实例的启动任务与相关用户的关系

 

2.5.2、查询待办任务

流程启动起来之后,我们不同的人需要对流程进行不同的操作,主要有查询待办、填写申请单、审评、查询历史任务等等

这些操作都是通过TaskService来完成的,它主要用于查询和管理当前用户可以操作的任务,以及完成任务

当流程实例创建之后,就会分配给不同的人来去执行流程中的任务,每个操作人都可以进行查询我的待办任务

根据流程图的中的定义,第一个填写请假单是由张三负责的,我们就可以先查询张三的任务,主要就是从act_ru_task表中进行查询

    //任务1--查询张三的待办任务
    @Test
    public void test5() {
        List<Task> taskList = taskService.createTaskQuery()//任务查询条件
                .processDefinitionKey("qingjia")//流程名称
                .taskAssignee("张三")//操作人名称
                .list();
        for (Task task : taskList) {
            System.out.println("流程实例id:" + task.getProcessInstanceId());
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
            System.out.println("任务负责人:" + task.getAssignee());
        }
    }

2.5.3、填写申请单

现在流程中的节点已经走到了张三这里,需要他进行处理,因为目前是请假申请单,所以他需要填写请假单的内容,并且提交数据,继续往下执行流程

调用complete方法,即可结束当前节点,并且流程会自动开启下一个节点的任务。

    //任务2--张三执行任务,完成申请单的编写和提交
    @Test
    public void test6() {
        //根据流程key 和 任务的负责人 查询任务,返回一个任务对象
        List<Task> list = taskService.createTaskQuery()
                .processDefinitionKey("qingjia") //流程Key
                .taskAssignee("张三")  //要查询的负责人
                .list();
​
        //执行任务
        if (CollectionUtil.isNotEmpty(list)) {
            for (Task task : list) {
                Map<String, Object> variables = new HashMap<>();
                variables.put("userName", "张三");
                variables.put("startDate", "2024-01-01");
                variables.put("days", 1);
                variables.put("reason", "元旦回家探亲");
​
                //完成任务
                taskService.complete(task.getId(), variables);
                System.out.println("任务完成...");
            }
        }
    }

张三完成自己的任务后,相关表的变化

test6()运行前的任务表如下:

test6()运行后的任务表如下:

test6()运行前后 act_ru_execution流程实例表不变

 test6()运行后act_hi_taskinst历史表发生改变:

  • act_ru_execution表中发现,新增了【经理审批】待执行的节点,而完成的【填写请假单】节点被删除了

  • act_ru_task表中,待办任务也变成了李四【经理审批】

  • act_ru_variable表中会存储代码中传入的表单数据

 注意:李四为绘图中的代理人,绘图的时候忘了加上

2.5.4、经理同意

经理同意这个操作其实就相当于完成当前节点,进入下一节点,因此处理方案跟上个流程类型

    //任务3--经理执行任务,同意申请
    @Test
    public void test7() {
        //查询到李四的当前任务
        Task task = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("李四")
                .singleResult();
​
        //执行任务
        if (null != task) {
            Map<String, Object> variables = new HashMap<>();
            variables.put("approvalStatus", "同意");
            variables.put("approvalNote", "123");
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }

上述代码执行成功后,到数据库中查看act_ru_execution、act_ru_task、act_ru_variable中的数据,你会发现里面没有了相关流程实例的数据;

那是因为【经理审批】节点执行完成之后,后面的结束节点也会自动执行,因此流程就执行完成了。

而act_ru_开头的表只存储运行中的流程信息,不会存储流程结束了的信息,结束了的相关信息都转到act_hi开头的表中存储了

  • act_hi_procinst:历史的流程实例

  • act_hi_actinst:历史的活动实例

  • act_hi_taskinst:历史的任务实例

  • act_hi_identitylink:历史流程用户关系

  • act_hi_varinst:历史流程运行中的变量信息

2.5.5、经理不同意

经理不同意则流程会终止执行。比如下面流程中,如果经理1审批不同意,那么经理2就不用审批了,整个流程就应该直接结束

因此不同意,则应该是终止流程而不是完成节点,在删除流程时,同时也把审批不同意及理由,作为流程变量存储到流程变量中

    //任务3--经理执行任务,不同意申请
    @Test
    public void test8() {
        Task task = taskService.createTaskQuery()
                .processDefinitionKey("qingjia") //流程Key
                .taskAssignee("李四")  //要查询的负责人
                .singleResult();
​
        if (null != task) {
            Map<String, Object> variables = new HashMap<>();
            variables.put("approvalStatus", "不同意");
            variables.put("approvalNode", "时间太久,不同意");
​
            //记录流程变量
            runtimeService.setVariables(task.getProcessInstanceId(), variables);
            //添加流程变量,删除流程实例,表示任务被拒绝
            runtimeService.deleteProcessInstance(task.getProcessInstanceId(), "时间太久,不同意");
        }
    }

上述代码执行成功后,同样在数据库中act_ru_execution、act_ru_task、act_ru_variable中的看不到数据

因为整个流程都被删除了,也结束了,信息也被转到了act_hi开头的表中

2.5.6、查询历史任务

历史任务的查询需要使用HistoryService完成,主要就是根据各种条件从前面讲过的一堆历史表中查询数据

    @Autowired
    private HistoryService historyService;
​
    @Test
    public void test9(){
        HistoricTaskInstanceQuery instanceQuery = historyService.createHistoricTaskInstanceQuery()
                .includeProcessVariables()//包含流程变量(配合下面使用)
                .orderByHistoricTaskInstanceEndTime().desc()//按历史任务实例结束时间排序
                .finished()//只查询已完成的任务
                .taskAssignee("张三");//根据执行人查询
​
        //自定义流程变量  条件查询
        //instanceQuery.processVariableValueGreaterThan("days", "1");
​
        //查询历史流程
        List<HistoricTaskInstance> list = instanceQuery.list();
        for (HistoricTaskInstance history : list) {
            System.out.println("Id: " + history.getId());
            System.out.println("ProcessInstanceId: " + history.getProcessInstanceId());
            System.out.println("StartTime: " + history.getStartTime());
            System.out.println("Name: " + history.getName());
            Map<String, Object> processVariables = history.getProcessVariables();
            System.out.println(processVariables.get("days").toString());
            System.out.println(processVariables.get("reason").toString());
            System.out.println("=======================================");
        }
    }

查询条件API说明

方法名称
processInstanceBusinessKey(String processInstanceBusinessKey)根据流程实例业务Key查询
taskId(String taskId)根据任务ID查询
taskAssignee(String taskAssignee) | taskAssigneeLike(String taskAssignee)根据执行人查询
finished()已完成的(申请过、同意过)
unfinished()未完成任务
orderByHistoricTaskInstanceEndTime().desc()按照执行时间排序
taskName(String var1) | taskNameLike(String var1)根据节点任务名称查询
list()返回分页数据
includeProcessVariables()包含流程变量(配合下面使用)
processVariableValueEquals(String variableName, Object variableValue)两个值相等
processVariableValueNotEquals(String variableName, Object variableValue)两个值不相等
processVariableValueGreaterThan(String name, Object value)大于
processVariableValueLessThan(String name, Object value)小于

3、任务分配

上一章的案例中,在指派用户任务的执行人时,使用的都是直接指派给固定账号,这样流程设计审批的灵活性就很差

因此,Activiti提供了各种不同的分配方式,这章我们就来详细研究下其它任务分配方式,主要有:表达式分配、监听器分配

3.1、表达式分配

值表达式就是使用UEL表达式(一种占位符)来替换具体的分配人,在使用的时候只需要对表达式中的变量进行赋值即可

UEL表达式,是一种用于在流程定义中评估和计算表达式的语言。可以用来做流程条件判断、变量赋值等

  • 定界符:${assignee} | ${user.assignee}

  • 数学运算:${ 5+5 }

  • 逻辑判断:${amount > 100}

  • 方法调用:${ list('参数') },调用的是list方法,这个方法是Activiti内置的方法,把参数转成一个集合

3.1.2、绘制流程

重新绘制前面的流程,但是在代理人的位置不再直接写死为张三、李四,而是使用${assingee0}、${assingee1}来代替

3.1.3、执行流程

为了观察数据更方便,可以将目前库中的所有表都删除,然后让其重建

import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.history.HistoricTaskInstanceQuery;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest1 {
    @Autowired
    private RepositoryService repositoryService;
​
    @Autowired
    private RuntimeService runtimeService;
​
    @Autowired
    private TaskService taskService;
​
    @Autowired
    private HistoryService historyService;
​
    //流程部署
    @Test
    public void test1() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("bpmn/qingjia1.bpmn")
                .name("qingjia")
                .deploy();
​
        System.out.println("流程部署的id:" + deployment.getId());
    }
​
    //启动流程实例
    @Test
    public void test2() {
        //此时在启动流程实例之前,必须要给流程中定义的变量赋值
        Map<String, Object> variables = new HashMap<>();
        variables.put("assingee0", "张三");
        variables.put("assingee1", "李四");
​
        ProcessInstance processInstance =
                runtimeService.startProcessInstanceByKey("qingjia", variables);
​
        System.out.println("流程实例Id: " + processInstance.getId());
    }
​
    //查询张三的待办任务并完成
    @Test
    public void test3() {
        //主要就是从act_ru_task表中进行查询
        List<Task> taskList = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("张三")
                .list();
​
​
        for (Task task : taskList) {
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
​
            //请假原因,根据业务自由设置
            Map<String, Object> variables = new HashMap<>();
            variables.put("userName", "张三");
            variables.put("startDate", "2024-01-01");
            variables.put("days", "1");
            variables.put("reason", "元旦回家探亲");
​
            //完成任务
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
​
    //查询李四的待办任务并完成,同意申请
    @Test
    public void test4() {
        //查询到李四的当前任务
        List<Task> list = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("李四")
                .list();
​
        //执行任务
        for (Task task : list) {
            Map<String, Object> variables = new HashMap<>();
            variables.put("approvalStatus", "同意");
            variables.put("approvalNote", "123");
​
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
}

3.1.4、备注说明

刚才的例子只是UEL表达式中最基本的设置变量的方式,UEL还支持其他很多类型

  1. 使用pojo对象赋值

  2. spring对象的方法

我们上面使用了变量来设置代理人,在activiti中这称为流程变量

  • 流程变量的默认作用域是流程实例,当一个流程变量的作用域为流程实例时,可以称为global变量

  • 除了global变量外,activiti还支持local变量,这种变量仅仅是针对一个任务,随着一个任务的结束会被删除

3.2、候选人

在前面的流程定义中在任务结点的assignee都是设置了一个负责人,但是在企业中,每个节点上都可能有多个负责人

下面我们就需要使用候选人或者候选人组做为身份标识替换掉前面的单个参与者来完成任务

一个审批节点可能有多个人同时具有审批的权限,这时我们就可以通过候选人来处理。

3.2.1、绘制流程

这次绘制流程时,对于经理审批,不再设置代理人,而是设置候选人,多个候选人是,分隔

3.2.2、部署并启动流程

import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest3 {
    @Autowired
    private RepositoryService repositoryService;
​
    @Autowired
    private RuntimeService runtimeService;
​
    @Autowired
    private TaskService taskService;
​
​
    //流程部署
    @Test
    public void test1() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("bpmn/qingjia3.bpmn")
                .name("qingjia")
                .deploy();
​
        System.out.println("流程部署的id:" + deployment.getId());
    }
​
    //启动流程实例
    @Test
    public void test2() {
        //此时在启动流程实例之前,必须要给流程中定义的变量赋值
        Map<String, Object> variables = new HashMap<>();
        variables.put("a1", "张三");
        variables.put("c1", "赵经理");
        variables.put("c2", "钱经理");
        variables.put("c3", "孙经理");
​
        ProcessInstance processInstance =
                runtimeService.startProcessInstanceByKey("qingjia",variables);
​
        System.out.println("流程实例Id: " + processInstance.getId());
    }
    
    //查询张三的待办任务并完成
    @Test
    public void test3() {
        //主要就是从act_ru_task表中进行查询
        List<Task> taskList = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("张三")
                .list();
​
​
        for (Task task : taskList) {
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
​
            //请假原因,根据业务自由设置
            Map<String, Object> variables = new HashMap<>();
            variables.put("userName", "张三");
            variables.put("startDate", "2024-01-01");
            variables.put("days", "1");
            variables.put("reason", "元旦回家探亲");
​
            //完成任务
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
}

流程启动后任务,在act_ru_task表中的审批人是空的,但是act_ru_identitylink表保存了候选人信息

3.2.3、拾取任务

拾取任务的目的是将候选人提升为审批人

    //孙经理作为候选人进行查询,并拾取任务
    @Test
    public void test4() {
        //模拟登录,防止UsernameNotFoundException错误
        new SecurityUtil().logInAs("孙经理");
​
        //根据候选人查询任务
        List<Task> list = taskService.createTaskQuery()
                .taskCandidateUser("孙经理") // 根据候选人查询审批任务
                .list();
     
        //任务拾取: 将指定用户从候选人提升为审批人
        for (Task task : list) {
            taskService.claim(task.getId(), "孙经理");
        }
    }

3.2.4、归还任务

拾取任务后如果不想操作那么可以归还任务

    //归还:拾取的用户不审批了。就放弃审批人的操作
    @Test
    public void test5() {
        //模拟登录,防止UsernameNotFoundException错误
        new SecurityUtil().logInAs("孙经理");
​
        List<Task> list = taskService.createTaskQuery()
                .taskCandidateOrAssigned("孙经理") // 根据 审批人或者候选人 来查询待办任务
                .list();
        for (Task task : list) {
            // 归还操作的本质其实就是设置审批人为空
            taskService.unclaim(task.getId());
        }
    }

3.3、候选人组

当候选人很多的情况下,我们可以分组来处理。也就是先创建组,然后把用户分配到这个组中,整组人就都成了候选人

3.3.1、绘制流程

3.3.2、部署并启动流程

import com.itheima.util.SecurityUtil;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest4 {
    @Autowired
    private RepositoryService repositoryService;
​
    @Autowired
    private RuntimeService runtimeService;
​
    @Autowired
    private TaskService taskService;
​
    //流程部署
    @Test
    public void test1() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("bpmn/qingjia4.bpmn")
                .name("qingjia")
                .deploy();
​
        System.out.println("流程部署的id:" + deployment.getId());
    }
​
    //启动流程实例
    @Test
    public void test2() {
        ProcessInstance processInstance =
                runtimeService.startProcessInstanceByKey("qingjia");
        System.out.println("流程实例Id: " + processInstance.getId());
    }
​
    //查询张三的待办任务并完成
    @Test
    public void test3() {
        //主要就是从act_ru_task表中进行查询
        List<Task> taskList = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("张三")
                .list();
​
        for (Task task : taskList) {
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
​
            //请假原因,根据业务自由设置
            Map<String, Object> variables = new HashMap<>();
            variables.put("userName", "张三");
            variables.put("startDate", "2024-01-01");
            variables.put("days", "1");
            variables.put("reason", "元旦回家探亲");
​
            //完成任务
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
}

流程启动后任务,在act_ru_task表中的审批人是空的,但是act_ru_identitylink表保存了候选人信息

3.3.3、拾取任务

拾取任务的目的是将候选人提升为审批人

    //查询经理部门的任务
    @Test
    public void test4() {
        //模拟登录,防止UsernameNotFoundException错误
        //new SecurityUtil().logInAs("孙经理");
​
        //根据候选人查询任务
        List<Task> list = taskService.createTaskQuery()
                .taskCandidateGroup("manageGroup") // 查询经理部门的任务
                .list();
​
        //任务拾取: 将指定用户从候选人提升为审批人
        for (Task task : list) {
            taskService.claim(task.getId(), "孙经理");
        }
    }

3.3.4、归还/交接任务

拾取任务后如果不想操作那么可以归还任务,也可以将任务交接给其他用户

    //任务归还和交接
    @Test
    public void test5() {
        //模拟登录,防止UsernameNotFoundException错误
        new SecurityUtil().logInAs("孙经理");
​
        List<Task> list = taskService.createTaskQuery()
                .taskCandidateOrAssigned("孙经理") // 根据 审批人或者 候选人来查询待办任务
                .list();
        for (Task task : list) {
            // 归还操作的本质其实就是设置审批人为空
            // taskService.unclaim(task.getId());
​
            //任务交接
            taskService.setAssignee(task.getId(), "刘经理");
        }
    }

4、流程网关

网关用于控制流程的执行流向,它的作用是在流程执行时进行决策,决定流程的下一个执行步骤。Activiti7中,有以下几种类型的网关:

  1. 排他网关:用于在流程中进行条件判断,根据不同的条件选择不同的分支路径,只有满足条件的分支会被执行,其他分支会被忽略

  2. 并行网关:用于将流程分成多个并行的分支,这些分支可以同时执行,当所有分支都执行完毕后,流程会继续向下执行

  3. 包容网关:用于根据多个条件的组合情况选择分支路径,可以选择满足任意一个条件的分支执行,或者选择满足所有条件的分支执行

4.1、排他网关

排他网关用于对流程中分支进行决策,当执行到达这个网关时,会按照所有出口顺序流定义的顺序对它们进行计算,选择第一个条件为true的顺序流继续流程

import com.itheima.util.SecurityUtil;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest5 {
    @Autowired
    private RepositoryService repositoryService;
​
    @Autowired
    private RuntimeService runtimeService;
​
    @Autowired
    private TaskService taskService;
​
    //流程部署
    @Test
    public void test1() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("bpmn/gateway-paita.bpmn")
                .name("qingjia")
                .deploy();
        System.out.println("流程部署的id:" + deployment.getId());
    }
​
    //启动流程实例
    @Test
    public void test2() {
        ProcessInstance processInstance =
                runtimeService.startProcessInstanceByKey("qingjia");
        System.out.println("流程实例Id: " + processInstance.getId());
    }
​
    //查询张三的待办任务并完成
    @Test
    public void test3() {
        //主要就是从act_ru_task表中进行查询
        List<Task> taskList = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("张三")
                .list();
​
        for (Task task : taskList) {
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
​
            //请假原因,根据业务自由设置
            Map<String, Object> variables = new HashMap<>();
            variables.put("userName", "张三");
            variables.put("startDate", "2024-01-01");
            variables.put("days", "10");//此处的天数决定了下一步会流转到哪个审批人手中
            variables.put("reason", "元旦回家探亲");
​
            //完成任务
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
}

4.2、并行网关

并行网关用于将流程分成多个并行的分支,这些分支可以同时执行,当所有分支都执行完毕后,流程会继续向下执行

  • fork分支:并行后的所有外出顺序流,为每个顺序流都创建一个并发分支

  • join汇聚: 所有到达并行网关,在此等待的进入分支,直到所有进入顺序流的分支都到达以后, 流程就会通过网关

import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest6 {
    @Autowired
    private RepositoryService repositoryService;
​
    @Autowired
    private RuntimeService runtimeService;
​
    @Autowired
    private TaskService taskService;
​
    //流程部署
    @Test
    public void test1() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("bpmn/gateway-bingxing.bpmn")
                .name("qingjia")
                .deploy();
        System.out.println("流程部署的id:" + deployment.getId());
    }
​
    //启动流程实例
    @Test
    public void test2() {
        ProcessInstance processInstance =
                runtimeService.startProcessInstanceByKey("qingjia");
        System.out.println("流程实例Id: " + processInstance.getId());
    }
​
    //查询张三的待办任务并完成
    @Test
    public void test3() {
        //主要就是从act_ru_task表中进行查询
        List<Task> taskList = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("张三")
                .list();
​
        for (Task task : taskList) {
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
​
            //请假原因,根据业务自由设置
            Map<String, Object> variables = new HashMap<>();
            variables.put("userName", "张三");
            variables.put("startDate", "2024-01-01");
            variables.put("days", "10");//此处的天数决定了下一步会流转到哪个审批人手中
            variables.put("reason", "元旦回家探亲");
​
            //完成任务
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
​
    //查询技术经理(李四)的任务,并处理
    @Test
    public void test4() {
        //查询到李四的当前任务
        List<Task> list = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("李四")
                .list();
​
        //执行任务
        for (Task task : list) {
            Map<String, Object> variables = new HashMap<>();
            variables.put("approvalStatus", "同意");
            variables.put("approvalNote", "123");
​
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
​
    //查询人事经理(王五)的任务,并处理
    @Test
    public void test5() {
        //查询到李四的当前任务
        List<Task> list = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("王五")
                .list();
​
        //执行任务
        for (Task task : list) {
            Map<String, Object> variables = new HashMap<>();
            variables.put("approvalStatus", "同意");
            variables.put("approvalNote", "123");
​
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
​
    //查询总经理(赵六)的任务,并处理
    @Test
    public void test6() {
        //查询到李四的当前任务
        List<Task> list = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("赵六")
                .list();
​
        //执行任务
        for (Task task : list) {
            Map<String, Object> variables = new HashMap<>();
            variables.put("approvalStatus", "同意");
            variables.put("approvalNote", "123");
​
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
}

在并行网关中我们需要注意的是执行实例的概念

  • 主流程实例:流程启动就会维护的一条实例, 在ACT_RU_EXECUTION表中parent_id_为null

  • 子流程实例:流程的每一步操作都会更新子流程实例,表示当前流程的执行进度

4.3、包容网关

包含网关用于根据多个条件的组合情况选择分支路径,可以选择满足任意一个条件的分支执行(有条件必须执行,无条件的必须执行)

import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest7 {
    @Autowired
    private RepositoryService repositoryService;
​
    @Autowired
    private RuntimeService runtimeService;
​
    @Autowired
    private TaskService taskService;
​
    //流程部署
    @Test
    public void test1() {
        Deployment deployment = repositoryService.createDeployment()
                .addClasspathResource("bpmn/gateway-baorong.bpmn")
                .name("qingjia")
                .deploy();
        System.out.println("流程部署的id:" + deployment.getId());
    }
​
    //启动流程实例
    @Test
    public void test2() {
        ProcessInstance processInstance =
                runtimeService.startProcessInstanceByKey("qingjia");
        System.out.println("流程实例Id: " + processInstance.getId());
    }
​
    //查询张三的待办任务并完成
    @Test
    public void test3() {
        //主要就是从act_ru_task表中进行查询
        List<Task> taskList = taskService.createTaskQuery()
                .processDefinitionKey("qingjia")
                .taskAssignee("张三")
                .list();
​
        for (Task task : taskList) {
            System.out.println("任务id:" + task.getId());
            System.out.println("任务名称:" + task.getName());
​
            //请假原因,根据业务自由设置
            Map<String, Object> variables = new HashMap<>();
            variables.put("userName", "张三");
            variables.put("startDate", "2024-01-01");
            variables.put("days", "10");//此处的天数决定了下一步会流转到哪个审批人手中
            variables.put("reason", "元旦回家探亲");
​
            //完成任务
            taskService.complete(task.getId(), variables);
            System.out.println("任务完成...");
        }
    }
​
    //完成后发现任务流转到人事经理审批 和 部门经理审批
}

5、附录

5.1、集成安全框架

由于Activiti7整合了SpringSecurity框架,在拾取候选人的时候,需要验证身份信息,否则机会报错:UsernameNotFoundException

我们可以临时模拟一个登录

5.1.1、添加工具类

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.stereotype.Component;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
 
@Component
public class SecurityUtil {
 
    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
 
    private static InMemoryUserDetailsManager inMemoryUserDetailsManager;
 
    //查询实例
    public static InMemoryUserDetailsManager findInstance(){
        if(inMemoryUserDetailsManager==null){
            inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        }
        return inMemoryUserDetailsManager;
    }
 
    //createUser() 方法用于创建一个新的用户,如果该用户已经存在则先删除再创建。新创建的用户需要设置两个角色:"ROLE_ACTIVITI_USER" 和 "GROUP_activitiTeam"
    public void createUser(String userName) {
        inMemoryUserDetailsManager = findInstance();
        if(inMemoryUserDetailsManager.userExists(userName)) {
            inMemoryUserDetailsManager.deleteUser(userName);
        }
        //SimpleGrantedAuthority 是一个自定义的权限类,用于设置角色的名称。
        List<SimpleGrantedAuthority> roles = new ArrayList<SimpleGrantedAuthority>(){{
           
            add(new SimpleGrantedAuthority("ROLE_ACTIVITI_USER"));
            add(new SimpleGrantedAuthority("GROUP_activitiTeam"));
        }};
        //passwordEncoder() 方法用于设置密码加密方式为 BCryptPasswordEncoder
        inMemoryUserDetailsManager.createUser(new User(userName, passwordEncoder().encode("password"),roles));
     }
 
     //logInAs() 方法用于以指定用户名登录系统,并获取当前用户的用户名和权限信息
    public void logInAs(String username) {
        createUser(username);
        UserDetails user = findInstance().loadUserByUsername(username);
        if (user == null) {
            throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
        }
        logger.info("> Logged in as: " + username);
        //SecurityContextHolder 是一个 Spring Security 提供的上下文处理器,用于在请求处理过程中维护用户的认证状态和权限信息
        SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return user.getAuthorities();
            }
 
            @Override
            public Object getCredentials() {
                return user.getPassword();
            }
 
            @Override
            public Object getDetails() {
                return user;
            }
 
            @Override
            public Object getPrincipal() {
                return user;
            }
 
            @Override
            public boolean isAuthenticated() {
                return true;
            }
 
            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
 
            }
 
            @Override
            public String getName() {
                return user.getUsername();
            }
        }));
 
        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
    }
 
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5.2.2、创建对象

在启动类中添加下面代码

    @Bean
    public UserDetailsService myUserDetailsService() {
        return SecurityUtil.findInstance();
    }

5.2、业务id对接

目前我们已经基本完成了activiti的学习,我们发现目前的工作流其实是脱离我们的实际业务存在的

如果想将activiti与实际业务联系起来,需要用到它提供的一个字段:buinessId,这个字段用来记录业务表的主键

我们可以在启动流程的时候设置

ProcessInstance startProcessInstanceByKey(String processDefinitionKey, String businessKey, Map<String, Object> variables);

;