一篇文章教会你使用 Activiti7 工作流

1,019 阅读7分钟

学会一个框架前,第一步就是安装依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M6</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.activiti.dependencies</groupId>
    <artifactId>activiti-dependencies</artifactId>
    <version>7.1.0.M6</version>
    <type>pom</type>
</dependency>

工作流的组成部分是:流程定义(图的XML),流程实例(相当于是一个图纸使用多次)

那么第一步,获取流程定义列表

流程定义

首先在activiti 7的数据库中,是这个表ACT_RE_PROCDEF存放流程定义数据的。大家可以自行查看,我这里说一些重要的字段。

// 需要注入
private final RepositoryService repositoryService;

// 起始页
int firstResult = (processDefinitionDto.getCurrent() - 1) * processDefinitionDto.getPageSize();
// 用于执行查询流程定义
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();

// 分页获取流程定义数据
List<ProcessDefinition> list = processDefinitionQuery.listPage(firstResult, processDefinitionDto.getPageSize());
// 获取流程定义的总数
long count = processDefinitionQuery.count();

// 那么不分页获取数据怎么实现呢?
// List<ProcessDefinition> list2 = processDefinitionQuery.list();

流程定义实体,这里我说一下经常使用的。

public interface ProcessDefinition {

  /** 流程定义名称 */
  String getName();

  /** 唯一Key */
  String getKey();

  /** 版本号 */
  int getVersion();

  /** 文件名  */
  String getResourceName();

  /** 流程定义ID */
  String getDeploymentId();
 
}

功能一:实现上传 BPMN 文件

什么是 BPMN呢,它的作用是描述和表示业务流程,而 Activiti7 加载的业务图就是 BPMN 文件,一个流程定义其实就是一个表示业务工作图的文件。

上传方式呢:一般是,zip压缩包或者直接是 BPMN 文件。

// 需要注入
private final RepositoryService repositoryService;

public boolean uploadBpmn(MultipartFile file) {
    // 获取文件名
    String filename = file.getOriginalFilename();
    // 这里加上时间
    String name = filename + "---" + System.currentTimeMillis();
    // 获取文件扩展名
    String extension = FilenameUtils.getExtension(filename);
    // 获取文件流
    try (InputStream inputStream = file.getInputStream()) {
        if ("zip".equalsIgnoreCase(extension)) {
            try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
                repositoryService.createDeployment().addZipInputStream(zipInputStream).name(name).deploy();
            }
        } else {
            repositoryService.createDeployment().addInputStream(filename, inputStream).name(name).deploy();
        }
    } catch (IOException e) {
        throw new RuntimeException("文件上传失败:" + e);
    }
}

功能二:在线绘制流程定义

这里前端部分,可以看我其他文章,这里不解释。

前端呢,需要传递一个 XML 和一个 name,XML呢,其实就是 BPMN 文件的内容了。程序其实相对简单哈。

// 需要注入
private final RepositoryService repositoryService;

public boolean saveDefinition(String xml, String name) {
    repositoryService.createDeployment().addString("createWithBpmnJs-" + name + ".bpmn", xml)
            .name(name).deploy();
    return true;
}

功能三:用户点击查看按钮,查看流程定义图

前端部分已经省略,用户请求该流程定义的 XML数据,将XML传递给前端 bpmnjs 进行渲染。

这里之所以选择请求,而不是将xml放到咱们获取流程定义的请求里,是因为 xml 的数据量很大,这样会有很多冗余的数据,并且性能也有受损。

这里需要传递两个参数,一个是流程定义的ID,另一个是文件名

// 需要注入
private final RepositoryService repositoryService;

public String getXml(String deploymentId, String resourceName) {
    // 获取流程定义的资源文件,返回的是一个 InputStream 对象,该流中的数据是XML数据
    try (InputStream resourceAsStream = repositoryService.getResourceAsStream(deploymentId, resourceName)) {
        // 将 InputStream 转换为字符串,编码方式是 UTF-8
        return IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8);
    } catch (IOException e) {
        throw new RuntimeException("获取流程定义失败", e);
    }
}

功能四:启动流程实例 / 删除流程定义

首先我们说启动流程实例吧,需要两个参数,这里的 key 不是流程定义的ID,注意下,名称可以随便写。

// 需要注入
private final ProcessRuntime processRuntime;

public ProcessInstance startInstance(String processDefinitionKey, String name) {
    // 启动流程实例
    StartProcessPayload build = ProcessPayloadBuilder.start()
            // 流程key
            .withProcessDefinitionKey(processDefinitionKey)
            // 启动名称
            .withName(name)
//                .withVariable("key", "value") // 不需要可以去除
//                .withBusinessKey("自定义的参数‘") // 不需要可以去除
            .build();
    return processRuntime.start(build);
}

