Activiti7 工作流实战笔记 - 整合SpringBoot

2,378 阅读14分钟

最近工作中用到Activiti 工作流,自学了相关知识,记录如下 2024年3月3日更新:www.yuque.com/frtgv/xm/bg… 《activiti7 工作流整合SpringBoot》

1. 基础知识:

1.1 建模语言BPMN

  • BPM软件在企业中应用非常广泛,凡是有业 务流程的地方都可以使用BPM进行管理。比如企业人事办公管理、采购流程管理

  • BPMN是Business Process Model And Notation 业务流程模型和符号,就是用来描述业务流程的一种建模标准。使用BPMN可以快速定义业务流程。

  • 整个BPMN是用一组符号来描述业务流程中发生的各种事件的。BPMN通过在这些符号事件之间连线来描述一个完整的业务流程。

img

  • 而对于一个完整的BPMN图形流程,其实最终是通过XML进行描述的

    通常,会将BPMN流程最终保存为一个.bpmn的文件,然后可以使用文本编辑器打开进行查看。而图形与xml文件之间,会有专门的软件来进行转换。

1.2 表结构解读

activiti的表都以act_开头。第二个部分表示表的用途。用途也和服务的API对应

  1. ACT_RE_ : 'RE’表示repository(存储库)。 这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等)

  2. ACT_RU_: 'RU’表示runtime。 这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。 Activiti只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。 这样运行时表可以一直很小速度很快

  3. ACT_ID_ : 'ID’表示identity。 这些表包含身份信息,比如用户,组等等

  4. ACT_HI_: 'HI’表示history。 这些表包含历史数据,比如历史流程实例, 变量,任务等等

  5. ACT_GE_*: 通用数据, 用于不同场景下

1.3 activiti 工作流不支持分布式

原因是:在Activiti工作流的act_ge_property表中通常情况下有3条记录:

  1. next.dbid
  2. schema.history
  3. schema.version

其中next.dbid对应的值为数据库中当前最近一次增长后的最大记录id,每次增长的步长为2500,protected int idBlockSize = 2500; (在ProcessEngineConfiguration类中)

  • Activiti中所有的id(如:Task的id,Execution的id,ProcessInstance的id等)都是通过IdGenerator来生成的
  • IdGenerator的默认实现是
