Activiti

http://www.activiti.org/

Activiti是一个工作流引擎,可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由activiti进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统维护开发的成本。

Activiti的基本概念与使用

Activiti负责流程的自动化控制,他通过25张数据库表存储所有流程的需要的数据,修改这些数据,来实现流程的自动化运行。为了方便实现流程操作,Activiti提供了多个Service类来实现业务控制,从而对数据库进行操作。

Activiti的使用大致分为四个步骤:

  1. 使用BPMN绘制出流程定义

  2. 部署流程定义

  3. 使用流程定义创建流程实例

  4. 操作流程实例中的各个task

定义入下申请单的BPMN20.xml流程:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
  <process id="leave" name="请假单" isExecutable="true">
    <startEvent id="startevent1" name="Start"/>
    <userTask id="usertask1" name="创建请假单" activiti:assignee="zhangsan"/>
    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"/>
    <userTask id="usertask2" name="部门经理审核" activiti:assignee="lisi"/>
    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"/>
    <userTask id="usertask3" name="人事复核" activiti:assignee="wangwu"/>
    <sequenceFlow id="flow3" sourceRef="usertask2" targetRef="usertask3"/>
    <endEvent id="endevent1" name="End"/>
    <sequenceFlow id="flow4" sourceRef="usertask3" targetRef="endevent1"/>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_myProcess">
    <bpmndi:BPMNPlane bpmnElement="myProcess" id="BPMNPlane_myProcess">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="130.0" y="160.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="210.0" y="150.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
        <omgdc:Bounds height="55.0" width="105.0" x="360.0" y="150.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
        <omgdc:Bounds height="55.0" width="105.0" x="510.0" y="150.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="660.0" y="160.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="165.0" y="177.0"/>
        <omgdi:waypoint x="210.0" y="177.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
        <omgdi:waypoint x="315.0" y="177.0"/>
        <omgdi:waypoint x="360.0" y="177.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="465.0" y="177.0"/>
        <omgdi:waypoint x="510.0" y="177.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="615.0" y="177.0"/>
        <omgdi:waypoint x="660.0" y="177.0"/>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

部署Activiti,部署流程定义,并使用流程定义开启一个流程实例:

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 创建部署一个请假申请单流程
RepositoryService repositoryService = engine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
        .name("请假申请单")
        .addClasspathResource("bpmn/leave.bpmn20.xml")
        .key(DEPLOY_KEY)
        .deploy();
Assertions.assertNotNull(deployment);

// 查询创建的流程
deployment = repositoryService.createDeploymentQuery()
        .deploymentId(deployment.getId())
        // 只查询一个结果,如果有两个,会报错,类似mybatis的selectOne方法
        .singleResult();
Assertions.assertNotNull(deployment);
System.out.println("部署信息: " + deployment);

// 使用部署的流程,发起一个流程实例
RuntimeService runtimeService = engine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(DEPLOY_KEY);
Assertions.assertNotNull(processInstance);

// 查询发起的流程实例
processInstance = runtimeService.createProcessInstanceQuery()
        .deploymentId(deployment.getId()) // 根据部署id查询他相关的实例
        .singleResult();
Assertions.assertNotNull(processInstance);
System.out.println("流程实例信息:" + processInstance);

// 查询该流程实例要完成的所有任务
List<Task> taskList = engine.getTaskService().createTaskQuery()
        .processInstanceId(processInstance.getId())
        .list();
Assertions.assertFalse(taskList.isEmpty());

// 最开始系应该有一个任务,zhangsan 创建请假单
Assertions.assertEquals(taskList.get(0).getName(), "创建请假单");

// 查询zhangsan下的任务
Task zhangsanTask = engine.getTaskService().createTaskQuery()
        .taskCandidateOrAssigned("zhangsan")
        .processInstanceId(processInstance.getId())
        .singleResult();
Assertions.assertNotNull(zhangsanTask);
Assertions.assertEquals(zhangsanTask.getName(), "创建请假单");

// zhangsan完成任务,下一个就是lisi,部门经理审核
engine.getTaskService().complete(zhangsanTask.getId());

