Activiti7-进阶(SpringBoot 2.6版)

Activiti7流程引擎 – 进阶篇

续接上一篇文章,在上一篇文章中,我们完成了activiti7基础的学习包括25张表、类关系图、流程符号、画流程图、部署流程、启动流程实例、任务查询等,并且使用activiti7提供的API完成了请假申请的流程流转。明白了流程引擎是如何工作的,要想完成企业级的开发,这些知识还是远远不够的,在这一篇文章,我们会深入activiti7的学习,继续探索流程引擎更高级的知识。

1、流程资源操作

对于activiti流程引擎,除了定义流程、启动流程实例、任务信息查询、完成任务等这些操作外,我们还可以做一些删除、资源下载等一些操作。

1.1、流程定义消息查询

查询流程相关信息,包括流程定义、流程部署、流程定义的版本等,注意流程定义信息主要在ACT_RE_PROCDEF,如果流程定义的key一样,启动流程实例时,会选择版本号最大的那个定义信息来启动。例如

@Test
public void test() {
    // 1.获取repositoryService
    RepositoryService repositoryService = processEngine.getRepositoryService();
    // 2.获取ProcessDefinitionQuery
    ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
    String processDefinitionKey = "leave";
    ProcessDefinition processDefinition = processDefinitionQuery.processDefinitionKey(processDefinitionKey).latestVersion().singleResult();
    // 3.输出流程定义信息
    logger.info("流程定义id:" + processDefinition.getId());
    logger.info("流程定义name:" + processDefinition.getName());
    logger.info("流程定义key:" + processDefinition.getKey());
    logger.info("流程定义version:" + processDefinition.getVersion());
}

1.2、删除流程

删除流程也需要特别注意,我们使用流程引擎需要定义或部署流程、启动流程实例等,这些数据会写到多张表里面去,删除流程时候要删除那些数据??

@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
    public void deleteTest() {
        RepositoryService repositoryService = processEngine.getRepositoryService();
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
        List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("leave").list();
        // 获取流程定义ID
        List<String> definitionIdList = definitionList.stream().map(ProcessDefinition::getDeploymentId).collect(Collectors.toList());
        logger.info(definitionIdList.toString());

        // 通过流程定义ID删除
        // void deleteDeployment(String deploymentId, boolean cascade); cascade 是否级联删除
        definitionIdList.forEach(item -> repositoryService.deleteDeployment(item, true));
    }
}

