一文快速入门工作流框架Activiti7工作流概述 工作流(Workflow),就是通过计算机对业务流程自动化执行管理。
工作流概述
工作流(Workflow),就是通过计算机对业务流程自动化执行管理。它主要解决的是:在多个参与者之间按照某种预定义的规则自动进行传递文档、信息或任务,从而实现某个预期的业务目标,或者促使此目标的实现。
一个软件系统中具有工作流的功能,我们把它称为工作流系统,一个系统中工作流的功能是什么?就是对系统的业务流程进行自动化管理,所以工作流是建立在业务流程的基础上,所以一个软件的系统核心根本上还是系统的业务流程,工作流只是协助进行业务流程管理。即使没有工作流业务系统也可以开发运行,只不过有了工作流可以更好的管理业务流程,提高系统的可扩展性。
适用的行业
消费品行业,制造业,电信服务业,银证险等金融服务业,物流服务业,物业服务业,物业管理,大中型进出口贸易公司,政府事业机构,研究院所及教育服务业等,特别是大的跨国企业和集团公司。
具体应用
- 关键业务流程:订单、报价处理、合同审核、客户电话处理、供应链管理等。
- 行政管理类:出差申请、加班申请、请假申请、用车申请、各种办公用品申请、购买申请、日报周报等凡是原来手工流转处理的行政表单。
- 人事管理类:员工培训安排、绩效考评、职位变动处理、员工档案信息管理等。
- 财务相关类:付款请求、应收款处理、日常报销处理、出差报销、预算和计划申请等。
- 客户服务类:客户信息管理、客户投诉、请求处理、售后服务管理等。
- 特殊服务类:ISO 系列对应流程、质量管理对应流程、产品数据信息管理、贸易公司报关处理、物流公司货物跟踪处理等各种通过表单逐步手工流转完成的任务均可应用工作流软件自动规范地实施。
Activiti基础
简介
Alfresco 软件在 2010 年 5 月 17 日宣布 Activiti 业务流程管理(BPM)开源项目的正式启动,其首席架构师由业务流程管理 BPM 的专家 Tom Baeyens 担任,Tom Baeyens 就是原来 JBPM 的架构师,而 JBPM 是一个非常有名的工作流引擎,当然 Activiti 也是一个工作流引擎。
Activiti 是一个工作流引擎,Activiti 可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0 进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由 Activiti 进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。
官方网站:www.activiti.org/
BPM
BPM(Business Process Management),即业务流程管理,是一种规范化的构造端到端的业务流程,以持续的提高组织业务效率。常见商业管理教育如 EMBA、MBA 等均将 BPM 包含在内。
BPM软件
BPM 软件就是根据企业中业务环境的变化,推进人与人之间、人与系统之间以及系统与系统之间的整合及调整的经营方法与解决方案的 IT 工具。
通过 BPM 软件对企业内部及外部的业务流程的整个生命周期进行建模、自动化、管理监控和优化,使企业成本降低,利润得以大幅提升。
BPM 软件在企业中应用领域广泛,凡是有业务流程的地方都可以 BPM 软件进行管理,比如企业人事办公管理、采购流程管理、公文审批流程管理、财务管理等。
BPMN
BPMN(Business Process Model AndNotation):业务流程模型和符号是由 BPMI(Business Process Management Initiative)开发的一套标准的业务流程建模符号,使用 BPMN 提供的符号可以创建业务流程。
2004 年 5 月发布了 BPMN1.0 规范。BPMI 于 2005 年 9 月并入 OMG(The Object Management Group 对象管理组织)组织。OMG 于 2011 年 1 月发布 BPMN2.0 的最终版本。
BPMN 是目前被各 BPM 厂商广泛接受的 BPM 标准。Activiti 就是使用 BPMN 2.0 进行流程建模、流程执行管理,它包括很多的建模符号,比如:
事件(Event),用一个圆圈表示,它是流程中运行过程中发生的事情。
活动(Activity)用圆角矩形表示,一个流程由一个活动或多个活动组成。
使用步骤
- 部署活动:
Activiti 是一个工作流引擎(其实就是一堆 jar 包 API),业务系统访问(操作)Activiti 的接口,就可以方便的操作流程相关数据,这样就可以把工作流环境与业务系统的环境集成在一起。
- 定义流程:
使用 Activiti 流程建模工具(activity-designer)定义业务流程(bpmn文件) 。.bpmn 文件就是业务流程定义文件,通过 xml 定义业务流程。
- 部署流程:
Activiti 部署业务流程定义(.bpmn文件)。使用 Activiti 提供的 API 把流程定义内容存储起来,在Activiti 执行过程中可以查询定义的内容。Activiti 执行把流程定义内容存储在数据库中。
- 启动流程实例:
流程实例也叫:ProcessInstance。启动一个流程实例表示开始一次业务流程的运行。在员工请假流程定义部署完成后,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响。
- 用户查询代办任务:
因为现在系统的业务流程已经交给 Activiti 管理,通过 Activiti 就可以查询当前流程执行到哪了,当前用户需要办理什么任务了,这些 Activiti 帮我们管理了,而不需要开发人员自己编写在 SQL 语句查询。
- 用户办理任务:
用户查询待办任务后,就可以办理某个任务,如果这个任务办理完成还需要其它用户办理,比如采购单创建后由部门经理审核,这个过程也是由 Activiti 帮我们完成了。
- 结束流程:
当任务办理完成没有下一个任务结点了,这个流程实例就完成了。
Activiti应用
基本使用
首先需要导入依赖项:
<properties>
<slf4j.version>1.6.6</slf4j.version>
<log4j.version>1.2.12</log4j.version>
<activiti.version>7.0.0.Beta1</activiti.version>
</properties>
<dependencies>
<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 json数据转换 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-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>
<!-- activiti 云支持 -->
<dependency>
<groupId>org.activiti.cloud</groupId>
<artifactId>activiti-cloud-services-api</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!-- 链接池 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- log start -->
<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>
</dependencies>
然后添加一个日志文件 log4j.properties:
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:\log\act\activiti.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m
添加 Activiti 的配置文件。 Activiti 的默认的使用方式是要求我们在resources 下创建 activiti.cfg.xml
文件,默认的方式的名称是不能修改的。
在配置文件中我们有两种配置方式:一种是单独配置数据源,另一种是不单独配置数据源。
<?xml version="1.0" encoding="UTF-8"?>
<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">
<bean class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration" id="processEngineConfiguration">
<property name="jdbcDriver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/activiti?characterEncoding=utf-8&nullCatalogMeansCurrent=true&serverTimezone=UTC" />
<property name="jdbcUsername" value="root" />
<property name="jdbcPassword" value="123456" />
<property name="databaseSchemaUpdate" value="true" />
<!--<property name="dataSource" ref="dataSource" />-->
</bean>
<bean class="org.apache.commons.dbcp.BasicDataSource" id="dataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/activiti?characterEncoding=utf-8&nullCatalogMeansCurrent=true&serverTimezone=UTC" />
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="maxActive" value="3" />
<property name="maxIdle" value="2" />
</bean>
</beans>
运行程序生成表结构:
public class TestActiviti {
/**
* 生成 Activiti 的相关的表结构
*/
@Test
public void testGenerateTables() {
// 使用 classpath 下的 activiti.cfg.xml 中的配置来创建 ProcessEngine 对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
System.out.println(engine);
}
}
运行完成之后,可以通过 Navicat 看到生成了 25 张表:
表结构介绍
看到刚才创建的表,我们发现 Activiti 的表都以 ACT_ 开头。
第二部分是表示表的用途的两个字母标识。 用途也和服务的 API 对应。
- ACT_RE:“RE” 表示 repository。这个前缀的表包含了流程定义和流程静态资源(图片,规则等等)。
- ACT_RU:“RU” 表示 runtime。 这些运行时的表,包含流程实例、任务、变量、异步任务等运行中的数据。Activiti 只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。这样运行时表可以一直很小速度很快。
- ACT_HI:“HI” 表示 history。这些表包含历史数据,比如历史流程实例,变量,任务等等。
- ACT_GE:“GE” 表示 general。通用数据,用于不同场景下。
表分类 | 表名 | 解释 |
---|---|---|
一般数据 | ||
[ACT_GE_BYTEARRAY] | 通用的流程定义和流程资源 | |
[ACT_GE_PROPERTY] | 系统相关属性 | |
流程历史记录 | ||
[ACT_HI_ACTINST] | 历史的流程实例 | |
[ACT_HI_ATTACHMENT] | 历史的流程附件 | |
[ACT_HI_COMMENT] | 历史的说明性信息 | |
[ACT_HI_DETAIL] | 历史的流程运行中的细节信息 | |
[ACT_HI_IDENTITYLINK] | 历史的流程运行过程中用户关系 | |
[ACT_HI_PROCINST] | 历史的流程实例 | |
[ACT_HI_TASKINST] | 历史的任务实例 | |
[ACT_HI_VARINST] | 历史的流程运行中的变量信息 | |
流程定义表 | ||
[ACT_RE_DEPLOYMENT] | 部署单元信息 | |
[ACT_RE_MODEL] | 模型信息 | |
[ACT_RE_PROCDEF] | 已部署的流程定义 | |
运行实例表 | ||
[ACT_RU_EVENT_SUBSCR] | 运行时事件 | |
[ACT_RU_EXECUTION] | 运行时流程执行实例 | |
[ACT_RU_IDENTITYLINK] | 运行时用户关系信息,存储任务节点与参与者的相关信息 | |
[ACT_RU_JOB] | 运行时作业 | |
[ACT_RU_TASK] | 运行时任务 | |
[ACT_RU_VARIABLE] | 运行时变量表 |
ProcessEngine
前面使用的是 getDefaultProcessEngine
方法来加载 classpath 下的 activiti.cfg.xml 文件,有些情况下我们可能没有按照默认的方式来处理,那这时我们应该怎么办呢?
/**
* 自定义的方式来加载配置文件
*/
@Test
public void testEngine() {
// 首先创建 ProcessEngineConfiguration 对象
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml");
// 通过 ProcessEngineConfiguration 对象来创建 ProcessEngine 对象
ProcessEngine processEngine = configuration.buildProcessEngine();
}
Service接口
Service
是工作流引擎提供用于进行工作流部署、执行、管理的服务接口,我们使用这些接口可以就是操作服务对应的数据表。
通过 ProcessEngine
创建 Service
:
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
TaskService taskService = processEngine.getTaskService();
Service 总览:
service名称 | service作用 |
---|---|
RepositoryService | activiti 的资源管理类 |
RuntimeService | activiti 的流程运行管理类 |
TaskService | activiti 的任务管理类 |
HistoryService | activiti 的历史管理类 |
ManagerService | activiti 的引擎管理类 |
RepositoryService
是 Activiti 的资源管理类,提供了管理和控制流程发布包和流程定义的操作。使用工作流建模工具设计的业务流程图需要使用此 Service 将流程定义文件的内容部署到计算机。
除了部署流程定义以外还可以:查询引擎中的发布包和流程定义。
暂停或激活发布包,对应全部和特定流程定义。暂停意味着它们不能再执行任何操作了,激活是对应的反向操作。获得多种资源,像是包含在发布包里的文件,或引擎自动生成的流程图。
获得流程定义的 pojo 版本,可以用来通过 Java 解析流程,而不必通过 xml。
RuntimeService
Activiti 的流程运行管理类,可以从这个服务类中获取很多关于流程执行相关的信息。
TaskService
Activiti 的任务管理类,可以从这个类中获取任务的信息。
HistoryService
Activiti 的历史管理类,可以查询历史信息,执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者,完成任务的时间,每个流程实例的执行路径等等。这个服务主要通过查询功能来获得这些数据。
ManagementService
Activiti 的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Activiti 系统的日常维护。
流程绘制
在 IDEA 中搜索 Activiti BPMN visualizer 插件,使用这个插件我们可以绘制流程图。
创建一个 BPMN 文件:
然后绘制一个请假流程图,圆形代表开始事件或者结束事件,矩形代表任务,可以认为是需要某个角色来处理。
Activit流程操作
通过调用 Activiti 的 API 将流程定义的 bpmn 和 png 两个文件一个一个添加部署到 Activiti 中,还可以将两个文件打成 zip 包部署。
部署流程
BPMN 文件和 PNG 图片分别部署:
/**
* 实现文件的单个部署
*/
@Test
public void testDeployBPMN() {
// 1.获取 ProcessEngine 对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取 RepositoryService 进行部署操作
RepositoryService service = engine.getRepositoryService();
// 3.使用 RepositoryService 进行部署操作
Deployment deploy = service.createDeployment()
.addClasspathResource("bpmn/ask-for-leave.bpmn20.xml") // 添加 bpmn 资源
.addClasspathResource("bpmn/ask-for-leave.png") // 添加 png 资源
.name("请假申请流程")
.deploy(); // 部署流程
// 4.输出流程部署的信息
System.out.println("流程部署的 id: " + deploy.getId());
System.out.println("流程部署的名称: " + deploy.getName());
}
BPMN 文件和 PNG 文件两个可以打包为 zip 文件统一上传:
/**
* 通过一个 zip 文件来部署操作
* 上传后的数据库中的数据和单个文件上传其实是一样的
*/
@Test
public void testDeployZip() {
// 定义 zip 文件的输入流
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("bpmn/ask-for-leave.zip");
// 对 inputStream 做装饰
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = engine.getRepositoryService();
Deployment deploy = repositoryService.createDeployment()
.addZipInputStream(zipInputStream)
.name("请假申请流程")
.deploy();
// 4.输出流程部署的信息
System.out.println("流程部署的id:" + deploy.getId());
System.out.println("流程部署的名称:" + deploy.getName());
}
流程定义部署后操作 Activiti 中的三张表:
- act_re_deployment:流程定义部署表,每部署一次就增加一条记录。
- act_re_procdef:流程定义表,部署每个新的流程定义都会在这张表中增加一条记录。
- act_ge_bytearray:流程资源表,流程部署的 bpmn 文件和 png 图片会保存在该表中。
启动流程
流程定义部署在 Activiti 后就可以通过工作流管理业务流程,也就是说上边部署的请假申请流程可以使用了。
针对该流程,启动一个流程表示发起一个新的请假申请单,这就相当于 Java 类和 Java 对象的关系,类定义好了后需要 “new” 创建一个对象使用,当然可以 “new” 出多个对象来,对于请假申请流程,张三可以发起一个请假申请单需要启动一个流程实例。
/**
* 启动一个流程实例
*/
@Test
public void testStartProcessInstance() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = engine.getRuntimeService();
String id = "ask-for-leave";
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(id);
// 4.输出相关的流程实例信息
System.out.println("流程定义的ID:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例的ID:" + processInstance.getId());
System.out.println("当前活动的ID:" + processInstance.getActivityId());
}
启动流程实例涉及到的表结构:
- act_hi_actinst:流程实例执行历史。
- act_hi_identitylink:流程的参与用户的历史信息。
- act_hi_procinst:流程实例历史信息。
- act_hi_taskinst:流程任务历史信息。
- act_ru_execution:流程执行信息。
- act_ru_identitylink:流程的参与用户信息。
- act_ru_task:任务信息。
查找任务
流程启动后,任务的负责人就可以查询自己当前能够处理的任务了,查询出来的任务都是当前用户的待办任务。
/**
* 任务查询
*/
@Test
public void testSelectTasks() {
String assignee = "张三";
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 任务查询 需要获取一个 TaskService 对象
TaskService taskService = engine.getTaskService();
// 根据流程的 key 和任务负责人查询任务
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("ask-for-leave")
.taskAssignee(assignee)
.list();
// 输出当前用户具有的任务
for (Task task : list) {
System.out.println("流程实例 ID: " + task.getProcessInstanceId());
System.out.println("任务 ID: " + task.getId());
System.out.println("任务负责人: " + task.getAssignee());
System.out.println("任务名称: " + task.getName());
}
}
任务处理
任务负责人查询出来了待办的人,选择任务进行处理,完成任务。
/**
* 流程任务的处理
*/
@Test
public void testProcessTasks() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
Task task = taskService.createTaskQuery()
.processDefinitionKey("ask-for-leave")
.taskAssignee("张三")
.singleResult();
// 完成任务
taskService.complete(task.getId());
}
“张三”处理了这个操作后,流程就流转到了下一个 Assignee。
流程定义的查询
查询流程相关的信息,包括流程的定义,流程的部署,流程定义的版本。
/**
* 查询流程的定义
*/
@Test
public void testSelectProcDefinition() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = engine.getRepositoryService();
// 获取一个 ProcessDefinitionQuery 对象 用来查询操作
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
List<ProcessDefinition> list = processDefinitionQuery.processDefinitionKey("ask-for-leave")
.orderByProcessDefinitionVersion() // 安装版本排序
.desc() // 倒序
.list();
// 输出流程定义的信息
for (ProcessDefinition processDefinition : list) {
System.out.println("流程定义的 ID: " + processDefinition.getId());
System.out.println("流程定义的 name: " + processDefinition.getName());
System.out.println("流程定义的 key: " + processDefinition.getKey());
System.out.println("流程定义的 version: " + processDefinition.getVersion());
System.out.println("流程部署的 ID: " + processDefinition.getDeploymentId());
}
}
删除流程
/**
* 删除流程
*/
@Test
public void testDeleteDeployment() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = engine.getRepositoryService();
// 删除流程定义,如果该流程定义已经有了流程实例启动则删除时报错
repositoryService.deleteDeployment("2501");
// 设置为 TRUE 级联删除流程定义,及时流程有实例启动,也可以删除,设置为 false 非级联删除操作。
//repositoryService.deleteDeployment("2501", true);
}
下载流程资源
现在我们的流程资源文件已经上传到了数据库中,如果其他用户想要查看这些资源,可以从数据库中把这些资源下载到本地。
使用 Activiti 的 api 来操作我们需要添加 commons-io 的依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
示例代码:
/**
* 读取数据库中的资源文件
*/
@Test
public void testDownloadResource() throws Exception {
// 1.得到 ProcessEngine 对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取 RepositoryService 对象
RepositoryService repositoryService = engine.getRepositoryService();
// 3.得到查询器
ProcessDefinition definition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("ask-for-leave")
.latestVersion()
.singleResult();
// 4.获取流程部署的 id
String deploymentId = definition.getDeploymentId();
// 5.通过 repositoryService 对象的相关方法 来获取图片信息和 bpmn 信息
// png 图片
InputStream pngInput = repositoryService.getResourceAsStream(deploymentId, definition.getDiagramResourceName());
// bpmn 文件的流
InputStream bpmnInput = repositoryService.getResourceAsStream(deploymentId, definition.getResourceName());
// 6.文件的保存
File filePng = new File("d:/ask-for-leave.png");
File fileBpmn = new File("d:/ask-for-leave.bpmn");
OutputStream pngOut = Files.newOutputStream(filePng.toPath());
OutputStream bpmnOut = Files.newOutputStream(fileBpmn.toPath());
IOUtils.copy(pngInput, pngOut);
IOUtils.copy(bpmnInput, bpmnOut);
pngInput.close();
pngOut.close();
bpmnInput.close();
bpmnOut.close();
}
查看流程历史信息
即使流程定义已经被删除了,流程执行的实例信息通过前面的分析,依然保存在 Activiti 的 act_hi_* 的相关表结构中,所以我们还是可以查询流程的执行的历史信息,可以通过 HistoryService
来查看。
/**
* 流程历史信息查看
*/
@Test
public void testSelectProcHistory() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 查看历史信息我们需要通过 HistoryService 来实现
HistoryService historyService = engine.getHistoryService();
// 获取 actinst 表的查询对象
HistoricActivityInstanceQuery instanceQuery = historyService.createHistoricActivityInstanceQuery();
List<HistoricActivityInstance> list = instanceQuery.processDefinitionId("ask-for-leave:2:2504")
.orderByHistoricActivityInstanceStartTime()
.desc()
.list();
// 输出查询的结果
for (HistoricActivityInstance hi : list) {
System.out.println(hi.getActivityId());
System.out.println(hi.getActivityName());
System.out.println(hi.getActivityType());
System.out.println(hi.getAssignee());
System.out.println(hi.getProcessDefinitionId());
System.out.println(hi.getProcessInstanceId());
System.out.println("-----------------------");
}
}
Activiti进阶
流程实例
什么是流程实例
流程实例(Process Instance)代表流程定义的执行实例。一个流程实例包括了所有的运行节点,我们可以利用这个对象来了解当前流程实例的进度等信息。
业务管理
流程定义部署在 Activiti 后,我们就可以在系统中通过 Activiti 去管理流程的执行,但是如果我们要将我们的流程实例和业务数据关联,这时我们需要使用到 Activiti 中预留的 BusinessKey(业务标识)来关联。
/**
* 启动流程实例,添加 businessKey
*/
@Test
public void testProcessBusinessKey() {
// 1.获取 ProcessEngine 对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2.获取 RuntimeService 对象
RuntimeService runtimeService = processEngine.getRuntimeService();
// 3.启动流程实例
ProcessInstance instance = runtimeService.startProcessInstanceByKey("ask-for-leave", "1000");
// 4.输出 processInstance 相关属性
System.out.println("businessKey = " + instance.getBusinessKey());
}
挂起和激活
在实际场景中可能由于流程变更需要将当前运行的流程暂停而不是删除,流程暂停后将不能继续执行。
全部挂起:
/**
* 全部流程挂起实例与激活
*/
@Test
public void testSuspendAndActivate() {
// 1.获取 ProcessEngine 对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取 RepositoryService 对象
RepositoryService repositoryService = engine.getRepositoryService();
// 3.查询流程定义的对象
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("ask-for-leave")
.latestVersion()
.singleResult();
// 4.获取当前流程定义的状态
boolean suspended = processDefinition.isSuspended();
String id = processDefinition.getId();
// 5.如果挂起就激活,如果激活就挂起
if (suspended) {
// 表示当前定义的流程状态是 挂起的
// 流程定义的 id 是否激活 激活时间
repositoryService.activateProcessDefinitionById(id, true, null);
System.out.println("流程定义: " + id + ": 已激活");
} else {
// 非挂起状态,激活状态 那么需要挂起流程定义
// 流程 id 是否挂起 挂起时间
repositoryService.suspendProcessDefinitionById(id, true, null);
System.out.println("流程定义: " + id + ": 已挂起");
}
}
挂起单个:
/**
* 单个流程实例挂起与激活
*/
@Test
public void testSuspendAndActivateOne() {
// 1.获取 ProcessEngine 对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取 RuntimeService
RuntimeService runtimeService = engine.getRuntimeService();
// 3.获取流程实例对象
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId("5001")
.singleResult();
// 4.获取相关的状态操作
boolean suspended = processInstance.isSuspended();
String id = processInstance.getId();
if (suspended) {
// 挂起 -> 激活
runtimeService.activateProcessInstanceById(id);
System.out.println("流程定义:" + id + ": 已激活");
} else {
// 激活 -> 挂起
runtimeService.suspendProcessInstanceById(id);
System.out.println("流程定义:" + id + ": 已挂起");
}
}
个人任务
分配任务责任人
固定分配
在进行业务流程建模的时候指定固定的任务负责人,直接在 Assignee 输入框内输入名称:
表达式分配
在 Activiti 中支持使用 UEL 表达式,UEL 表达式是 Java EE 6 规范的一部分,UEL(Unified Expression Language)即统一表达式语音,Activiti 支持两种 UEL 表达式:UEL-Value 和 UEL-Method。
UEL-Value:
在把流程部署到数据库之后,我们要创建一个新的流程实例,然后关联 UEL 表达式:
/**
* 创建一个流程实例
* 给流程定义中的 UEL 表达式赋值
*/
@Test
public void testCreateProcessInstanceUEL() {
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = processEngine.getRuntimeService();
// 设置 assignees 的取值,
Map<String, Object> assignees = new HashMap<>();
assignees.put("assignee1", "张三");
assignees.put("assignee2", "李四");
assignees.put("assignee3", "王五");
assignees.put("assignee4", "赵财务");
runtimeService.startProcessInstanceByKey("ask-for-leave-uel", assignees);
}
UEL-Method:
Assignee ${userBean.getUserId()}
userBean
是 Spring 容器中的一个 Bean,表示调用该 Bean 的 getUserId
方法。
再比如:${ldapService.findManagerForEmployee(emp)}
,ldapService
是 Spring 容器的一个 bean。findManagerForEmployee
是该 bean 的一个方法,emp 是 activiti 流程变量,emp 作为参数传到 ldapService.findManagerForEmployee
方法中。
表达式支持解析基础类型、bean、list、array 和 map,也可作为条件判断。比如:${order.price > 100 && order.price < 250}
监听器分配
我们可以使用监听器来完成很多 Activiti 的流程业务。我们在此处使用监听器来完成负责人的指定,那么我们在流程设计的时候就不需要指定 Assignee。
UserTask 的 Event 选项:
- Create:任务创建后触发。
- Assignment:任务分配后触发。
- Delete:任务完成后触发。
- All:所有事件都触发。
public class ITaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
if ("创建请假流程".equals(delegateTask.getName()) && "create".equals(delegateTask.getEventName())) {
// 指定任务的负责人
delegateTask.setAssignee("张三-Listener");
}
}
}
在 bpmn 文件的 XML 代码如下:
流程启动时会自动触发监听器的逻辑。
流程变量
什么是流程变量
流程变量在 Activiti 中是一个非常重要的角色,流程运转有时需要靠流程变量,业务系统和 Activiti 结合时少不了流程变量,流程变量就是 Activiti 在管理工作流时根据管理需要而设置的变量。比如:在出差申请流程流转时如果出差天数大于 3 天则由总经理审核,否则由人事直接审核,出差天数就可以设置为流程变量,在流程流转时使用。
注意:虽然流程变量中可以存储业务数据可以通过 Activiti 的 api 查询流程变量从而实现查询业务数据,但是不建议这样使用,因为业务数据查询由业务系统负责,Activiti 设置流程变量是为了流程执行需要而创建。
流程变量类型
如果将 pojo 存储到流程变量中,必须实现序列化接口 Serializable
,为了防止由于新增字段无法反序列化,需要生成 serialVersionUID
。
流程变量作用域
流程变量的作用域可以是一个流程实例(Process Instance),或一个任务(Task),或一个执行实例 (execution)。
global 变量:
流程变量的默认作用域是流程实例。当一个流程变量的作用域为流程实例时,可以称为 Global 变量。Global 变量中变量名不允许重复,设置相同名称的变量,后设置的值会覆盖前设置的变量值。
local 变量:
任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大,称为 Local 变量。
Local 变量由于在不同的任务或不同的执行实例中,作用域互不影响,变量名可以相同没有影响。Local 变量名也可以和 Global 变量名相同,没有影响。
流程变量的使用方法
可以在 Assignee 处设置 UEL 表达式,表达式的值为任务的负责人,比如:${assignee}
,assignee 就是一个流程变量名称。
Activiti 获取 UEL 表达式的值,即流程变量 assignee 的值 ,将 assignee 的值作为任务的负责人进行任务分配。
可以在连线上设置 UEL 表达式,决定流程走向。
比如:${price < 10000}
。price 就是一个流程变量名称,UEL 表达式结果类型为布尔类型。如果 UEL 表达式是 true,要决定流程执行走向。
设置流程变量
Global变量
启动时设置流程变量:
/**
* 启动流程实例,设置流程变量
*/
@Test
public void testStartProcessWithVars() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = engine.getRuntimeService();
// 流程定义key
String key = "evection-variable";
// 创建变量集合
Map<String, Object> variables = new HashMap<>();
// 创建出差对象 POJO
Evection evection = new Evection();
// 设置出差天数
evection.setNum(4d);
// 定义流程变量到集合中
variables.put("evection", evection);
// 设置 assignees 的取值
variables.put("assignee0", "张三1");
variables.put("assignee1", "李四1");
variables.put("assignee2", "王五1");
variables.put("assignee3", "赵财务1");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, variables);
System.out.println("获取流程实例名称:" + processInstance.getName());
System.out.println("流程定义ID:" + processInstance.getProcessDefinitionId());
}
任务办理时设置:
/**
* 完成任务时设置流程变量
*/
@Test
public void testCompleteTaskAndSetVars() {
String key = "evection-variable";
String assignee = "李四1";
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
Task task = taskService.createTaskQuery()
.processDefinitionKey(key)
.taskAssignee(assignee)
.singleResult();
Map<String, Object> variables = new HashMap<>();
Evection evection = new Evection();
evection.setNum(4d);
variables.put("evection", evection);
if (task != null) {
taskService.complete(task.getId(), variables);
System.out.println("任务执行完成...");
}
}
当前流程实例设置:
/**
* 通过流程实例 id 设置全局变量,该流程实例必须未执行完成。
*/
@Test
public void setGlobalVariableByExecutionId() {
// 当前流程实例执行 id 通常设置为当前执行的流程实例
String executionId = "2601";
// 获取 processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取 RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 创建出差 pojo 对象
Evection evection = new Evection();
// 设置天数
evection.setNum(3d);
// 通过流程实例 id 设置流程变量
runtimeService.setVariable(executionId, "evection", evection);
// 一次设置多个值
// runtimeService.setVariables(executionId, variables)
}
当前任务设置:
/**
* 在当前任务设置流程变量
*/
@Test
public void setGlobalVariableByTaskId() {
// 当前待办任务 id
String taskId = "1404";
// 获取 processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
Evection evection = new Evection();
evection.setNum(3);
// 通过任务设置流程变量
taskService.setVariable(taskId, "evection", evection);
// 一次设置多个值
//taskService.setVariables(taskId, variables)
}
Local变量
任务办理时设置:
/**
* 任务办理时设置 local 流程变量,当前运行的流程实例只能在该任务结束前使用,
* 任务结束该变量无法在当前流程实例使用,可以通过查询历史任务查询。
*/
@Test
public void completeTaskAndSetLocal() {
// 任务 id
String taskId = "1404";
// 获取 processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
// 定义流程变量
Map<String, Object> variables = new HashMap<>();
Evection evection = new Evection();
evection.setNum(3d);
// 变量名是 holiday,变量值是 holiday 对象
variables.put("evection", evection);
// 设置 local 变量,作用域为该任务
taskService.setVariablesLocal(taskId, variables);
// 完成任务
taskService.complete(taskId);
}
通过当前任务设置:
/**
* 通过当前任务设置 local 变量
*/
@Test
public void setLocalVariableByTaskId() {
// 当前待办任务 id
String taskId = "1404";
// 获取 processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
Evection evection = new Evection();
evection.setNum(3d);
// 通过任务设置流程变量
taskService.setVariableLocal(taskId, "evection", evection);
// 一次设置多个值
//taskService.setVariablesLocal(taskId, variables)
}
组任务
在流程定义中在任务结点的 Assignee 固定设置任务负责人,在流程定义时将参与者固定设置在 .bpmn 文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性差。
针对这种情况可以给任务设置多个候选人,可以从候选人中选择参与者来完成任务。
bpmn 文件的代码:
<userTask activiti:candidateUsers="lisi,wangwu" activiti:exclusive="true" id="_3" name="经理审批"/>
组任务办理流程:
- 查询组任务:指定候选人,查询该候选人当前的待办任务。候选人不能立即办理任务。
- 拾取组任务:该组任务的所有候选人都能拾取。将候选人的组任务,变成个人任务。原来候选人就变成了该任务的负责人。
- 查询个人任务:查询方式同个人任务部分,根据assignee查询用户负责的个人任务。
- 办理个人任务。
查询组任务
/**
* 根据候选人查询组任务
*/
@Test
public void testSelectGroupTasksByCandidate() {
String key = "evection1";
String candidateUser = "lisi";
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey(key)
.taskCandidateUser(candidateUser)
.list();
for (Task task : list) {
System.out.println("流程实例Id:" + task.getProcessInstanceId());
System.out.println("任务ID:" + task.getId());
System.out.println("负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());
}
}
拾取组任务
/**
* 候选人 拾取任务
*/
@Test
public void testClaimTasksByCandidate() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String taskId = "72505";
String userId = "lisi";
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskCandidateUser(userId)
.singleResult();
if (task != null) {
taskService.claim(taskId, userId);
System.out.println("拾取成功");
}
}
查询个人代办任务
/**
* 查询个人待办任务
*/
@Test
public void testSelectPersonalTasks() {
String key = "evection1";
String candidateUser = "lisi";
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey(key)
//.taskCandidateUser(candidateUser)
//.taskCandidateOrAssigned(candidateUser)
.taskAssignee(candidateUser)
.list();
for (Task task : list) {
System.out.println("流程实例Id:" + task.getProcessInstanceId());
System.out.println("任务ID:" + task.getId());
System.out.println("负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());
}
}
办理个人任务
/**
* 完成个人任务
*/
@Test
public void testCompletePersonalTask() {
String taskId = "72505";
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
taskService.complete(taskId);
System.out.println("完成任务:" + taskId);
}
归还组任务
/**
* 归还任务
*/
@Test
public void test06() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String taskId = "75002";
String userId = "zhangsan";
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(userId)
.singleResult();
if (task != null) {
// 如果设置为 null,归还组任务,任务没有负责人
taskService.setAssignee(taskId, null);
}
}
任务交接
/**
* 任务交接
*/
@Test
public void testTaskAssociate() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String taskId = "75002";
String userId = "zhangsan";
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(userId)
.singleResult();
if (task != null) {
// 设置该任务的新的负责人
taskService.setAssignee(taskId, "赵六");
}
}
数据库操作
查询当前任务执行表:
SELECT * FROM act_ru_task
任务执行表,记录当前执行的任务,由于该任务当前是组任务,所有 assignee 为空,当拾取任务后该字段就是拾取用户的 id。
查询任务参与者:
SELECT * FROM act_ru_identitylink
任务参与者,记录当前参考任务用户或组,当前任务如果设置了候选人,会向该表插入候选人记录,有几个候选就插入几个。
与 act_ru_identitylink 对应的还有一张历史表 act_hi_identitylink,向 act_ru_identitylink 插入记录的同时也会向历史表插入记录。
网关
排他网关
排他网关,用来在流程中实现决策。 当流程执行到这个网关,所有分支都会判断条件是否为 true,如果为 true 则执行该分支。
注意:排他网关只会选择一个为 true 的分支执行。如果有两个分支条件都为 true,排他网关会选择 id值较小的一条分支去执行。
为什么要用排他网关?不用排他网关也可以实现分支,如:在连线的 condition 条件上设置分支条件。在连线设置 condition 条件的缺点:如果条件都不满足,流程就结束了(是异常结束)。如果使用排他网关决定分支的走向,如下:
如果从网关出去的线所有条件都不满足则系统抛出异常。
并行网关
并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的。
fork 分支:并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
join 汇聚:所有到达并行网关,在此等待的进入分支,直到所有进入顺序流的分支都到达以后,流程就会通过汇聚网关。
注意,如果同一个并行网关有多个进入和多个外出顺序流,它就同时具有分支和汇聚功能。这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。
与其他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略。
说明:
技术经理和项目经理是两个 execution 分支,在 act_ru_execution 表有两条记录分别是技术经理和项目经理,act_ru_execution 还有一条记录表示该流程实例。
待技术经理和项目经理任务全部完成,在汇聚点汇聚,通过 parallelGateway 并行网关。并行网关在业务应用中常用于会签任务,会签任务即多个参与者共同办理的任务。
包含网关
包含网关可以看做是排他网关和并行网关的结合体。和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。
包含网关的功能是基于进入和外出顺序流的:
分支:所有外出顺序流的条件都会被解析,结果为 true 的顺序流会以并行方式继续执行,会为每个顺序流创建一个分支。
汇聚:所有并行分支到达包含网关,会进入等待状态,直到每个包含流程 token 的进入顺序流的分支都到达。 这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。在汇聚之后,流程会穿过包含网关继续执行。
事件网关
事件网关允许根据事件判断流向。网关的每个外出顺序流都要连接到一个中间捕获事件。当流程到达一个基于事件网关,网关会进入等待状态:会暂停执行。与此同时,会为每个外出顺序流创建相对的事件订阅。
事件网关的外出顺序流和普通顺序流不同,这些顺序流不会真的“执行”,相反它们让流程引擎去决定执行到事件网关的流程需要订阅哪些事件。要考虑以下条件:
- 事件网关必须有两条或以上外出顺序流。
- 事件网关后,只能使用
intermediateCatchEvent
类型(activiti 不支持基于事件网关后连接ReceiveTask)。 - 连接到事件网关的中间捕获事件必须只有一个入口顺序流。
Activiti整合SpringBoot
Activiti 7 发布正式版本之后,它和 SpringBoot2.x 已经完全整合开发了。
创建好了 SpringBoot 工厂之后需要导入依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.0.0.Beta2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
修改配置文件:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///activiti?characterEncoding=utf-8&nullCatalogMeansCurrent=true&serverTimezone=UTC
name: root
password: root1234
activiti:
# activiti 的配置
# 1. false:默认值。activiti 在启动时,对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出异常
# 2. true: activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建
# 3. create_drop: 在 activiti 启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)
# 4. drop-create: 在 activiti 启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)
database-schema-update: true
# 检测历史表是否存在, Activiti7 中默认是没有开启数据库历史记录的,启动数据库历史记录
db-history-used: true
# 记录历史等级 可配置的历史级别有 none, activity, audit, full
# none: 不保存任何的历史数据,因此,在流程执行过程中,这是最高效的。
# activity: 级别高于 none,保存流程实例与流程行为,其他数据不保存。
# audit: 除 activity 级别会保存的数据外,还会保存全部的流程任务及其属性。audit 为 history 的默认值。
# full: 保存历史数据的最高级别,除了会保存 audit 级别的数据外,还会保存其他全部流程相关的细节数据,包括一些流程参数等。
history-level: full
# 校验流程文件,默认校验 resources下的 process 文件夹里的流程文件
check-process-definitions: false
整合Spring Security
因为 Activiti 7 和 SpringBoot 整合后,默认情况下,集成了 Spring Security 安全框架,这样我们就要准备 Spring Security 的相关配置信息:
声明一个 Spring Security 的工具类:
package org.codeart.activiti.util;
import org.apache.catalina.security.SecurityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collection;
@Component
public class Security4Activiti {
private final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
@Resource(name = "myUserDetailsService")
private UserDetailsService userDetailsService;
public void logInAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (user == null) {
throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
}
logger.info("> Logged in as: " + username);
SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public Object getCredentials() {
return user.getPassword();
}
@Override
public Object getDetails() {
return user;
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return user.getUsername();
}
}));
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
}
}
添加一个 Spring Security 的配置文件:
package org.codeart.activiti.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService myUserDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
// 这里添加用户,后面处理流程时用到的任务负责人,需要添加在这里
String[][] usersGroupsAndRoles = {
{"jack", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
{"rose", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
{"tom", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
{"other", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"},
{"system", "password", "ROLE_ACTIVITI_USER"},
{"admin", "password", "ROLE_ACTIVITI_ADMIN"},
};
for (String[] user : usersGroupsAndRoles) {
List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
log.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]), authoritiesStrings.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList())));
}
return inMemoryUserDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
创建一个BPMN文件
创建一个简单的 bpmn 文件,并设置任务的用户组,CandidateGroups,CandidateGroups 中的内容要与在Spring Security 的配置文件中配置的用户组的名称要保持一致,可以填写 activitTeam 或者 otherTeam。这样填写的好处是,当不确定到底由谁来负责当前的任务的时候,只要是 Groups 内的用户都可以拾取这个任务。
Activiti 7 中可以自动部署流程,前提是在 resources 目录下,创建一个新的目录 processes,用来放置 bpmn 文件。
单元测试
package org.codeart.activiti.test;
import org.activiti.api.process.model.ProcessDefinition;
import org.activiti.api.process.model.ProcessInstance;
import org.activiti.api.process.model.builders.ProcessPayloadBuilder;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.activiti.api.task.model.Task;
import org.activiti.api.task.model.builders.TaskPayloadBuilder;
import org.activiti.api.task.runtime.TaskRuntime;
import org.activiti.engine.RepositoryService;
import org.codeart.activiti.util.Security4Activiti;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ActSpringbootApplicationTests {
@Autowired
private ProcessRuntime processRuntime;
@Autowired
private TaskRuntime taskRuntime;
@Autowired
private Security4Activiti security4Activiti;
@Autowired
private RepositoryService repositoryService;
@Test
void contextLoads() {
System.out.println(taskRuntime);
}
/**
* 查询流程的定义
*/
@Test
public void test02() {
security4Activiti.logInAs("system");
Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(0, 10));
System.out.println("可用的流程定义数量:" + processDefinitionPage.getTotalItems());
for (ProcessDefinition processDefinition : processDefinitionPage.getContent()) {
System.out.println("流程定义:" + processDefinition);
}
}
/**
* 部署流程
*/
@Test
public void test03() {
repositoryService.createDeployment()
.addClasspathResource("bpmn/my-evection.bpmn")
.addClasspathResource("bpmn/my-evection.png")
.name("出差申请单")
.deploy();
}
/**
* 启动流程实例
*/
@Test
public void test04() {
security4Activiti.logInAs("system");
ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder.start()
.withProcessDefinitionKey("my-evection")
.build());
System.out.println("流程实例id:" + processInstance.getId());
}
/**
* 任务查询、拾取及完成操作
*/
@Test
public void test05() {
security4Activiti.logInAs("jack");
Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
if (tasks != null && tasks.getTotalItems() > 0) {
for (Task task : tasks.getContent()) {
// 拾取任务
taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(task.getId()).build());
System.out.println("任务:" + task);
taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(task.getId()).build());
}
}
Page<Task> taskPage2 = taskRuntime.tasks(Pageable.of(0, 10));
if (taskPage2.getTotalItems() > 0) {
System.out.println("任务:" + taskPage2.getContent());
}
}
}
转载自:https://juejin.cn/post/7401066742069706764