删除流程定义,根据流程定义ID

// 需要注入
private final RepositoryService repositoryService;

public boolean deleteDefinitionById(String processDefinitionId) {
    // true 表示所有的历史都删掉
    repositoryService.deleteDeployment(processDefinitionId, true);
    return true;
}

说到现在,流程定义基本上是讲完了。

流程实例

先来看一下实体类,说了一些关键的字段。

public interface ProcessInstance extends ApplicationElement {

    // 流程实例的ID
    String getId();

    // 名称
    String getName();

    // 启动时间
    Date getStartDate();

    // 当前状态
    ProcessInstanceStatus getStatus();

    // 流程定义ID
    String getProcessDefinitionId();

    // 流程定义Key
    String getProcessDefinitionKey();

    // 流程定义版本
    Integer getProcessDefinitionVersion();
}

功能一:分页查询流程实例列表

// 需要注入
private final ProcessRuntime processRuntime;

// 传入分页参数,第 1 页,每页 10 条
Pageable pageable = Pageable.of(0, 10);
// 仅返回正在进行的或已暂停的流程实例
Page<ProcessInstance> processInstancePage = processRuntime.processInstances(pageable);
// 获取内容
List<ProcessInstance> content = processInstancePage.getContent();
// 获取流程实例的总数
long totalItems = processInstancePage.getTotalItems();

功能二:挂起 / 激活流程实例

业务我来说一下,前端传递流程实例的ID,通过 processInstance 方法查询该流程实例,判断当前的状态,如果是运行中,执行挂起,如果是暂停状态,执行激活。

// 需要注入
private final ProcessRuntime processRuntime;

public boolean updateStatus(String id) {
    ProcessInstance processInstance = processRuntime.processInstance(id);
    // 根据Id查询出来流程实例
    if (processInstance.getStatus().equals(ProcessInstance.ProcessInstanceStatus.RUNNING)) {
        // 挂起
        SuspendProcessPayload build = ProcessPayloadBuilder.suspend().withProcessInstanceId(id).build();
        processRuntime.suspend(build);
    } else if (processInstance.getStatus().equals(ProcessInstance.ProcessInstanceStatus.SUSPENDED)) {
        // 激活
        ResumeProcessPayload build = ProcessPayloadBuilder.resume().withProcessInstanceId(id).build();
        processRuntime.resume(build);
    }
    return true;
}

功能三: 删除流程实例

// 需要注入
private final ProcessRuntime processRuntime;

public boolean deleteById(String instanceId) {
    // 删除流程实例
    DeleteProcessPayload build = ProcessPayloadBuilder.delete()
        .withProcessInstanceId(instanceId).build();
    processRuntime.delete(build);
    return true;
}

功能四:获取全部流程实例

// 需要注入
private final RuntimeService runtimeService;

List<ProcessInstance> list = runtimeService.createProcessInstanceQuery().list();

功能五:查看当前实例执行进度

这里还说比较复杂的,有一些业务逻辑需要梳理。

首先,用户传递 实例ID,查询该实例的进度。我们需要做的是什么?

  1. 用灰色显示已经执行过的,节点标注灰色
  2. 如果该节点是自己执行的,连接线标注绿色
  3. 如果该节点是正在执行的,节点标注黄色

理解了需求,现在看一些 API

根据实例ID查询流程实例ID历史记录

HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery()
        // 根据流程实例ID查询
        .processInstanceId(processInstanceId)
        // 唯一
        .singleResult();

根据流程实例获取活动列表

List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
        // 根据实例ID查询
        .processInstanceId(processInstanceId)
        // 根据执行人查询
        .taskAssignee("执行人")
        .list();

整体执行逻辑:

  1. 根据流程实例ID,获取流程定义KEY
  2. 根据流程定义KEY获取 BPMN xml 结构
  3. 根据流程实例ID获取活动节点
  4. 查询出自己的活动节点
  5. 遍历节点忽略掉 开始和结束节点
  6. 判断是否是连接线,如果是,在自己的活动节点中查询,存在则记录。
  7. 判断是节点,如果是,判断结束时间,第一个结束时间是null的表示是当前活动,存在的标注为已执行,其他的标注为未执行。