删除说明:

  • 删除分为普通删除和级联删除(cascade=true
  • 删除流程定义信息,如果流程定义已有实例启动则删除时会报错(除非使用级联删除)
  • 删除流程定义,历史表信息不会被删除
  • 级联删除操作一般至开放给超级管理员使用
  • 删除流程定义犹如流程的部署或定义,会把ACT_RE_PROCDEF、ACT_RE_DEPLOYMENT、ACT_GE_BYTEARRAY表关联的数据进行删除

1.3、流程资源的下载

我们的流程资源已经上传到数据库(包括bpmn、png等文件)这些文件会以二进制的形式保存到数据库字段,当然我们也可以对这些资源进行操作,例如读到内存里进行下载等一些IO操作。

这里我们主要以下载为例来介绍流程资源的IO操作,针对流程资源的下载我们可以使用activiti提供的API,也可以自己写下载代码(但是需要解决jdbc对blob类型,clob类型字段的转换问题)

Activiti7-进阶(SpringBoot 2.6版)_第1张图片

例如下载至磁盘:

/**
 * @description:
 * @author: laizhenghua
 * @date: 2022/12/31 12:53
 */
@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
    public void downloadTest() {
        RepositoryService repositoryService = processEngine.getRepositoryService();
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
        List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("leave").list();

        definitionList.forEach(item -> {
            String deploymentId = item.getDeploymentId();
            String resourceName = item.getResourceName();
            InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
            // 下载
            File file = new File(String.format("src/main/resources%s%s.xml", File.separator, item.getKey() + "_" + item.getVersion()));
            FileOutputStream fileOutputStream = null;
            try {
                fileOutputStream = new FileOutputStream(file);
                write(inputStream, fileOutputStream);
                fileOutputStream.flush();
            } catch (IOException e) {
                logger.error(e.getMessage());
                e.printStackTrace();
            } finally {
                IOUtils.close(fileOutputStream);
            }
            IOUtils.close(inputStream);
        });
    }

    public void write(InputStream inputStream, OutputStream outputStream) {
        byte[] buffer = new byte[4096];
        try {
            int count = inputStream.read(buffer, 0, buffer.length);
            while (count != -1) {
                outputStream.write(buffer, 0, count);
                count = inputStream.read(buffer, 0, buffer.length);
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }
}

2、流程实例

2.1、businessKey

我们都知道流程定义相当于一个模板,启动流程实例的时候,通过这个模板启动,因此流程定义与流程实例是一对多的关系。
Activiti7-进阶(SpringBoot 2.6版)_第2张图片

为了方便区分流程实例,以便做任务查询、完成任务等操作,可用一个字段BUSINESS_KEY_标识流程实例,即启动流程实例指定这个businessKey,就可很方便的区分流程实例,对查询个人任务等有极大的意义,例如:

/**
 * @description:
 * @author: laizhenghua
 * @date: 2022/12/31 12:53
 */
@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
    public void startInstanceTest() {
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // 以请假申请流程为例 businessKey就可以是员工的工号或者是学生的学号
        // 老王工号=2017001
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave", "2017001");
        String processDefinitionId = processInstance.getProcessDefinitionId(); // 流程定义ID
        String processInstanceId = processInstance.getProcessInstanceId(); // 流程实例ID
        String id = processInstance.getId(); // taskId
        String businessKey = processInstance.getBusinessKey(); // 业务/流程标识
        logger.info(String.format("流程定义ID=[%s]", processDefinitionId));
        logger.info(String.format("流程实例ID=[%s]", processInstanceId));
        logger.info(String.format("taskID=[%s]", id));
        logger.info(String.format("业务/流程标识=[%s]", businessKey));
    }
}

启动流程实例,加上businessKey后,有字段BUSINESS_KEY_的表都会写入这个字段值,例如:

SELECT * FROM ACT_RU_EXECUTION E WHERE E.BUSINESS_KEY_ = '2017001'

SELECT * FROM ACT_RU_TASK T WHERE T.BUSINESS_KEY_ = '2017001'

SELECT * FROM ACT_HI_PROCINST P WHERE P.BUSINESS_KEY_ = '2017001'

2.2、挂起与激活

某些情况可能由于流程变更需要将当前运行的流程暂停而不是直接删除,流程暂停后将不会继续执行。

1、通流程定义ID挂起所有流程即操作流程定义为挂起状态,该流程定义下边所有的流程实例全部暂停,流程定义为挂起状态该流程定义将不允许启动新的流程实例,同时该流程定义下所有的流程实例将全部挂起暂停执行。

/**
 * @description:
 * @author: laizhenghua
 * @date: 2022/12/31 12:53
 */
@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
    public void suspendInstanceTest() {
        RepositoryService repositoryService = processEngine.getRepositoryService();
        List<ProcessDefinition> definitionList = repositoryService.createProcessDefinitionQuery().processDefinitionKey("leave").list();
        definitionList.forEach(item -> {
            if (item.isSuspended()) {
                // 挂起即激活
                // void activateProcessDefinitionById(String processDefinitionId, boolean activateProcessInstances, Date activationDate);
                // processDefinitionId 流程定义ID
                // activateProcessInstances 是否激活流程实例
                // activationDate 激活时间
                repositoryService.activateProcessDefinitionById(item.getId(), true, new Date());
            } else {
                // 挂起
                repositoryService.suspendProcessDefinitionById(item.getId(), true, new Date());
            }
        });
    }
}

2、单个流程实例挂起即操作流程实例对象,针对单个流程执行挂起操作,某个流程实例挂起则此流程不再继续执行,完成该流程实例的当前任务将报异常。

/**
 * @description:
 * @author: laizhenghua
 * @date: 2022/12/31 12:53
 */
