Activiti实战

一、简介

1.1 Activiti

Activiti是一个开源的、易扩展的业务流程管理框架,采用BPMN2.0标准定义流程。

1.2 版本选取

版本 特点
5.x 中文资料最多
6.x 对5.x进行了代码重构,没有什么重大革新和bug修复
7.x 支持spring cloud

二、BPMN

2.1 概述

业务流程建模与标注(business process modeling notation),使用BPMN可以定义一个业务流程图。

Activiti支持BPMN2.0标准并加入了一些自定义的属性。

2.2 流程定义

下面是一个最简单的流程定义。

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" 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/processdef">
  <process id="leave" name="请假流程" isExecutable="true">
    <startEvent id="start1" name="开始"></startEvent>
      <userTask id="task1" name="请假" activiti:candidateGroups="employee" activiti:formKey="create"></userTask>
    <endEvent id="end1" name="结束"></endEvent>
    <sequenceFlow id="flow1" sourceRef="start1" targetRef="task1"></sequenceFlow>
    <sequenceFlow id="flow2" sourceRef="task1" targetRef="end1"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_planOrder">
    <bpmndi:BPMNPlane bpmnElement="leave" id="BPMNPlane_planOrder">
      <bpmndi:BPMNShape bpmnElement="start1" id="BPMNShape_start1">
        <omgdc:Bounds height="30.0" width="30.0" x="90.0" y="75.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="task1" id="BPMNShape_task1">
        <omgdc:Bounds height="80.0" width="100.0" x="225.0" y="50.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="end1" id="BPMNShape_end1">
        <omgdc:Bounds height="28.0" width="28.0" x="435.0" y="76.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
        <omgdi:waypoint x="120.0" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="225.0" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
        <omgdi:waypoint x="325.0" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="435.0" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

定义分成流程和流程图两个部分,根据定义中的流程图定义可以生成流程图如下。

leave

2.3 基本元素

  • task,节点任务,包含:用户任务、脚本任务、邮件任务、业务规则任务等
  • sequence flow,流,也就是连接线
  • gateway,网关,如排他网关、并行网关、包容网关、事件网关
  • event,事件,如启动、结束
  • listener,监听器

2.4 绘制工具

  • eclipse插件
  • idea插件
  • modeler,activiti内置的web端绘制流程定义的方式
  • 手写xml

三、数据库

3.1 表结构概述

表类别 描述
act_hi_* history,完整的流程信息
act_id_* identity,内置的用户及角色组
act_ge_* general,基本信息
act_ru_* runtime,运行时流程数据
act_re_* repository,流程定义数据

3.2 流程与表的关系

  • deploy

流程部署成功后,打包上传的 bpmnpng 文件属于静态资源文件,保存在 act_ge_bytearray 中;

除了资源文件外,生成的部署信息和流程定义分别保存在 act_re_deploymentact_re_procdef 中。

  • model

流程定义的一种形式,以 model 形式保存流程,信息保存在表 act_re_modelact_ge_bytearray 中;

model 流程的部署同 bpmn 流程, 生成 bpmnpng 文件保存在 act_ge_bytearray 中。

  • processInstance

流程每次启动都会生成一个流程实例,保存在 act_hi_procint 表中, proc_inst_id_ 是流程实例的 id

  • execution

execution 指一个流程实例要执行的过程对象,保存在 act_ru_execution 表中;

execution 表的 id_ 字段通常与 proc_inst_id_ 字段一致,不一致时代表该 execution 是子流程。

  • task

流程图中的每个节点都是一个任务,上一个任务完成后,下一个任务就会被添加到 act_hi_taskinstact_ru_task 表中。

  • variable

执行任务过程中产生的变量信息(也包含动态表单的信息)存储在 act_ru_variableact_hi_varinst 表中;

任务完成后,历史变量信息会被存储在 act_hi_detail 表中,该表存储哪些信息由配置中的 HistoryLevel 决定。

  • user

任务的执行者信息存储在 act_ru_identitylinkact_hi_identitylink 表中

四、API文档

4.1 引入项目

以spring boot工程为例,在pom文件中添加如下依赖即可。

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-engine</artifactId>
    <version>5.22.0</version>
</dependency>
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter-basic</artifactId>
    <version>5.22.0</version>