public List<InstanceHistory> highlightProcessHistory(String processInstanceId) {
    // 查询流程实例
    HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery()
            // 根据流程实例ID查询
            .processInstanceId(processInstanceId)
            // 唯一
            .singleResult();
    // 获取bpmnModel对象
    BpmnModel bpmnModel = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId());
    // 因为我们这里只定义了一个Process 所以获取集合中的第一个即可
    Process process = bpmnModel.getProcesses().get(0);
    //获取所有的FlowElement信息
    ArrayList<FlowElement> flowElements = (ArrayList<FlowElement>) process.getFlowElements();

    //获取流程实例 历史节点(全部)
    Map<String, HistoricActivityInstance> collect = historyService.createHistoricActivityInstanceQuery()
            .processInstanceId(processInstanceId).list().stream()
            .collect(Collectors.toMap(HistoricActivityInstance::getActivityId, Function.identity()));
    // 查询执行人是我自己的,历史任务
    List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
            .processInstanceId(processInstanceId).taskAssignee(SecurityUtils.getUsername()).list();

    boolean flag = false;
    ArrayList<InstanceHistory> instanceHistories = new ArrayList<>();
    // 执行过的(灰色),我执行的,当前执行的
    for (FlowElement flowElement : flowElements) {
        // 排除起始节点和结束节点
        if (flowElement instanceof StartEvent || flowElement instanceof EndEvent) {
            continue;
        }
        String id = flowElement.getId();
        String name = flowElement.getName();
        if (flowElement instanceof SequenceFlow) {
            String sourceRef = ((SequenceFlow) flowElement).getSourceRef();
            HistoricActivityInstance historicActivityInstance = list.stream().filter(taskInstance ->
                    taskInstance.getActivityId().equals(sourceRef)).findFirst().orElse(null);
            // 表示是我执行的并且已经执行过
            if (historicActivityInstance != null && historicActivityInstance.getEndTime() != null) {
                InstanceHistory e = new InstanceHistory(id, name, 0, true, 0);
                instanceHistories.add(e);
            }
            continue;
        }
        HistoricActivityInstance activiti = collect.get(flowElement.getId());
        // 第一个没有结束时间的是正在执行的 否则未执行
        int isExecuted = 0;
        if (activiti.getEndTime() == null) {
            isExecuted = flag ? 1 : 2;
            flag = true;
        }
        InstanceHistory e = new InstanceHistory(id, name, isExecuted,
                Objects.equals(activiti.getAssignee(), SecurityUtils.getUsername()), 1);
        instanceHistories.add(e);
    }
    return instanceHistories;
}

最终呢,结果就是这样的

public class InstanceHistory {

    private String id;
    private String name;
    // 是否已经执行过了. 0 已经执行了,1 未执行,2 当前执行
    private Integer isExecuted;
    // 是否是当前用户执行的
    private Boolean isMyTask;
    // 类型:线、节点 , 0 线 1 节点
    private Integer type;

}

image.png

任务

实体类,一些关键的字段。

public interface Task extends ApplicationElement {

    // 任务ID
    String getId();

    // 待办人,如果为空表示,候选人
    String getAssignee();

    // 名称
    String getName();
    
    // 描述
    String getDescription();

    // 创建时间
    Date getCreatedDate();

    // 流程定义ID
    String getProcessDefinitionId();

    // 流程实例ID
    String getProcessInstanceId();

    // 状态
    TaskStatus getStatus();

    // 完成时间
    Date getCompletedDate();

}

功能一:获取我的任务

默认是分页的,没有提供获取全部的任务方法。

// 获取任务,第1页的10条数据
Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
// 获取任务
List<Task> content = tasks.getContent();
// 总条数
int totalItems = tasks.getTotalItems();

功能二:完成任务 / 拾取任务

这里我就做到一块了,因为有些情况下,拾取任务在进行完成,其实没必要的。

前面我们说过了,如果待办人是空,其实是候选人。

// 需要注入
private final TaskRuntime taskRuntime;

public Task completeTask(String taskId) {
    // 查询任务
    Task task = taskRuntime.task(taskId);
    if (task.getAssignee() == null) {
        // 不需要先拾取任务,再执行(表示是候选人)
        ClaimTaskPayload build = TaskPayloadBuilder.claim().withTaskId(taskId).build();
        // 拾取任务
        taskRuntime.claim(build);
    }
    // 完成任务
    CompleteTaskPayload build = TaskPayloadBuilder.complete().withTaskId(taskId)
            // 可以完成时加参数
//                .withVariables()
            .build();
    // 执行完成
    return taskRuntime.complete(build);
}

项目暂时到这里啦,后续继续补充

后续会补充监听器、超时提醒等