Task lisiTask = engine.getTaskService().createTaskQuery()
        .taskCandidateOrAssigned("lisi")
        .processInstanceId(processInstance.getId())
        .singleResult();
engine.getTaskService().complete(lisiTask.getId());
Assertions.assertNotNull(lisiTask);
Assertions.assertEquals(lisiTask.getName(), "部门经理审核");

Task wangwuTask = engine.getTaskService().createTaskQuery()
        .taskCandidateOrAssigned("wangwu")
        .processInstanceId(processInstance.getId())
        .singleResult();
engine.getTaskService().complete(wangwuTask.getId());
Assertions.assertNotNull(wangwuTask);
Assertions.assertEquals(wangwuTask.getName(), "人事复核");

// wangwu复核之后,该流程应该就结束了,进入到了历史表中
HistoricProcessInstance historicProcessInstance = engine.getHistoryService().createHistoricProcessInstanceQuery().processInstanceId(processInstance.getId()).singleResult();
Assertions.assertNotNull(historicProcessInstance);
Assertions.assertEquals(historicProcessInstance.getProcessDefinitionName(), "请假单");
Assertions.assertEquals(historicProcessInstance.getProcessDefinitionKey(), "leave");

// 删除流程以及关联的流程实例,参数2会级联删除关联的其他数据
repositoryService.deleteDeployment(deployment.getId(), true);

部署Activiti

Activiti支持的数据库

Activiti在运行时需要数据库支持,目前共支持:

  • H2 默认

  • Mysql

  • Oracle

  • DB2

  • Postgres

  • MsSql

使用MySQL生成Activiti库

创建Activiti的数据库:

CREATE DATABASE activiti DEFAULT CHARACTER SET utf8;

创建用于生成表的Maven工程,并添加如下的依赖:

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-engine</artifactId>
    <version>${activiti.version}</version>
</dependency>
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring</artifactId>
    <version>${activiti.version}</version>
</dependency>
<!--BPMN模型处理-->
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-bpmn-model</artifactId>
    <version>${activiti.version}</version>
</dependency>
<!--BPMN转换-->
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-bpmn-converter</artifactId>
    <version>${activiti.version}</version>
</dependency>
<!--BPMN布局-->
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-bpmn-layout</artifactId>
    <version>${activiti.version}</version>
    <exclusions>
        <exclusion>
            <groupId>com.github.jgraph</groupId>
            <artifactId>jgraphx</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--JSON数据转换-->
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-json-converter</artifactId>
    <version>${activiti.version}</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.13</version>
</dependency>

<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>${log4j.version}</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${slf4j.version}</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${slf4j.version}</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

创建activiti.cfg.xml,指定数据库配置:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--使用默认方式初始化数据库,必须配置一个名称为processEngineConfiguration的Bean-->
    <bean id="processEngineConfiguration"
          class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<!--        <property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>-->
<!--        <property name="jdbcUrl" value="jdbc:mysql:///activiti?nullCatalogMeansCurrent=true"/>-->
<!--        <property name="jdbcUsername" value="activiti"/>-->
<!--        <property name="jdbcPassword" value="activiti"/>-->
        <!--引用连接池-->
        <property name="dataSource" ref="dataSource"/>
        <!--生成数据库时的策略-->
        <!--如果已经存在表,就会直接使用,否则会创建-->
        <property name="databaseSchemaUpdate" value="true"/>
    </bean>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///activiti?nullCatalogMeansCurrent=true"/>
        <property name="username" value="activiti"/>
        <property name="password" value="activiti"/>
        <property name="maxActive" value="3"/>
        <property name="maxIdle" value="1"/>
    </bean>
</beans>

使用代码初始化数据库:

// 使用 getDefaultProcessEngine 方法,读取默认配置(activiti.cfg.xml)
// 并在创建processEngine时,就会生成数据库表结构
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
Assertions.assertNotNull(processEngine);

创建流程引擎