@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
    public void suspendInstanceTest() {
        RuntimeService runtimeService = processEngine.getRuntimeService();
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceBusinessKey("2017001").singleResult();
        if (processInstance.isSuspended()) {
            logger.info(String.format("ProcessInstance[id=%s] is activate", processInstance.getProcessInstanceId()));
            runtimeService.activateProcessInstanceById(processInstance.getProcessInstanceId());
        } else {
            logger.info(String.format("ProcessInstance[id=%s] is suspended", processInstance.getProcessInstanceId()));
            runtimeService.suspendProcessInstanceById(processInstance.getProcessInstanceId());
        }
    }
}

尝试完成挂起的任务:

@Test
public void completeTaskTest() {
    TaskService taskService = processEngine.getTaskService();
    Task task = taskService.createTaskQuery().processInstanceBusinessKey("2017001").singleResult();
    boolean suspended = task.isSuspended();
    if (suspended) {
        logger.info(String.format("Task[id=%s] is suspended", task.getId()));
    }
    taskService.complete(task.getId());
}

抛出异常

Activiti7-进阶(SpringBoot 2.6版)_第3张图片

3、个人任务

3.1、分配任务负责人

我们都知道,每个流程都有参与者,也就是ASSIGNEE_字段。在前面基础知识学习中我们也有指定Assignee,这也是固定分配写法:在进行业务流程建模时指定固定的任务负责人,每一个任务将按照bpmn的配置去分配任务负责人。但是在实际开发中这个变量值一定不会写死,流程的参与者不可能一直固定不变。

Activiti7-进阶(SpringBoot 2.6版)_第4张图片

因此实际开发中,我们应该使用站位符(即表达式)指定流程参与者,动态的分配任务负责人。Activiti支持两个UEL表达式分别是UEL-valueUEL-method,UEL 是Java EE6规范的一部分, UEL(Unified Expression Language)即统一表达式语言。

1、UEL-value,如下图

Activiti7-进阶(SpringBoot 2.6版)_第5张图片

另外表达式还可以写为${user.assignee}的形式,和Assignee一样,user也是activiti的一个流程变量,user.assignee表示通过user的getter方法获取值。例如:

Activiti7-进阶(SpringBoot 2.6版)_第6张图片
启动流程实例:

/**
 * @description:
 * @author: laizhenghua
 * @date: 2022/12/31 12:53
 */
@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
    public void processDeploymentTest() {
        RepositoryService repositoryService = processEngine.getRepositoryService();
        Deployment deploy = repositoryService.createDeployment().addClasspathResource("bpmn/leave.bpmn")
                .name("出差申请(UEL)").deploy();
        logger.info(String.format("流程定义ID=%s", deploy.getId()));
    }

    @Test
    public void processStart() {
        RuntimeService runtimeService = processEngine.getRuntimeService();
        Map<String, Object> variables = new HashMap<>();
        variables.put("assignee0", "alex");
        variables.put("assignee1", "zs");
        variables.put("assignee2", "ls");
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave_uel", variables);
        logger.info(String.format("流程实例ID=%s", processInstance.getProcessInstanceId()));
    }
}

执行成功后,可以在ACT_RU_VARIABLE表中看到刚才map中的数据

SELECT * FROM ACT_RU_VARIABLE V WHERE V.PROC_INST_ID_ = '412923c2-89be-11ed-ad64-080058000005'

Activiti7-进阶(SpringBoot 2.6版)_第7张图片

2、UEL-method,如下图

Activiti7-进阶(SpringBoot 2.6版)_第8张图片

UserBean是spring容器中的一个bean实例,表示调用该beangetUserName()方法。

3、UEL-valueUEL-method相结合

再比如:

${userService.getUserByLoginName(username)}
/*
userService spring容器的一个bean实例
getUserByLoginName 该bean的一个方法
username 可以是activiti的流程变量 做为参数传到getUserByLoginName()方法中
*/

除了上面两种固定分配和表达式分配任务负责人外,Activiti还可以使用监听器分配流程参与者,监听器不仅可以分配流程参与者,还可以完成很多Activiti流程的业务。

Activiti7-进阶(SpringBoot 2.6版)_第9张图片

/*
Event的选项包含:
	Create 任务创建后触发
	Assignment 任务分配后触发
	Delete 任务完成后触发
	All 所有事件发生都触发
*/

所谓监听器就是发生对应的任务相关事件执行自定义的Java逻辑或表达式,使用监听器后就不需要再指定Assignee了,完全可以在监听器中设置Assignee,例如:

1、创建自定义的监听器,必须实现org.activiti.engine.delegate.TaskListener接口:

/**
 * @description:
 * @author: laizhenghua
 * @date: 2023/1/1 19:10
 */
public class ActTaskListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        if ("create".equals(delegateTask.getEventName()) && "班长审批".equals(delegateTask.getName())) {
            delegateTask.setAssignee("alex");
        }
    }
}

2、流程修改(添加监听器):

Activiti7-进阶(SpringBoot 2.6版)_第10张图片

3、定义流程

@Test
public void processDeploymentTest() {
    RepositoryService repositoryService = processEngine.getRepositoryService();
    Deployment deploy = repositoryService.createDeployment().addClasspathResource("bpmn/test.bpmn")
            .name("出差申请(UEL)").deploy();
    logger.info(String.format("流程定义ID=%s", deploy.getId()));
}

4、启动流程,输出监听器为我们设置的Assignee

@Test
public void processStart() {
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("test", "2023");
    logger.info(String.format("流程实例ID=%s", processInstance.getProcessInstanceId()));

    TaskService taskService = processEngine.getTaskService();
    Task task = taskService.createTaskQuery().processInstanceBusinessKey("2023").singleResult();
    logger.info(String.format("当前环节参与者=%s", task.getAssignee()));
}

4、认识流程变量

4.1、什么是流程变量

流程变量在Activiti中是一个非常重要的角色,流程运转需要流程变量,业务系统和Activiti结合时少不了流程变量的参与,流程变量就是Activiti在管理工作流是根据管理需要而设置的变量。

比如说:出差报销流程,如果报销金额大于1W,则由总裁办审核,否则由人事直接审核,报销金额就可以设置为流程变量,在流程运转时使用,从而丰富我们的流程。

那么既然是变量,在Java中肯定是有类型的,在Activiti中又支持那些类型变量呢?

Type Description
String 与Java数据类型一致
Integer 与Java数据类型一致
short 与Java数据类型一致
Long 与Java数据类型一致
Double 与Java数据类型一致
Date 与Java数据类型一致
Binary 二进制数据
Serializable Java对象

如果将实体存储到流程变量中,必须实现序列化接口Serializable,为了防止由于新增字段而无法反序列化,还需要生成SerialVersionUID

4.2、流程变量的作用域

Activiti中流程变量的作用域可以是一个流程实例(ProcessInstance)、一个任务(Task)、或一个执行实例(Execution)。

当然不同的作用域也有不同的叫法。

1、Global变量

流程变量的默认作用域是流程实例。当一个流程变量的作用域为流程实例时,可以称为Global变量。需要注意的是:Global变量中变量名不允许重复,如果设置了相同名称的变量,后设置的值会覆盖前面设置的变量值。

2、Local变量

任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大,称为Local变量。Local变量由于在不同的任务或不同的执行实例中,作用域互不影响,变量名可以相同不会造成啥影响。

4.3、流程变量的使用

1、在属性上使用UEL表达式

UEL前面我们也有介绍到,比如:${assignee},assignee就是一个流程变量。

2、在连线上使用UEL表达式

可以在连线上设置UEL表达式,决定流程的走向。比如:${price < 10000},price就是一个流程变量名称,UEL表达式结果类型为布尔类型。

3、使用示例

需要注意的是一个环节连两条线时,两条线的Condition都不能为空。

Activiti7-进阶(SpringBoot 2.6版)_第11张图片

如上图报销流程,假设报销金额超过10000的时候需要运营总监审核,这时候又该如何实现呢?

/**
 * @description:
 * @author: laizhenghua
 * @date: 2022/12/31 12:53
 */
@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
   public void globalVariableTest() {
        RepositoryService repositoryService = processEngine.getRepositoryService();
        // 报销流程定义/部署
        Deployment deploy = repositoryService.createDeployment().key("test").name("报销申请")
                .addClasspathResource("bpmn/test.bpmn").deploy();
        String key = deploy.getKey();
        logger.info(String.format("报销流程ID=%s", deploy.getId()));

        // 设置流程变量
        Map<String, Object> variables = new HashMap<>();
        variables.put("money", 12000); // 报销金额
        variables.put("assignee0", "fqz"); // 发起者
        variables.put("assignee1", "zjl"); // 总经理
        variables.put("assignee2", "yyzj"); // 运营总监
        variables.put("assignee3", "cw"); // 财务

        // 启动流程
        RuntimeService runtimeService = processEngine.getRuntimeService();
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, "bx", variables);

        logger.info(String.format("流程实例ID=%s", processInstance.getProcessInstanceId()));
    }
}