/**2  * @author Tom Baeyens3  */4 public class DbIdGenerator implements IdGenerator {5 6   protected int idBlockSize;7   protected long nextId = 0;8   protected long lastId = -1;9   
10   protected CommandExecutor commandExecutor;
11   protected CommandConfig commandConfig;
12   
13   public synchronized String getNextId() {
14     if (lastId<nextId) {
15       getNewBlock();
16     }
17     long _nextId = nextId++;
18     return Long.toString(_nextId);
19   }
20 
21   protected synchronized void getNewBlock() {
22     IdBlock idBlock = commandExecutor.execute(commandConfig, new GetNextIdBlockCmd(idBlockSize));
23     this.nextId = idBlock.getNextId();
24     this.lastId = idBlock.getLastId();
25   }

从上面的代码可以看出,获取下一个id的方法是加锁的,

也就是在一台服务器上id的增长是没有问题的,但是如果将Activiti部署在多台服务器上就会有两个问题

  1. 从代码的第17,18行可以看出id是本地自增,如果有多台服务器就会出现id相同的情况(由并发写造成的);
  2. 获取lastId的方法是操作同一个数据库的,会有问题,代码22中通过执行GetNextIdBlockCmd来获取数据库中的next.dbid的值,如果在多台服务器上由于一台服务器修改后,其他服务器无法知道

2. Activiti 入门

img

定义和实例的概念

  • 流程定义:ProcessDefinition

    • 类 Class : 离职工作流定义
  • 流程实例:ProcessInstance

    • 对象 Obj : 张三离职单流程

2.1. 加载最新的流程定义id

查询所有流程定义,依次放入map集合;

  • 注:这些定义是SpringBoot 读取文件夹,自动部署到数据库中的。
 /**
     * 加载最新的流程定义的id
     *
     * @see [相关类/方法](可选)
     * @since [产品/模块版本](可选)
     */
    private void findLastVersionProcessDefinition() {
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery().orderByProcessDefinitionVersion().asc().list();
        Map<String, ProcessDefinition> map = new LinkedHashMap<String, ProcessDefinition>();
        if (list != null && list.size() > 0) {
            for (ProcessDefinition pd : list) {
                map.put(pd.getKey(), pd);
            }
        }
        List<ProcessDefinition> pdList = new ArrayList<ProcessDefinition>(map.values());
        if (pdList != null && pdList.size() > 0) {
            for (ProcessDefinition pd : pdList) {
                System.out.println(pd.getName() + "  ----  " + pd.getId());
                InitializationBean.activitiMap.put(pd.getName(), pd.getId());
            }
        }
    }

2.2. 启动流程实例

根据流程定义id,启动流程实例(StartEvent)

			// 启动流程
            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(activitiProcdId).singleResult();
            Map<String, String> param = new HashMap<String, String>();
            param.put("departureCode", departureCode);
            ProcessInstance pi = formService.submitStartFormData(processDefinition.getId(), param);
            logHelper.LoggerSendINFO("离职申请:" + departureCode + " , 已提交,过程实例id: " + pi.getId());
            System.out.println("离职申请:" + departureCode + " , 已提交,过程实例id: " + pi.getId());
            return pi.getId();

2.3. 部门经理审批

结果标记字段:

拒绝离职: String departureRemark = "false";

同意离职:departureRemark = "true";

@Override
    public Boolean approvalTask(String departureCode, String procId, String departureRemark) {
        // 查询task
        Task task = this.queryTask(procId);
        if (null != task) {
            Map<String, String> properties = new HashMap<String, String>();
            properties.put("departureCode", departureCode);
            properties.put("departureRemark", departureRemark);
            formService.submitTaskFormData(task.getId(), properties);
            return true;
        }
        return false;
    }


/**
     * 根据流程实例id 查询任务节点
     *
     * @param procId 流程实例id
     * @return 任务节点
     */
    private Task queryTask(String procId) {
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(procId).list();
        return tasks.get(0);
    }

2.4.财务审批

		 // 查询task
        Task task = this.queryTask(procId);
        if (null != task) {
            Map<String, String> properties = new HashMap<String, String>();
            properties.put("departureCode", departureCode);
            formService.submitTaskFormData(task.getId(), properties);
            return true;
        }else{
            throw new RuntimeException("工作流找不到");
        }


/**
     * 根据流程实例id 查询任务节点
     *
     * @param procId 流程实例id
     * @return 任务节点
     */
    private Task queryTask(String procId) {
        List<Task> tasks = taskService.createTaskQuery().processInstanceId(procId).list();
        return tasks.get(0);
    }

2.5. 流程结束 EndEvent

3. Activiti 进阶

流程定义 ProcessDefinition 和流程实例 ProcessInstance是Activiti中非常重要的两个概念。

他们的关系其实类似于JAVA中类和对象的概念。

流程定义ProcessDefinition是以BPMN文件定义的一个工作流程,是一组工作规范;

流程实例ProcessInstance则是指一个具体的业务流程。例如某个员工发起一次请假,就会实例化一个请假的流程实例,并且每个不同的流程实例之间是互不影响的。

在后台的表结构中,有很多张表都包含了流程定义ProcessDefinetion和流程实例ProcessInstance的字段。流程定义的字段通常是PROC_DEF_ID,而流程实例的字段通常是PROC_INST_ID。

3.1 启动流程实例时,添加Businesskey

当我们去查看下startProcessInstanceByKey这个方法时,会看到这个方法有好几个

重载的实现方法,可以传一些不同的参数。其中几个重要的参数包括:

  • String processDefinitionKey:流程定义的唯一键 不能为空

  • String businessKey:每个线程实例上下文中关联的唯一键。这个也是我们这一章节要介绍的重点。

  • Map<String,Object> variables:在线程实例中传递的流程变量。这个流程变量可以在整个流程实例中使用,后面会介绍到。

  • String tenantId:租户ID,这是Activiti的多租户设计。相当于每个租户可以上来获取一个相对独立的运行环境。

3.2 挂起和激活 流程实例

  • 一种是将整个流程定义Process Definition挂起,这样,这个流程定义下的所有流程实例都将挂起,无法继续执行
  • 另一种方式是将某一个具体的流程实例挂起。

3.3 流程变量

流程变量的类型是Map<String,Object>

Map<String, String> param = new HashMap<String, String>();
            param.put("departureCode", departureCode);
            ProcessInstance pi = formService.submitStartFormData(processDefinition.getId(), param);

配置了这个流程后,可以配合流程变量使用。例如:执行完成后,可以在act_ru_variable表中看到刚才map中的数据

流程变量比业务关键字要强大很多。变量值不仅仅是字符串,也可以是POJO对象。但是当需要将一个POJO对象放入流程变量时,要注意这个对象必须要实现序列化接口

流程变量的作用域

变量的作用域可以设置为Global和Local两种

  • Global变量

这个是流程变量的默认作用域,表示是一个完整的流程实例。 Global变量中变量名不能重复。如果设置了相同的变量名,后面设置的值会直接覆盖前面设置的变量值。

  • Local 变量

Local变量的作用域只针对一个任务或一个执行实例的范围,没有流程实例大。

Local变量由于作用在不同的任务或不同的执行实例中,所以不同变量的作用域是互不影响的,变量名可以相同。Local变量名也可以和Global变量名相同,不会有影响。

使用流程变量

定义好流程变量后,就可以在整个流程定义中使用这些流程变量了。

设置Global流程变量

1) 启动流程时设置变量