通过流程引擎的配置类(ProcessEngineConfiguration)可以创建工作流引擎对象ProcessEngine。常用的配置类有:

  • StandaloneProcessEngineConfiguration:这个配置会使用默认的配置文件activiti.cfg.xml,并寻找processEngineConfiguration实例,然后创建流程引擎对象,他会创建一个独立的Spring环境,然后执行。

  • SpringProcessEngineConfiguration:它不会创建IoC容器,而是直接与Spring进行整合。

示例:

// 加载默认配置文件创建
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();

// 使用自定义配置文件的方式创建流程引擎
// 可以指定配置文件名称,以及bean的名字
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml", "processEngineConfiguration");
ProcessEngine processEngine = configuration.buildProcessEngine();
Assertions.assertNotNull(processEngine);

businessKey

在发起流程时,还可以指定businessKey字段:

runtimeService.startProcessInstanceByKey(DEPLOY_KEY, "businessKey");

该字段代表在该流程下,这个业务的唯一id值,如果这个流程是请假单,那么businessKey就是请假单业务表中的主键,在实际的应用场景中,通常都会制定businessKey字段。

在流程实例的运行阶段,businessKey被记录在表ACT_RU_EXECUTION中,该表就是流程实例表。如果任务的执行人要找到对应的业务数据(businessKey),他可以从ACT_RU_TASK表中根据PROC_INST_ID字段,该字段就是流程实例的id字段,找到对应的流程实例记录,从而找到businessKey。

在历史记录中,类似运行阶段, 可以从ACT_HI_PROCINST表中,找到对应的流程实例,从而拿到业务id。

其他表类似,都是通过关联字段找到流程实例记录,并找到businessKey。

挂起、激活流程

通常到达月底时,财务需要封账,所有的报销流程,可能都需要挂起,等到月初再激活再次处理。针对这种情况,Activiti提供了相应的功能已支持。

挂起/激活指定的流程实例

被挂起的流程实例,不可以进行任何任务处理,否则会抛出异常ActivitException

// 判断当前流程实例不是挂起的
Assertions.assertFalse(processInstance.isSuspended());

// 将当前这个流程实例挂起,直挂起当前流程实例
engine.getRuntimeService().suspendProcessInstanceById(processInstance.getProcessInstanceId());

// 挂起后,流程就是挂起状态的,这个状态需要重新查询
processInstance = engine.getRuntimeService().createProcessInstanceQuery().processInstanceId(processInstance.getProcessInstanceId()).singleResult();
Assertions.assertTrue(processInstance.isSuspended());

// 重新激活流程
engine.getRuntimeService().activateProcessInstanceById(processInstance.getProcessInstanceId());

// 这个时候状态又变成了激活
processInstance = engine.getRuntimeService().createProcessInstanceQuery().processInstanceId(processInstance.getProcessInstanceId()).singleResult();
Assertions.assertFalse(processInstance.isSuspended());

挂起/激活指定的流程

流程也可以被挂起,被挂起的流程不能启动新的流程,如果启动会报错ActivitiException,而在挂起之前启动的流程实例则不会受到影响。

// 不仅可以针对流程实例挂起,还可以对指定的流程进行挂起
// 被挂起的流程不能发起流程实例,以及发起的流程实例也会处于挂起状态
engine.getRepositoryService().suspendProcessDefinitionByKey(processInstance.getProcessDefinitionKey());

// 流程状态变为挂起时,不可以发起流程
Assertions.assertThrowsExactly(ActivitiException.class, () -> {
    engine.getRuntimeService().startProcessInstanceByKey(DEPLOY_KEY);
});

// 在流程实例挂起前启动的流程实例,不会被挂起
processInstance = engine.getRuntimeService().createProcessInstanceQuery().processInstanceId(processInstance.getProcessInstanceId()).singleResult();
Assertions.assertFalse(processInstance.isSuspended());

任务处理-为task指定Assignee

在BPMN流程图中,含有多种任务,任务可以指定任务责任人(assignee),责任人的分配又包含多种方式。

固定分配

所谓固定分配,就是将责任人在绘制时直接写入到bpmn文件中,比如如下的BPMN文件的部分显示的含义为,创建请假单的任务的责任人为zhangsan。