完成任务(一直执行完成任务代码,直至走完所有环节):

@Test
public void completeTest() {
    String processInstanceId = "0082a793-98d2-11ed-9d75-080058000005";
    TaskService taskService = processEngine.getTaskService();
    Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
    if (task != null) {
        taskService.complete(task.getId());
    } else {
        logger.info(String.format("<---- process [processInstanceId=%s] ---->", processInstanceId));
    }
}

流程全生命周期查询:

SELECT  
	AI.PROC_DEF_ID_ AS 流程定义ID,
	AI.PROC_INST_ID_ AS 流程实例ID,
	AI.TASK_ID_ AS 任务ID,
	AI.ACT_NAME_ AS 环节名称,
	AI.ACT_TYPE_ AS 环节类型,
	AI.ASSIGNEE_ AS 环节负责人,
	AI.START_TIME_ AS 开始时间,
	AI.END_TIME_ AS 结束时间
FROM ACT_HI_ACTINST AI WHERE AI.PROC_INST_ID_ = '0082a793-98d2-11ed-9d75-080058000005' ORDER BY AI.START_TIME_ ASC

Activiti7-进阶(SpringBoot 2.6版)_第12张图片
OK,另外一种场景可自行测试下。

以上是流程实例启动时指定流程变量,除了这种方式外Activiti还可以在任务办理时、当前流程实例(流程未结束)、当前任务设置流程变量。


任务办理时设置变量

在完成任务时设置流程变量,改流程变量只有在该任务完成后其他节点才可以使用该变量,它的作用域是整个流程实例,如果设置的流程变量的key在流程实例中存在相同的名字则后设置的变量会替换前面设置的变量。

我们还是以上面流程为例(流程实例ID=30005,下一环节是总经理且办理人是zjl,对应assignee1变量):

Activiti7-进阶(SpringBoot 2.6版)_第13张图片

完成任务时我们改变assignee1的变量值,例如:

@Test
public void completeTaskTest() {
    String processInstanceId = "30005";
    TaskService taskService = processEngine.getTaskService();
    Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
    if (task != null) {
        // 重新指定流程变量
        Map<String, Object> variables = new HashMap<>();
        variables.put("assignee1", "zjl666"); // 总经理
        taskService.complete(task.getId(), variables, true); // 任务办理时重新指定变量(其他节点生效)
    } else {
        logger.info(String.format("<---- process [processInstanceId=%s] is end ---->", processInstanceId));
    }
}

我们再来看总经理环节的任务办理人:

Activiti7-进阶(SpringBoot 2.6版)_第14张图片


通过当前流程实例设置变量

Activiti中也可以通过流程实例设置全局变量,前提是当前流程实例未结束,例如:

@Test
public void processTest() {
    String processInstanceId = "30005";
    RuntimeService runtimeService = processEngine.getRuntimeService();
    String variableName = "assignee1";
    Object variable = runtimeService.getVariable(processInstanceId, variableName); // 根据key获取流程变量

    logger.info(String.format("设置前: [%s=%s]", variableName, variable));
    runtimeService.setVariable(processInstanceId, variableName, "zjl001"); // 重新设置变量

    variable = runtimeService.getVariable(processInstanceId, variableName);
    logger.info(String.format("设置后: [%s=%s]", variableName, variable));
}

当然通过RuntimeService操作流程变量的API不止这些,可自行了解。


通过当前任务设置变量

例如现有taskId=32509的一条任务

Activiti7-进阶(SpringBoot 2.6版)_第15张图片
通过taskId设置变量

@Test
public void taskTest() {
    String taskId = "32509"; // taskId必须有效即在 ACT_RU_TASK 表中存在
    TaskService taskService = processEngine.getTaskService();
    String variableName = "assignee1";
    Object variable = taskService.getVariable(taskId, variableName);
    logger.info(String.format("设置前: [%s=%s]", variableName, variable));

    taskService.setVariable(taskId, variableName, "zjl7777");

    variable = taskService.getVariable(taskId, variableName);
    logger.info(String.format("设置后: [%s=%s]", variableName, variable));
}