2) 任务办理时设置变量

3) 通过当前流程实例设置

4) 通过当前任务设置

3.4 网关

网关是用来控制流程流向的重要组件,通常都会要结合流程变量来使用。

3.4.1 排他网关ExclusiveGateway(常用)

排他网关,用来在流程中实现决策。

当流程执行到这个网关,所有分支都会判断条件是否为true,如果为true则执行该分支

img

注意

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

  • 如果从网关出去的线所有条件都不满足则系统抛出异常。

3.4.2 并行网关ParallelGateway

并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:并行网关在业务应用中常用于会签任务,会签任务即多个参与者共同办理的任务

与其他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。

img

说明:此时会要求技术经理和项目经理都进行审批。而连线上的条件会被忽略

技术经理和项目经理是两个execution分支,在act_ru_execution表有两条记录分别是技术经理和项目经理,act_ru_execution还有一条记录表示该流程实例。

待技术经理和项目经理任务全部完成,在汇聚点汇聚,通过parallelGateway并行网关。

并行网关在业务应用中常用于会签任务,会签任务即多个参与者共同办理的任务。

3.4.3 包含网关InclusiveGateway

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

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

  • 分支: 所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。
  • 汇聚: 所有并行分支到达包含网关,会进入等待状态, 直到每个包含流程token的进入顺序流的分支都到达。

img

3.4.4 事件网关EventGateway

img

3.5 组任务分配

3.5.1 设置多个候选责任人

某个订单合同,需要找部门经理级别的负责人签字。而公司中有多个部门经理,业务上只需要找其中任意一个人完成审批就可以了。

这种场景下,我们就无法通过设置流程变量的方式来设置负责人。这时,就需要用到Activiti提供的另一个利器-任务候选人Candidate Users。

3.5.2 组任务办理流程

给任务分配了候选人后,后续就需要这些候选人主动认领自己的业务,然后进行处理。

  • 先查询任务
  • 再认领任务(也可以退还任务)
  • 最后完成任务

4. Activiti7与SpringBoot整合开发

Activiti与Spring整合的基本思想是将Activiti最为核心的ProcessEngine类交由Spring容器进行管理。

1.POM 依赖

        <dependency>
            <groupId>org.activiti</groupId>
            <artifactId>activiti-spring-boot-starter-basic</artifactId>
            <version>6.0.0</version>
        </dependency>

2.配置文件

spring:
	activiti:
    activityFontName: '宋体'
    labelFontName: '宋体'
    annotationFontName: '宋体'

    	# false:默认值。activiti在启动时,会对比数据库表中保存的版本。如果没有表或者版本不匹配,将抛出异常
      # true:activiti会对数据库中所有表进行更新操作,如果表不存在,则会自动创建
      # create_drop:在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)
      # drop-create:在activiti启动时删除原来的旧表,然后再创建新表(不需要手动关闭引擎)
    database-schema-update: true

		# 自动部署验证设置:true-开启(默认)、false-关闭
    check-process-definitions: true
  		# 
    async-executor-enabled: true
    job-executor-activate: true

    	# 自定义流程文件位置
    process-definition-location-prefix: classpath:/processes/

    	#记录历史等级 可配置的历史级别有none, activity, audit, full
    	#none:不保存任何的历史数据,因此,在流程执行过程中,这是最高效的。
    	#activity:级别高于none,保存流程实例与流程行为,其他数据不保存。
    	#audit:除activity级别会保存的数据外,还会保存全部的流程任务及其属性。audit为history的默认值。
    	#full:保存历史数据的最高级别,除了会保存audit级别的数据外,还会保存其他全部流程相关的细节数据,包括一些流程参数等。
    history-level: full