<userTask id="creator" name="创建请假单" activiti:assignee="zhangsan"/>

但是大多数的情况下,任务的责任人通常不应该在流程图绘制的过程中就指定他们,通常是作为变量传入到流程中,再启动流程实例。

表达式分配

大多数情况下,我们都会使用UEL表达式为任务分配责任人。UEL是Unified Expression Language,统一表达式语言。Activiti支持两种UEL表达式:UEL-value 和 UEL-method。

简单的来说,就是在定义BPMN的时候,为Task分配的assignee不再是静态值,而是一个UEL表达式,然后程序在启动实例的时候,为流程指定所有的UEL表达式的值,即可完成责任人的自定义分配。

为BPMN指定UEL表达式:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
  <process id="leave-uel" name="请假单-uel" isExecutable="true">
    <startEvent id="startevent1" name="Start"/>
    <userTask id="creator" name="创建请假单" activiti:assignee="${creator}"/>
    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="creator"/>
    <userTask id="usertask2" name="部门经理审核" activiti:assignee="${approvator1}"/>
    <sequenceFlow id="flow2" sourceRef="creator" targetRef="usertask2"/>
    <userTask id="usertask3" name="人事复核" activiti:assignee="${approvator2}"/>
    <sequenceFlow id="flow3" sourceRef="usertask2" targetRef="usertask3"/>
    <endEvent id="endevent1" name="End"/>
    <sequenceFlow id="flow4" sourceRef="usertask3" targetRef="endevent1"/>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_myProcess">
    <bpmndi:BPMNPlane bpmnElement="myProcess" id="BPMNPlane_myProcess">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="130.0" y="160.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="creator" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="210.0" y="150.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
        <omgdc:Bounds height="55.0" width="105.0" x="360.0" y="150.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
        <omgdc:Bounds height="55.0" width="105.0" x="510.0" y="150.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="660.0" y="160.0"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="165.0" y="177.0"/>
        <omgdi:waypoint x="210.0" y="177.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
        <omgdi:waypoint x="315.0" y="177.0"/>
        <omgdi:waypoint x="360.0" y="177.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="465.0" y="177.0"/>
        <omgdi:waypoint x="510.0" y="177.0"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="615.0" y="177.0"/>
        <omgdi:waypoint x="660.0" y="177.0"/>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

部署流程,启动并指定负责人的值:

String processInstanceId = runtimeService
        // 第三个参数就是uel的取值
        // 这些值都会存储到关联表ACT_RU_VARIABLE中
        .startProcessInstanceByKey("leave-uel", "测测请假单id101", new HashMap<String, Object>() {{
            put("creator", "张三");
            put("approvator1", "李四");
            put("approvator2", "wangwu");
        }})
        .getId();

// 查询流程的所有任务
List<Task> taskList = engine.getTaskService().createTaskQuery()
        .processInstanceId(processInstanceId)
        .list();
Assertions.assertEquals(taskList.size(), 1);

Assertions.assertEquals(taskList.get(0).getAssignee(), "张三");

上面的方式是通过UEL-value获取的,我们也可以通过UEL-method来设置:

定义BPMN的值为${userBean.getUserName()},然后再发起流程实例的时候,指定userBean,会自动调用getUserName()方法。

此外,还可以写入一些特殊的表达式,比如果请假天数大于5天,换王富贵审批:

${times > 5 ? "王五" : "王富贵"}

任务监听器分配

除了表达式分配,还有一种监听器分配的方式,他会为UserTask分配一个任务监听器(Task Listeners),当与用户任务相关的生命周期事件发生时触发。例如,在任务被分配给用户之前或之后,或者在用户完成任务时,可以执行一些定制的操作,如更新数据库记录、发送通知邮件、审计日志记录等。

实现任务监听需要先实现接口TaskListener,任务监听器监听的时间类型分为:

  • create 任务创建后事件(分配之前)

  • assignment 任务分配后事件

  • complete 任务完成后事件

  • delete 任务删除后事件

首先,创建TaskListener实现:

public class MyTaskListener implements TaskListener {

    @Override
    public void notify(DelegateTask delegateTask) {
        // 为每个任务设置Assignee
        if (delegateTask.getName().equals("创建请假单")) {
            delegateTask.setAssignee("张三-listener");
        } else if (delegateTask.getName().equals("部门经理审核")) {
            delegateTask.setAssignee("李四-listener");
        } else if (delegateTask.getName().equals("人事复核")) {
            delegateTask.setAssignee("王五-listener");
        }
    }
}

其次,在BPMN中,为所有的任务指定事件类型为create的任务监听器,这样任务创建完毕后,都会为他们设置一个责任人。

任务查询

查询指定负责人的、指定流程的、指定业务id的待办任务:

engine.getTaskService().createTaskQuery()
    .processDefinitionKey("leave-tasklistener") // 查询指定的流程定义
    .processInstanceBusinessKey("测测请假单id102") // 查询指定的业务
    .taskAssignee("张三-listener")
    .list();

任务办理

只有当前责任人才能完成任务,在调用完成任务的方法前,需要我们先校验任务的责任人是否是当前人,并调用complete方法进行任务办理:

taskService.complete(taskId);

流程变量

流程运转时,有时会需要流程变量,业务系统和Activiti结合时少不了流程变量,流程变量就是Activiti在管理工作流时根据管理需要而设置的变量。比如在出差流程中,如果出差天数大于三天,则由总经理审核,否则则由人事直接审核。这里,出差天数就是一个流程变量,需要在流程流转时使用。

  1. 在属性上使用UEL表达式,比如在Assignee出设置读取变量${assigee}assignee就是一个类流程变量名称。

  2. 在连线上使用UEL表达式,比如在连线处设置:${price < 1000}。price就是一个流程变量名称,uel表达式结果类型为布尔类型,根据值返回流程的走向。

他们的变量名称都是不可以重复的,如果重复设置,后面的会覆盖前面的。

流程变量的类型

设置流程变量

在连接线处设置分支判断:

<conditionExpression xsi:type="tFormalExpression">${days &lt;= 3}</conditionExpression>

设置流程变量共有三种方式:

  1. 发起流程时,设置流程变量

    String processInstanceId = runtimeService
                 // 第三个参数就是uel的取值
                 // 这些值都会存储到关联表ACT_RU_VARIABLE中
                 .startProcessInstanceByKey("leave-var", "测测请假单id109", new HashMap<String, Object>() {{
                     put("days", days);
                 }})
                 .getId();
  2. 在变量被使用的前一步的complete方法设置:

     // lisi 审核时,设置流程变量,给下一步使用ø
     taskService.complete(task.getId(), new HashMap<String,Object>(){{
        put("days", days);
     }});
  3. 在变量被使用的前一步,使用setVariables方法设置:

    taskService.setVariables(task.getId(), new HashMap<String, Object>() {{
        put("days", days);
    }});
    taskService.complete(task.getId());

获取流程变量

taskService.getVariable(taskId, "days")

局部流程变量

局部流程变量不同于流程变量,是用于在某个任务阶段,记录任务的额外信息的。

设置局部流程变量

局部流程变量只能在该任务节点中设置和获取,在其他任务节点中,无法获取。

注意,连线是属于头任务的,也就是说如果头任务。

// 在任务办理前通过方法setVariablesLocal设置:
int days = 10;
taskService.setVariablesLocal(task.getId(), new HashMap<String, Object>(){{
    put("days", days);
}});
taskService.complete(task.getId());

获取局部流程变量

taskService.getVariableLocal(taskId, "days");

组任务

在流程定义中任务节点的assignee固定设置任务负责人,将参与者固定设置在bpmn文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性则比较差。针对这种情况,可以设置多个候选人,从多个候选人中选择参与者来完成任务。

我们可以通过在BPMN中对对应的任务设置候选人和候选组来完成此类需求的设置:

  1. Candidate Users

  2. Candidate Group

设置候选人

在BPMN中设置 Candidate Users,多个候选人之间使用英文逗号隔开:

<userTask id="usertask3" name="人事复核" activiti:candidateUsers="zhangsan,lisi,wangwu,zhaoliu" />

当任务流程进入到人事复合节点时,zhangsan、lisi、wangwu、zhaoliu都可以进行任务的完成操作,需要候选人中的某个人拾取任务:

task = engine.getTaskService().createTaskQuery()
        .processInstanceId(processInstanceId)
        .singleResult();
// 拥有候选人的任务,如果没有设置负责人本身是没有负责人的
Assertions.assertNull(task.getAssignee());
// 如果候选人拾取了这个任务,这个任务才会有负责人
engine.getTaskService().claim(task.getId(), "zhangsan");

task = engine.getTaskService().createTaskQuery()
        .processInstanceId(processInstanceId)
        .singleResult();
// zahngsan拾取这个任务之后,任务的负责人就变为了zhangsan
Assertions.assertEquals(task.getAssignee(), "zhangsan");

查询作为候选人的的任务待办:

// 查询zhangsan作为候选人或者责任人的任务
List<Task> taskList = engine.getTaskService().createTaskQuery()
        .taskCandidateOrAssigned("zhangsan")
        .list();

如果拾取任务之后,不想继续处理任务,可以归还任务:

// 归还任务
task = engine.getTaskService().createTaskQuery()
        .taskId(task.getId())
        .taskAssignee("zhangsan")
        .singleResult();
// zhangsan确实有这个任务,有这个任务才可归还
Assertions.assertNotNull(task);

// 归还任务直接将Assignee设置为空即可
engine.getTaskService().setAssignee(task.getId(), null);

任务负责人也可以将任务交接给其他人:

// 任务负责人将任务交给其他人来处理
engine.getTaskService().setAssignee(task.getId(), "lisi");

网关

网关用来控制流程的流向。

排他网关 ExclusiveGateway

排他网关,用来在流程中实现决策。 当流程执行到这个网关,所有分支都会判断条件是否为true,如果为true则执行该分支,

注意:排他网关只会选择一个为true的分支执行。如果有两个分支条件都为true,排他网关会选择id值较小的一条分支去执行。

为什么要用排他网关?

不用排他网关也可以实现分支,如:在连线的condition条件上设置分支条件。 在连线设置condition条件的缺点:如果条件都不满足,流程就结束了(是异常结束)。

并行网关 ParallelGateway

并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:

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

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

注意:

  • 如果同一个并行网关有多个进入和多个外出顺序流, 它就同时具有分支和汇聚功能。 这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。

  • 并行网关不会解析条件,即使顺序流中定义了条件,也会被忽略。

包含网关 InclusiveGateway

包含网关可以看做是排他网关和并行网关的结合体。

和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。 但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。

包含网关的功能是基于进入和外出顺序流的:

  • 分支: 所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。

  • 汇聚:所有并行分支到达包含网关,会进入等待状态, 直到每个包含流程token的进入顺序流的分支都到达。 这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。 在汇聚之后,流程会穿过包含网关继续执行。

事件网关 EventGateway

事件网关允许根据事件判断流向。网关的每个外出顺序流都要连接到一个中间捕获事件。 当流程到达一个基于事件网关,网关会进入等待状态:会暂停执行。与此同时,会为每个外出顺序流创建相对的事件订阅。

事件网关的外出顺序流和普通顺序流不同,这些顺序流不会真的"执行", 相反它们让流程引擎去决定执行到事件网关的流程需要订阅哪些事件。 要考虑以下条件:

  1. 事件网关必须有两条或以上外出顺序流;

  2. 事件网关后,只能使用intermediateCatchEvent类型(activiti不支持基于事件网关后连接ReceiveTask)

  3. 连接到事件网关的中间捕获事件必须只有一个入口顺序流。

Service 接口

创建流程引擎后,通过引擎对象,获取针对不同的业务的操作Service,目前共有如下几个Service:

Activiti在创建ProcessEngine后,会派生出来多个Service为业务程序提供服务,这些API最终都会操作对应数据库的表,并且可以通过getXxxService()方法获取他们,目前主要拥有的Service有:

  • RepositoryService,资源管理

  • TaskService,任务管理

  • RuntimeService,运行管理

  • ManagementService,引擎管理

  • HistoryService,历史管理

  • IdentityService,已删除

  • FormService,已删除

Activiti的表结构

共分为四类:

  • ACT_RE_*,RE代表Repository,仓库;是用于存储流程定义、流程部署、流程模型等相关数据的。

  • ACT_RU_*,RU代表Runtime,运行时;流程启动到结束过程中所有运行的数据全放在该表中。结束后会从这些表中删除掉。

  • ACT_HI_*,HI代表History,历史;是所有流程实例运行的历史信息,其中的表名和字段名,与运行时表相对应。

  • ACT_GE_*,GE代表General,通用;是用于记录一些其他的通用的数据,比如bpmn文件流、系统属性。

ACT_RE_PROCDEF 流程定义表

create table if not exists activiti.ACT_RE_PROCDEF
(
    -- 流程ID,由 流程key:版本:自增id 组成
    ID_                     varchar(64)             not null     primary key,
    -- 数据版本,乐观锁
    REV_                    int                     null,
    -- 类别,自动生成
    CATEGORY_               varchar(255)            null,
    -- 画图的时候的NAME
    NAME_                   varchar(255)            null,
    -- 画图的时候的ID
    KEY_                    varchar(255)            not null,
    -- 流程的版本
    VERSION_                int                     not null,
    -- 管理流程部署的ID
    DEPLOYMENT_ID_          varchar(64)             null,
    -- BPMN文件名称
    RESOURCE_NAME_          varchar(4000)           null,
    -- 图片名称
    DGRM_RESOURCE_NAME_     varchar(4000)           null,
    -- 流程描述
    DESCRIPTION_            varchar(4000)           null,
    -- 是否从key启动,0否1是
    HAS_START_FORM_KEY_     tinyint                 null,
    HAS_GRAPHICAL_NOTATION_ tinyint                 null,
    -- 是否挂起 1激活2挂起
    SUSPENSION_STATE_       int                     null,
    -- 租户ID
    TENANT_ID_              varchar(255) default '' null,
    -- 流程引擎的版本
    ENGINE_VERSION_         varchar(255)            null,
    -- 应用版本
    APP_VERSION_            int                     null,
    constraint ACT_UNIQ_PROCDEF
        unique (KEY_, VERSION_, TENANT_ID_)
)
    collate = utf8mb3_bin;

ACT_RE_DEPLOYMENT 流程部署记录表

存储流程的部署记录。相同的key不同的内容多次部署,会产生多条部署记录。

create table activiti.ACT_RE_DEPLOYMENT
(
    -- ID
    ID_                      varchar(64)             not null    primary key,
    -- 部署的名字
    NAME_                    varchar(255)            null,
    -- 分类,通过API设置
    CATEGORY_                varchar(255)            null,
    -- 唯一标识,通过API设置
    KEY_                     varchar(255)            null,
    -- 租户ID
    TENANT_ID_               varchar(255) default '' null,
    -- 部署时间
    DEPLOY_TIME_             timestamp(3)            null,
    -- 引擎版本
    ENGINE_VERSION_          varchar(255)            null,
    -- 版本
    VERSION_                 int          default 1  null,
    PROJECT_RELEASE_VERSION_ varchar(255)            null
)
    collate = utf8mb3_bin;

ACT_RE_MODEL 流程模型表

用于存储每个流程最新的一个流程模型,可通过API来操作。