RuntimeService类似,操作例程变量的API不止是这个,一次可设置多个变量等,可自行了解。


注意事项

  1. 如果UEL表达式中流程变量名不存在则抛出异常
  2. 如果UEL表达式中流程变量值为NULL,流程不会按UEL表达式去执行,而结束流程
  3. 如果UEL表达式都不符合条件,流程结束
  4. 如果连线不设置条件,则会走flow序号小的那条线
  5. 实际开发中一般很少使用连线的方式去控制流程的走向,一般都使用网关去控制

5、组任务

5.1、候选人设置

根据以往的学习来看,如果任务节点的ASSIGNEE_值写死,也就是说流程定义时将参与者固定设置在.bpmn文件中,如果临时负责人变更则需要修改流程定义,需要重新启动流程实例,这显然是不合理的,也不可能这样。

针对这种情况Activiti可以给任务设置多个候选人(最终只能由一个人来完成此任务),可以从候选人中选择某个参与者来完成任务,例如(设置candidateUsers属性就是指定候选人,多个候选人之间使用英文逗号隔开):

Activiti7-进阶(SpringBoot 2.6版)_第16张图片

设置好后,我们再来看流程引擎是如何运转的

1、请假流程定义并启动流程实例

/**
 * @description:
 * @author: laizhenghua
 * @date: 2022/12/31 12:53
 */
@SpringBootTest
public class ActTest {
    private Logger logger = LoggerFactory.getLogger(ActTest.class);
    @Autowired
    private ProcessEngine processEngine;

    @Test
    public void candidateTest() {
        RepositoryService repositoryService = processEngine.getRepositoryService();
        // 请假流程定义
        Deployment deploy = repositoryService.createDeployment().key("test").name("请假申请")
                .addClasspathResource("bpmn/test.bpmn").deploy();
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploy.getId()).singleResult();
        String definitionId = processDefinition.getId();
        logger.info(String.format("请假流程ID=%s", definitionId));

        // 启动流程
        RuntimeService runtimeService = processEngine.getRuntimeService();
        ProcessInstance processInstance = runtimeService.startProcessInstanceById(definitionId, "qj");

        logger.info(String.format("请假流程实例ID=%s", processInstance.getProcessInstanceId()));
    }
}

2、候选人任务查询

@Test
public void completeTaskTest() {
    String processInstanceId = "47505"; // 流程实例ID
    TaskService taskService = processEngine.getTaskService();
    // 注意这里使用bzr1或bzr2都能查出此任务
    List<Task> list = taskService.createTaskQuery().processInstanceId(processInstanceId)
            .taskCandidateUser("bzr1").list();
    logger.info(list.toString());
}

踩坑指南:如果activiti-spring-boot-starter版本是7.x +版本的,因为默认集成了security,因此security上下文中还需有查询指定的用户也就是bzr1bzr2(用户名相等即可即username=bzr1)否则会抛出Cause: org.springframework.security.core.userdetails.UsernameNotFoundException: bzr1异常,解决方案如下:

@Bean
public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    String[][] usersGroupsAndRoles = {{"system", "password", "ROLE_ACTIVITY_USER"},
            {"bzr1", "password", "ROLE_ACTIVITY_USER"},
            {"bzr2", "password", "ROLE_ACTIVITY_USER"},
            {"admin", "password", "ROLE_ACTIVITY_ADMIN"}
    };
    for (String[] user : usersGroupsAndRoles) {
        List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
        logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
        inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]),
                authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
    }
    return inMemoryUserDetailsManager;
}

3、拾取任务