5. 专项补充:

Activiti 核心类

activiti工作流框架其实是一个半成品项目,所以它自带了25张数据库表,而且它还有service层,可以在我们ssm框架整合好oa系统后,就可以直接@Autowired注入到我们的controller或者service层就可以了。

img

RepositoryService-仓储服务

Activiti的资源管理类,提供了管理和控制流程发布包和流程定义的操作。

//仓储服务
@Autowired
private RepositoryService repositoryService;
  • 仓储服务可以用来部署我们的流程图,还可以创建我们的流程部署查询对象,用于查询刚刚部署的流程列表,便于我们管理流程,方法如下。

	//这个是部署流程的方法,流程图以inputStream流的形式传入	 
	DeploymentBuilder builder = repositoryService.createDeployment(); 
	builder.name(process.getName()); 
	builder.addInputStream(fileName, inputStream); 
	Deployment deployment = builder.deploy(); 
	//这个是流程部署列表查询的方法 
	DeploymentQuery deploymentQuery = repositoryService.createDeploymentQuery(); 
	//可以根据很多条件查询,我这是根据部署名称模糊查询 
	List<Deployment> list = deploymentQuery.deploymentNameLike("%"+name+"%")
	

RuntimeService-activiti运行时服务

Activiti的流程运行管理类。可以从这个服务类中获取很多关于流程执行相关的信息

	//运行时服务 
	@Autowired 
	private RuntimeService runtimeService;

运行时服务主要用来开启流程实例,一个流程实例对应多个任务,也就是多个流程节点,好比如请假审批是一个流程实例,部门主管,部门经理,总经理都是节点,我们开启服务是通过流程定义key或者流程定义id来开启的,方法如下:

	//首先根据部署id创建流程定义 
	ProcessDefinition def = repositoryService.createProcessDefinitionQuery().deploymentId(form.getDeployId()).singleResult(); 
	
	//然后根据流程定义id或者key开启流程实例	 
	ProcessInstance proInst = runtimeService.startProcessInstanceById(def.getId());
	

备注:当我们用仓储服务部署了流程图之后,就会产生一个流程部署id,一个流程部署id对应一个流程定义,一个流程定义对应多个流程实例,一个流程实例对应多个任务节点,

这样的逻辑应该明白吧,打个比方就是我设计了一个手机图纸(流程定义),是可以供N多个人生产出手机并去使用的,这些人就是流程实例,手机里面的各种功能就是任务节点。

TaskService-任务服务

	//任务服务 
	@Autowired 
	private TaskService taskService;

任务服务是用来可以用来领取,完成,查询任务列表功能的,使用方法分别如下:

//根据任务id和用户领取任务 
taskService.claim(String taskId, String userId) 
//根据任务id完成自己节点的任务 
taskService.complete(String taskId) 
//创建任务查询对象之后根据候选人也就是任务处理人查询自己的任务列表 
taskService.createTaskQuery().taskAssignee(String assignee)

HistoryService-历史服务

	//历史服务 
	@Autowired 
	private HistoryService historyService;

历史服务可以查看审批人曾经审批完成了哪些项目,审批项目总共花了多少时间,以及在哪个环节比较耗费时间等等,便于审批人查看历史信息,方法如下。

	//根据审批人查看该审批人审批了哪些项目 
	List<HistoricTaskInstance> = historyService.createHistoricTaskInstanceQuery().
	taskAssignee(String assignee).finished().list();

历史任务对象HistoricTaskInstance,它里面封装了任务开始时间,结束时间,该节点花费的时间等等信息。

FormService-表单服务

	//表单服务
	@Autowired
	private FormService formService;

IdentityService-实体服务

主要是操作用户信息,用户分组信息等,组信息包括如部门表和职位表,我一般都是自己建表来存储用户信息和组信息的

	//实体服务
	@Autowired
	private IdentityService identityService;
- - - - ```





## Activiti 监听器的使用

有位网友写的挺好的,我就不赘述了,点击查看

https://blog.csdn.net/qq_30739519/article/details/51258447


--------------------------------------------
**如有帮助,动动小手点个赞!**