</dependency>
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-bpmn-model</artifactId>
    <version>5.22.0</version>
</dependency>

4.2 API

模块 描述
HistoryService 历史数据操作接口
IdentityService 组织架构操作接口
FormService 任务表单管理
RepositoryService 管理流程定义
RuntimeService 运行时操作
TaskService 任务管理
ManagementService 流程的管理和维护,控制台操作

4.3 常用方法

1.查询待办任务
 List<Task> taskUser = taskService.createTaskQuery()
                .processDefinitionKey(procInstId)
                .taskCandidateOrAssigned(userId)
                .list();
2.查询历史任务
 HistoricTaskInstance task = historyService.createHistoricTaskInstanceQuery()
                .taskDefinitionKey(taskDefKey)
                .processInstanceId(procInstId)
                .singleResult();
3.查询流程实例
HistoricProcessInstance processInstance = historyService
                .createHistoricProcessInstanceQuery()
                .processInstanceId(procInstId)
                .singleResult();
4.部署流程定义

a.zip

repositoryService.createDeployment()
                .name(deployFile.getOriginalFilename())
                .addZipInputStream(new ZipInputStream(deployFile.getInputStream()))
                .deploy();

b.classpath

repositoryService.createDeployment()
                .name(deployName)
                .addClasspathResource("processes/" + fileName + ".bpmn20.xml")
                .addClasspathResource("processes/" + fileName + ".png")
                .deploy();
5.启动流程实例
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(procKey);
6.删除流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
                        .processInstanceId(pi.getId()).singleResult();
if (processInstance == null) {
    historyService.deleteHistoricProcessInstance(pi.getId());
} else {
    runtimeService.deleteProcessInstance(pi.getId() "");
    historyService.deleteHistoricProcessInstance(pi.getId());
}

五、常用扩展

5.1 自定义表单

在Activiti中,每个节点都可以定义不一样的表单内容。

动态表单是Activiti自带的一种自定义表单的方式,以key-value的形式定义表单,表单以每行一条属性的方式显示。

<userTask activiti:candidateGroups="emp" activiti:exclusive="true" id="usertask6" name="员工发起请假申请">
        <extensionElements>
            <activiti:formProperty datePattern="yyyy-MM-dd" id="startTime" name="开始时间" type="date"/>
            <activiti:formProperty datePattern="yyyy-MM-dd" id="endTime" name="结束时间" type="date"/>
            <activiti:formProperty id="restReason" name="请假原因" type="string"/>
        </extensionElements>
</userTask>

由于该方式不能满足大部分web应用的需求,Activiti还提供了外置表单。

所谓外置表单,就是通过Task中定义的formKey加载不同的表单模板,从而实现复杂的布局、交互、结构需求。

这里的模板一般指jsp或者thymeleaf这类模板引擎。

<userTask activiti:candidateGroups="manger" activiti:exclusive="true" activiti:formKey="audit.jsp" id="usertask1" name="审批"/>

通过formService获取formKey进行处理,formKey一般来说是路径+名称+后缀。

5.2 动态指派

动态指派的简单实现有两种方式,一种是定义任务监听器,另一种是在流程实例中注入变量。

1.TaskListener

首先实现TaskListenerImpl接口:

public class TaskListenerImpl implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        delegateTask.deleteCandidateGroup("manager");
        delegateTask.addCandidateGroup("president");
    }
}

然后在流程定义中引用该实现:

<userTask activiti:candidateGroups="leader" activiti:exclusive="true" activiti:formKey="audit.jsp" id="usertask1" name="项目组长审批">
        <extensionElements>
            <activiti:taskListener event="create" class="com.xxx.TaskListenerImpl"/>
        </extensionElements>
</userTask>

除了节点监听器,还有全局监听器(ExecutionListener)。

2.注入变量
<userTask id="hrAudit" name="人事审批" activiti:assignee="${hrUserId}" />

在执行该任务前,需要在流程中设置该变量。

Map<String Object> variables = new HashMap<String Object>();
variables.put("hrUserId" hrUserId);
taskService.complete(taskId variables);

其实监听器也可以结合spring的注解进行注入。

5.3 组织架构

Activiti内置了一套组织架构管理,结构较为简单,不符合应用场景。

介绍
act_id_group 角色
act_id_info 用户扩展信息
act_id_membership 用户与角色的关联表
act_id_user 用户基本信息