@Test
public void completeTaskTest() {
    String processInstanceId = "47505"; // 流程实例ID
    String userId = "bzr1";
    TaskService taskService = processEngine.getTaskService();
    // 注意这里使用bzr1或bzr2都能查出此任务
    List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId)
            .taskCandidateUser(userId).list();
    logger.info(taskList.toString());

    // 拾取任务(API为claim())
    taskList.forEach(task -> {
        taskService.claim(task.getId(), userId);
    });

    // 查询当前任务负责人
    Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
    logger.info(String.format("Assignee=%s", task.getAssignee()));

    // 拾取成功后验证bzr2是否还可以查询到当前任务
    task = taskService.createTaskQuery().processInstanceId(processInstanceId).taskCandidateUser("bzr2").singleResult();
    assert task != null;
    logger.info(String.format("bzr2查到一任务,任务id=%s", task.getId()));
}

其中一个拾取任务成功后,另外一个人就查询不到此任务了。

6、网关

前面流程变量我们已经可以实现通过连线的方式去控制流程的走向,显然网关也是做这个事情,只是网关可以实现更灵活的流程控制如可以几个分支都走,也可以几个分支都走完以后统一的结束,也可以选择走某一条分支这都是连线达不到的效果。

6.1、排他网关ExclusiveGateway

排他网关

  1. 排他网关exclusive gateway 经常使用流程变量决定流程下一步要选择的路径
  2. 排他网关(也叫异或网关 XOR gateway,或者更专业的,基于数据的排他网关 exclusive data based gateway),用于为流程中的决策建模。
  3. 当执行到达这个网关时,所有出口顺序流会按照它们定义的顺序进行计算。条件计算为true的顺序流(当没有设置条件时,认为顺序流定义为true)会被选择用于继续流程。
  4. 用排他网关时,只会选择一条顺序流。当多条顺序 流的条件都计算为true时,其中在XML中定义的第一条(也只有这条)会被选择,用于继续流程。如果没有可选的顺序流,会抛出异常。
  5. 排他网关,用内部带有’X’图标的标准网关(菱形)表示,'X’图标代表异或(XOR)的含义。请注意内部没有图标的网关默认为排他网关。BPMN 2.0规范不允许在同一个流程中,混合使用带有及没有X的菱形标志
  6. 一个排他网关对应一个以上的顺序流

Activiti7-进阶(SpringBoot 2.6版)_第17张图片

如下图,排他网关使用也很简单,一般需要配合流程变量,判断连线分支是否为true,需要注意的是不用排他网关也能实现此场景的分支控制,只是使用排他网关后即使条件都不满足也会默认走第一条分支,而condition条件如果条件都不满足,流程就异常结束了。

Activiti7-进阶(SpringBoot 2.6版)_第18张图片

6.2、并行网关ParallelGateway

并行网关允许将流程分成(fork)多条分支,也可以把多条分支汇聚(join)到一起,并行网关的功能是基于进入和外出顺序流的。因此使用并行网关也有前提条件,就是说流程必须基于出口顺序流和入口顺序流。

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

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

注意:

  1. 如果并行网关同时具有入口顺序流和出口顺序流,并行网关会先执行入口顺序流,然后执行再分裂成多条可以执行的路径。

  2. 并行网关不执行计算条件。并行网关上面的计算条件会被忽略。

  3. 并行网关分支和合并是同时存在的,就是说一个至少有一个分支和一个合并。但是网关是可以不平衡的,分支和合并的数量可以不一致。

如下图,并行网关内部是一个加号。
Activiti7-进阶(SpringBoot 2.6版)_第19张图片
如下流程图,流程引擎又该如何运转呢?
Activiti7-进阶(SpringBoot 2.6版)_第20张图片
1、启动流程实例

@Test
public void gatewayTest() {
    RepositoryService repositoryService = processEngine.getRepositoryService();
    // 预审流程定义
    Deployment deploy = repositoryService.createDeployment().key("test").name("预审").addClasspathResource("bpmn/test.bpmn").deploy();
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploy.getId()).singleResult();
    String definitionId = processDefinition.getId();
    logger.info(String.format("预审流程ID=%s", definitionId));

    // 启动流程
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance processInstance = runtimeService.startProcessInstanceById(definitionId, "ys");

    logger.info(String.format("预审流程实例ID=%s", processInstance.getProcessInstanceId()));
}

2、得到流程实例id后我们完成窗口环节的任务,观察窗口后的环节会有几条任务?