create table ACT_RE_MODEL
(
    -- 模型ID
    ID_                           varchar(64)             not null    primary key,
    -- 模型版本,乐观锁
    REV_                          int                     null,
    -- 模型名称
    NAME_                         varchar(255)            null,
    -- 模型key
    KEY_                          varchar(255)            null,
    -- 模型类别
    CATEGORY_                     varchar(255)            null,
    -- 创建时间
    CREATE_TIME_                  timestamp(3)            null,
    -- 最后更新时间
    LAST_UPDATE_TIME_             timestamp(3)            null,
    -- 当前版本
    VERSION_                      int                     null,
    -- 元信息,可用json存储
    META_INFO_                    varchar(4000)           null,
    -- 外键关联部署表
    DEPLOYMENT_ID_                varchar(64)             null,
    -- 外键关联ACT_GE_BYTEARRAY的BPMN资源ID
    EDITOR_SOURCE_VALUE_ID_       varchar(64)             null,
    -- 外键关联ACT_GE_BYTEARRAY的PNG资源ID
    EDITOR_SOURCE_EXTRA_VALUE_ID_ varchar(64)             null,
    -- 租户ID
    TENANT_ID_                    varchar(255) default '' null,
    constraint ACT_FK_MODEL_DEPLOYMENT
        foreign key (DEPLOYMENT_ID_) references activiti.ACT_RE_DEPLOYMENT (ID_),
    constraint ACT_FK_MODEL_SOURCE
        foreign key (EDITOR_SOURCE_VALUE_ID_) references activiti.ACT_GE_BYTEARRAY (ID_),
    constraint ACT_FK_MODEL_SOURCE_EXTRA
        foreign key (EDITOR_SOURCE_EXTRA_VALUE_ID_) references activiti.ACT_GE_BYTEARRAY (ID_)
)
    collate = utf8mb3_bin;

ACT_GE_BYTEARRAY 二进制资源表

create table activiti.ACT_GE_BYTEARRAY
(
    -- 主键
    ID_            varchar(64)  not null    primary key,
    -- 数据版本,乐观锁
    REV_           int          null,
    -- 资源名称,与流程定义的name相同
    NAME_          varchar(255) null,
    -- 对应的流程部署的ID,一对一
    DEPLOYMENT_ID_ varchar(64)  null,
    -- 二进制资源流
    BYTES_         longblob     null,
    -- 生成来源,0为用户生成,1为Activiti
    GENERATED_     tinyint      null,
    constraint ACT_FK_BYTEARR_DEPL
        foreign key (DEPLOYMENT_ID_) references activiti.ACT_RE_DEPLOYMENT (ID_)
)
    collate = utf8mb3_bin;

部署流程

ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 创建部署一个请假申请单流程
RepositoryService repositoryService = engine.getRepositoryService();
Deployment leave = repositoryService.createDeployment()
        .name("请假申请单")
        .addClasspathResource("bpmn/leave.bpmn")
        .key("leave")
        .deploy();
Assertions.assertNotNull(leave);

部署一个请假申请单流程,根据定义,会执行如下三条SQL:

insert into ACT_RE_PROCDEF(ID_, REV_, CATEGORY_, NAME_, KEY_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_, DGRM_RESOURCE_NAME_, DESCRIPTION_, HAS_START_FORM_KEY_, HAS_GRAPHICAL_NOTATION_, SUSPENSION_STATE_, TENANT_ID_, ENGINE_VERSION_, APP_VERSION_)
values ('leave:1:3', 1, 'http://www.activiti.org/test', '请假流程', 'leave', 1, '1', 'bpmn/leave.bpmn', null, null,false, true, 1, '', null, null);

-- 插入一个流程 Deployment
insert into ACT_RE_DEPLOYMENT(ID_, NAME_, CATEGORY_, KEY_, TENANT_ID_, DEPLOY_TIME_, ENGINE_VERSION_, VERSION_, PROJECT_RELEASE_VERSION_)
values (1, '请假申请单', null, 'leave', '', '2024-01-17 16:34:30.264', null, 1, null);

-- 插入上传的文件流,REV代表所属的流程id,以及文件名称、文件流字节数组
insert into ACT_GE_BYTEARRAY(ID_, REV_, NAME_, BYTES_, DEPLOYMENT_ID_, GENERATED_)
values (2, 1, 'bpmn/leave.bpmn', '文件流', 1, false);

-- 更新下一个id序列值
update ACT_GE_PROPERTY SET REV_ = 2, VALUE_ = 2501 where NAME_ = 'next.dbid' and REV_ = 1

最后更新于