自定义组织架构的实现参考了博客:重构用户组数据的多种方案比较

原理

自定义 sessionFactory 替换原生的数据源模块。

实现

1.配置 activiti 使用自定义 sessionFactory

@Configuration
@AutoConfigureBefore(org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class)
public class ActivitiConfiguration implements ProcessEngineConfigurationConfigurer {

    @Resource(name = "customGroupEntityFactory")
    private SessionFactory groupManagerFactory;

    @Resource(name = "customUserEntityFactory")
    private SessionFactory userManagerFactory;

    @Override
    public void configure(SpringProcessEngineConfiguration pec) {
        pec.setDbIdentityUsed(false);
        List<SessionFactory> customSessionFactories = new ArrayList<>();
        customSessionFactories.add(userManagerFactory);
        customSessionFactories.add(groupManagerFactory);
        if (pec.getCustomSessionFactories() == null) {
            pec.setCustomSessionFactories(customSessionFactories);
        } else {
            pec.getCustomSessionFactories().addAll(customSessionFactories);
        }
    }
}

2.自定义 UserManagerFactoryGroupManagerFactory 实现 SessionFactory 接口。

@Service
public class CustomUserEntityFactory implements SessionFactory{
    private CustomUserEntityManager customUserEntityManager;

    @Autowired
    public void setUserEntityManager(CustomUserEntityManager customUserEntityManager) {
        this.customUserEntityManager = customUserEntityManager;
    }

    @Override
    public Class<?> getSessionType() {
        return UserIdentityManager.class;
    }

    @Override
    public Session openSession() {
        return customUserEntityManager;
    }
}

3.继承 activiti 定义的 UserEntityManagerGroupEntityManager , 重写主要方法。

替换后可删除表 act_id_* ,该方案在使用的时候需要避免调用未重写的方法。

@Service
public class CustomUserEntityManager extends UserEntityManager {

    @Resource
    private OemUserRepo oemUserRepo;
    @Resource
    private CategoryRepo categoryRepo;

    @Override
    public User createNewUser(String userId) {
        return new UserEntity(userId);
    }

    @Override
    public void updateUser(User updatedUser) {
        throw new CustomEntityManagerException(CANNOT_USE_FUNC);
    }

    @Override
    public UserEntity findUserById(String userId) {
        return ActivitiUtil.toActivitiUser(oemUserRepo.getOne(Integer.parseInt(userId)));
    }

    @Override
    public void deleteUser(String userId) {
        throw new CustomEntityManagerException(CANNOT_USE_FUNC);
    }

    @Override
    public List<Group> findGroupsByUser(String userId) {
        List<Group> list = new ArrayList<>();
        // list by customize service
        return list;
    }
    // need override all function
}   

5.4 流程图及标识

通过BPMN绘制工具获取的流程定义,除了xml文件,还可以生成图片。

部署后可通过流程实例查询到流程图。

InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, diagramResourceName);

配合流程定义中的位置信息,可以实现当前节点定位。

ProcessInstance pi = runtimeService.createProcessInstanceQuery()
        .processInstanceId(processInstanceId)
        .singleResult();
ActivityImpl activityImpl = processDefinitionEntity.findActivity(pi.getActivityId());
modelView.addObject("x", activityImpl.getX());
modelView.addObject("y", activityImpl.getY());
modelView.addObject("width", activityImpl.getWidth());
modelView.addObject("height", activityImpl.getHeight());

5.5 回退的实现

1. 增加中转节点

createOrder

实际上不增加中转节点直接指向需要回退的节点也没有什么问题。

back

2. 自定义扩展

实现思路:

获取当前活动实例,清空当前节点的下一步,获取当前节点的上一步,设置当前节点的下一步为上一步。

自定义扩展可以实现回退到指定节点

5.6 指派与委托

当执行人不能执行当前任务时,可委托给他人执行。task实例有两个字段ASSIGNEE_OWNER_来保存这两个值。

可以定义在流程定义中。

<userTask id="usertask2" name="审批" activiti:assignee="manager" activiti:formKey="hello.jsp"></userTask>

也可以在运行时设置。

Task task=taskService.createTaskQuery().singleResult();
// set assignee
taskService.claim(task.getId(), "billy");
// change assignee and set owner
taskService.delegateTask(task.getId(), "cc");