@Test
public void completeTaskTest() {
    String processInstanceId = "75005";
    TaskService taskService = processEngine.getTaskService();
    Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
    logger.info(String.format("activityName=%s", task.getName()));
    taskService.complete(task.getId()); // 完成窗口环节任务

    List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list();
    taskList.forEach(taskItem -> {
        logger.info(String.format("activityName=%s,taskId=%s,assignee=%s", taskItem.getName(), taskItem.getId(), taskItem.getAssignee()));
    });
}

输出情况(窗口任务完成后,生成了3条任务)

Activiti7-进阶(SpringBoot 2.6版)_第21张图片

并行网关在业务应用中常用于会签任务,会签任务即多个参与者共同办理的任务。也就是说A部门会审、B部门会审、C部门会审任务全部完成后,才可以走到下一环节,可自行测试下,包括数据库中到底操作了那些表、那些数据。

6.3、包含网关InclusiveGateway

什么是包容网关?

包容网关(Inclusive Gateway)就是并行网关(Parallel Gateway)和排他网关(Exclusive Gateway)的组合。可以在出口顺序流上定义条件,包容网关会计算它们。然而主要的区别是,包容网关与并行网关一样,可以选择多于一条(出口)顺序流。

包容网关、并行网关和排他网关的异同:

同:

  1. 都有出口顺序流和入口顺序流。
  2. 至少有一个分支

异:

  1. 排他网关只有一条分支被执行,如果有多条符合条件的分支,流程会默认走第一条。并行网关至少有一条分支被执行,而且所有的分支都会被执行。包容网关有多条或者一条分支会被执行。

  2. 包容网关包括了并行网关和排他网关的所有功能。

使用场合:

不确定分支的情形下面。

例如:

审批部门领导根据不同的条件确定。

审批的会签根据不同的条件确定。

如下图包含网关内部是一个圆。
Activiti7-进阶(SpringBoot 2.6版)_第22张图片
包容网关两个重要的特性

分支(fork):所有出口顺序流都会被计算,对于计算为true的分支都会被执行。

聚合(join):所有到达包容网关的并行执行,都会在网关处等待,直到每一条具有流程标志的入口顺序流,都有一个执行到达。这是与并行网关的重要区别。换句话说,包容网关只会等待将会被执行的入口顺序流。在合并后,流程穿过合并并行网关继续。

包容网关测试:如下图,给定一个流程变量xs,包容网关的执行逻辑为xs < 100走A部门会审和B部门会审,xs > 100走B部门会审和C部门会审。
Activiti7-进阶(SpringBoot 2.6版)_第23张图片
1、流程定义

@Test
public void gatewayTest() {
    RepositoryService repositoryService = processEngine.getRepositoryService();
    // 预审流程定义
    Deployment deploy = repositoryService.createDeployment().key("test").name("预审").addClasspathResource("bpmn/test.bpmn").deploy();
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploy.getId()).singleResult();
    String definitionId = processDefinition.getId();
    logger.info(String.format("预审流程ID=%s", definitionId));
    // 设置流程变量
    Map<String, Object> variables = new HashMap<>();
    variables.put("xs", 99);

    // 启动流程
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance processInstance = runtimeService.startProcessInstanceById(definitionId, "ys", variables);

    logger.info(String.format("预审流程实例ID=%s", processInstance.getProcessInstanceId()));
}

2、完成任务(拿到流程实例id后先完成窗口环节任务,在完成余下环任务)

@Test
public void completeTaskTest() {
    String processInstanceId = "82505";
    TaskService taskService = processEngine.getTaskService();
    Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
    logger.info(String.format("activityName=%s", task.getName()));
    taskService.complete(task.getId()); // 完成窗口环节任务

    boolean isEnd = false;
    while (!isEnd) {
        List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).list();
        if (CollectionUtils.isEmpty(taskList)) {
            isEnd = true;
        } else {
            taskList.forEach(taskItem -> {
                logger.info(String.format("activityName=%s,taskId=%s,assignee=%s", taskItem.getName(), taskItem.getId(), taskItem.getAssignee()));
                taskService.complete(taskItem.getId());
            });
        }
    }
}

3、再来看当xs=99时,流程流转是否按照我们预想的一样。

Activiti7-进阶(SpringBoot 2.6版)_第24张图片

END

THANK YOU

END

你可能感兴趣的:(JavaEE,activity7)