5.7 流程挂起

需要暂停流程而不是删除流程时,可以对流程实例或定义进行挂起,挂起的流程可以重新激活。

挂起的流程实例不可以继续执行,挂起的流程定义不可以新增流程实例。

// suspend process instance 
ProcessInstance pi = runtimeService.createProcessInstanceQuery()
    .processInstanceId(procInstId).singleResult();
    if (!pi.isSuspended()) {
        runtimeService.suspendProcessInstanceById(procInstId);
    }
// active process instance
if (pi.isSuspended()) {
    runtimeService.activateProcessInstanceById(procInstId);
}
// suspend process definition
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
        .processDefinitionId(procDefId)
        .singleResult();
if (!processDefinition.isSuspended()) {
    repositoryService.suspendProcessDefinitionById(procDefId);
}
// activie process definition
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
        .processDefinitionId(procDefId)
        .singleResult();
if (processDefinition.isSuspended()) {
    repositoryService.activateProcessDefinitionById(procDefId, true, null);
}

挂起与激活实际上是变更了字段SUSPENSION_STATE_,在源码中可以看到该值的定义。

SuspensionState ACTIVE = new SuspensionState.SuspensionStateImpl(1, "active");
SuspensionState SUSPENDED = new SuspensionState.SuspensionStateImpl(2, "suspended");

5.8 其他

通过代码动态定义流程,全局事件处理,邮件服务,定时任务,规则任务,租户

六、modeler

Activiti官方demo中提供了web端绘制流程定义的方式,只要将该部分代码剥离拷贝至自己的项目中稍作修改即可,绘制页面有汉化包。

参考博客:https://blog.csdn.net/h1059141989/article/details/79870043

最终页面展示:

modeler

绘制的model可以先进行校验。

Model modelData = repositoryService.getModel(modelId);
ObjectNode modelNode = (ObjectNode) new ObjectMapper().readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
ProcessValidatorFactory processValidatorFactory = new ProcessValidatorFactory();
ProcessValidator defaultProcessValidator = processValidatorFactory.createDefaultProcessValidator();
List<ValidationError> validate = defaultProcessValidator.validate(model);

model可以直接部署,也可以导出xml或png部署静态资源文件。

// deploy
Model modelData = repositoryService.getModel(modelId);
ObjectNode modelNode = (ObjectNode) new ObjectMapper().readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
String processName = modelData.getName() + ".bpmn20.xml";
repositoryService.createDeployment().name(modelData.getName()).addBpmnModel(processName, model).deploy();
// export xml
Model modelData = repositoryService.getModel(modelId);
BpmnJsonConverter jsonConverter = new BpmnJsonConverter();
JsonNode editorNode = new ObjectMapper().readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnModel bpmnModel = jsonConverter.convertToBpmnModel(editorNode);
BpmnXMLConverter xmlConverter = new BpmnXMLConverter();
byte[] bpmnBytes = xmlConverter.convertToXML(bpmnModel);
ByteArrayInputStream in = new ByteArrayInputStream(bpmnBytes);
IOUtils.copy(in, response.getOutputStream());
String filename = bpmnModel.getMainProcess().getId() + ".bpmn20.xml";
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.flushBuffer();
// export png
Model modelData = repositoryService.getModel(modelId);
ObjectNode modelNode;
try {
    modelNode = (ObjectNode) new ObjectMapper()
            .readTree(repositoryService.getModelEditorSource(modelData.getId()));
    BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
    ProcessDiagramGenerator processDiagramGenerator = new DefaultProcessDiagramGenerator();
    ProcessEngineConfiguration processEngineConfiguration = ProcessEngines.getDefaultProcessEngine().getProcessEngineConfiguration();
    InputStream inputStream = processDiagramGenerator.generateDiagram(model, "png",
            processEngineConfiguration.getActivityFontName(),
            processEngineConfiguration.getLabelFontName(),
            processEngineConfiguration.getAnnotationFontName(),
            processEngineConfiguration.getClassLoader(), 1.0);
    OutputStream out = response.getOutputStream();
    for (int b = -1; (b = inputStream.read()) != -1; ) {
        out.write(b);
    }
    out.close();
    inputStream.close();
} catch (IOException e) {
    // logger
}

七、附录

咖啡兔官网API