前言
工作流程是我们日常开发项目中常见的功能,本文主要springboot整合activiti7,梳理activiti在工作中的一些常见用法和功能
一、工作流介绍
1.1 概述
工作流(Workflow):就是“业务过程的部分或整体在计算机应用环境下的自动化”,它主要解决的是“使在多个参与者之间按照某种预定义的规则传递文档、信息或任务的过程自动进行,从而实现某个预期的业务目标,或者促使此目标的实现”。
1.2 常见工作流
采用工作流有以下优点
1、提高系统的柔性,适应业务流程的变化
2、实现更好的业务过程控制,提高顾客服务质量
3、降低系统开发和维护成本
常见工作流:Activiti、JBPM、OSWorkflow、ActiveBPEL、YAWL等。本文主要采用activiti7工作流开发
1.3 Activiti7
Alfresco软件在2010年5月17日宣布Activiti业务流程管理(BPM)开源项目的正式启动。Activiti是一个工作流引擎, activiti可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN2.0进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由activiti进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。
官方地址:https://www.activiti.org/
Activiti6.0开发手册:https://www.activiti.org/userguide/#bpmnMultiInstance
GitHub Activiti7开发手册地址:https://github.com/Activiti/activiti-7-developers-guide
二、工作流术语
2.1 工作流引
ProcessEngine对象: 这是Activiti工作的核心.负责生成流程运行时的各种实例及数据,监控和管理流程的运行
2.2 BPM
BPM(业务流程管理):是一种以规范化的构造端到端的卓越业务流程为中心,以持续的提高组织业务绩效为目的的系统化方法
2.3 BPMN
BPMN(Business Process Model and Notation) 业务流程建模与标注,描述流程的基本符号,包括这些图元如何组合成一个业务流程图(Business Process Diagram)bpmn文件又可以叫做流程定义文件,它需要遵循BPMN语言规。
2.4 流对象
流对象(process engine)通过它可以获得我们需要的一切activiti服务
一个业务流程图有三个流对象的核心元素
- 事件
一个事件用圆圈来描述,表示一个业务流程期间发生的东西。事件影响流程的流动,一般有一个原因(触发器)或一个影响(结果),基于它们对流程的影响,有三种事件:开始事件,中间事件,终止事件。
- 活动
用圆角矩形表示,一个流程由一个活动或多个活动组成
- 条件
条件用菱形表示,用于控制序列流的分支与合并,可以作为选择,包括路径的分支与合,内部的标记会给出控制流的类型
三、Activiti结构
3.1 Activiti系统服务结构图
- 核心类:ProcessEngine: 流程引擎的抽象,可以通过此类获取需要的所有服务
- 服务类:XxxService: 通过ProcessEngine获取,Activiti将不同生命周期的服务封装在不同Service中,包括定义,部署,运行.通过服务类可获取相关生命周期中的服务信息
- RepositoryService
Repository Service提供了对repository的存取服,Activiti中每一个不同版本的业务流程的定义都需要使用一些定义文件,部署文件和支持数据(例如BPMN2.0XML文件,表单定义文件,流程定义图像文件等),这些文件都存储在Activiti内建的Repository中 - RuntimeService:
Runtime Service提供了启动流程,查询流程实例,设置获取流程实例变量等功能.此外它还提供了对流程部署,流程定义和流程实例的存取服务 - TaskService:
Task Service提供了对用户Task和Form相关的操作.它提供了运行时任务查询,领取,完成,删除以及变量设置等功能 - HistoryService
History Service用于获取正在运行或已经完成的流程实例的信息,与Runtime Service获取的流程信息不同,历史信息包含已经持久化存储的永久信息,并已经被针对查询优化 FormService
使用Form Service可以存取启动和完成任务所需的表单数据并且根据需要来渲染表单
Activiti中的流程和状态Task均可以关联业务相关的数据IdentityService
Identity Service提供了对Activiti系统中的用户和组的管理功,Activiti中内置了用户以及组管理的功能,必须使用这些用户和组的信息才能获取到相应的Task- ManagementService
Management Service提供了对Activiti流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用。主要用于 Activiti 系统的日常维护核心业务对象:org.activiti.engine.impl.persistence.entity包下的类,Task,ProcessInstance,Execution等根据不同职责实现相应接口的方法(如需要持久化则继承PersistentObject接口),与传统的实体类不同
(注:Activiti7删除了FormService和IdentityService接口),工作流程与业务解耦,结合具体业务自由的配置用户和用户组。
- RepositoryService
3.2 Activiti数据库表结构
Activiti7工作流总共包含25张数据表(Activiti6包含23张表),Activiti会自动帮你生成这25张表,所有的表名默认以“ACT_”开头。
Activiti 使用到的表都是 ACT_ 开头的。表名的第二部分用两个字母表明表的用途:
- ACT_GE_ (GE) 表示 general 全局通用数据及设置,各种情况都使用的数据。
- ACT_HI_ (HI) 表示 history 历史数据表,包含着程执行的历史相关数据,如结束的流程实例,变量,任务,等等
- ACT_RE_ (RE) 表示 repository 存储,包含的是静态信息,如,流程定义,流程的资源(图片,规则等)。
- ACT_RU_ (RU) 表示 runtime 运行时,运行时的流程变量,用户任务,变量,职责(job)等运行时的数据。Activiti 只存储实例执行期间的运行时数据,当流程实例结束时,将删除这些记录。这就保证了这些运行时的表小且快。
Activiti7 25张表含义
表名 | 介绍 |
---|---|
act_evt_log | 流程引擎通用日志表 |
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_procdef_info | 流程定义的动态变更信息 |
act_re_deployment | 部署信息表 |
act_re_model | 流程设计实体表 |
act_re_procdef | 流程定义数据表 |
act_ru_deadletter_job | 作业失败表,失败次数>重试次数 |
act_ru_event_subscr | 运行时事件表 |
act_ru_execution | 运行时流程执行实例表 |
act_ru_identitylink | 运行时用户信息表 |
act_ru_integration | 运行时综合表 |
act_ru_job | 作业表 |
act_ru_suspended_job | 作业暂停表 |
act_ru_task | 运行时任务信息表 |
act_ru_timer_job | 运行时定时器表 |
act_ru_variable | 运行时变量表 |
四、流程步骤
4.1 部署Activiti
Activiti是一个工作流引擎(其实就是一堆jar包API),业务系统访问(操作)activiti的接口,就可以方便的操作流程相关数据,这样就可以把工作流环境与业务系统的环境集成在一起。
4.2 流程定义
工作流要有流程模型图,使用activiti流程建模工具(activity-designer)定义业务流程(.bpmn文件) 绘制好流程模型,.bpmn文件就是业务流程定义文件,通过xml定义业务流程。
4.3 流程定义部署
将画好的流程图(activiti部署业务流程定义(.bpmn文件)),使用activiti提供的api把流程定义内容存储起来,在Activiti执行过程中可以查询定义的内容,Activiti执行把流程定义内容存储在数据库中
4.4 启动一个流程实例
流程实例也叫:ProcessInstance,启动一个流程实例表示开始一次业务流程的运行。在启动流程实例之前可以配置相应的业务需求,将某个业务绑定到当前流程上
4.5 用户查询待办任务(Task)
将系统的业务流程已经交给activiti管理,通过activiti就可以查询当前流程执行到哪里了,当前用户需要办理什么任务,activiti帮我们管理执行操作
4.6 用户已办任务历史记录
用户可以查询已经办理的业务和正在处理的任务,查询历史任务表获得具体流程执行细节,当一个流程没有下一节点时,整个流程实例就执行结束。
五、springboot整合Activiti
5.1 springboot整合Activiti环境依赖
1)maven环境
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mengxuegu</groupId>
<artifactId>activiti-boot</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/>
</parent>
<properties>
<activiti.version>7.1.0.M6</activiti.version>
<mybatis-plus.version>3.3.1</mybatis-plus.version>
</properties>
<dependencies>
<!-- Activiti -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- java绘制activiti流程图 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- activiti json转换器-->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- svg转png图片工具-->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-all</artifactId>
<version>1.10</version>
</dependency>
<!-- web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-plusǷ-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- SpringSecurity -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2)添加log4j日志配置
我们使用log4j日志包,可以对日志进行配置,在resources 下创建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\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=f:\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\n
3)添加activiti配置文件
- 官方采用默认方式的要求是在 resources 下创建 activiti.cfg.xml 文件,注意:默认方式目录和文件名不能修改,因为activiti的源码中已经设置,到固定的目录读取固定文件名的文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
我们这里不做采用,直接配置application.yaml
server:
port: 9889
servlet:
context-path: /com/act
spring:
mvc:
static-path-pattern: /**
cloud:
nacos:
discovery:
server-addr: 填写自己地址
datasource:
druid:
配置自己数据库连接
# 初始化大小,最小,最大
initial-size: 1
min-idle: 3
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 6000
# activiti配置
activiti:
#自动更新数据库结构
# true:适用开发环境,默认值。activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建
# false:适用生产环境。activiti在启动时,对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出异常
# create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)
# drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)
database-schema-update: false
# activiti7与springboot整合后默认不创建历史表,需要手动开启
db-history-used: true
# 记录历史等级 可配置的历史级别有none, activity, audit, full
# none:不保存任何的历史数据,因此,在流程执行过程中,这是最高效的。
# activity:级别高于none,保存流程实例与流程行为,其他数据不保存。
# audit:除activity级别会保存的数据外,还会保存全部的流程任务及其属性。
# full:保存历史数据的最高级别,除了会保存audit级别的数据外,还会保存其他全部流程相关的细节数据,包括一些流程参数等。
history-level: full
# 是否自动检查resources下的processes目录的流程定义文件
check-process-definitions: false
# # smtp服务器地址
# mail-server-host:
# # SSL端口号
# mail-server-port:
# # 开启ssl协议
# mail-server-use-ssl: true
# # 默认的邮件发送地址(发送人),如果activiti流程定义中没有指定发送人,则取这个值
# mail-server-default-from:
# # 邮件的用户名
# mail-server-user-name:
deployment-mode: never-fail
logging:
level:
com:
oneconnect:
sg: debug
# 日志级别是debug才能显示SQL日志
org.activiti.engine.impl.persistence.entity: inf
mybatis-plus:
type-aliases-package: com.act.entity
# xxxMapper.xml 路径
mapper-locations: classpath*:com/act/mapper/**/*.xml
5.2 流程操作—流程模型
1)绘制流程模型编辑器
我们这里用java绑定activity-designer模型编辑器绘制模型,导入官方编辑器文件
- ModelEditorJsonRestResource
package com.oneconnect.sg.act.activiti;
import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Model;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* @author Tijs Rademakers
*/
@RestController
public class ModelEditorJsonRestResource implements ModelDataJsonConstants {
protected static final Logger LOGGER = LoggerFactory.getLogger(ModelEditorJsonRestResource.class);
@Autowired
private RepositoryService repositoryService;
@Autowired
private ObjectMapper objectMapper;
@RequestMapping(value="/model/{modelId}/json", method = RequestMethod.GET, produces = "application/json")
public ObjectNode getEditorJson(@PathVariable String modelId) {
ObjectNode modelNode = null;
Model model = repositoryService.getModel(modelId);
if (model != null) {
try {
if (StringUtils.isNotEmpty(model.getMetaInfo())) {
modelNode = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
} else {
modelNode = objectMapper.createObjectNode();
modelNode.put(MODEL_NAME, model.getName());
}
modelNode.put(MODEL_ID, model.getId());
ObjectNode editorJsonNode = (ObjectNode) objectMapper.readTree(
new String(repositoryService.getModelEditorSource(model.getId()), "utf-8"));
modelNode.put("model", editorJsonNode);
} catch (Exception e) {
LOGGER.error("Error creating model JSON", e);
throw new ActivitiException("Error creating model JSON", e);
}
}
return modelNode;
}
}
- ModelSaveRestResource
package com.oneconnect.sg.act.activiti;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.activiti.editor.constants.ModelDataJsonConstants;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Model;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
/**
* @author Tijs Rademakers
*/
@RestController
public class ModelSaveRestResource implements ModelDataJsonConstants {
protected static final Logger LOGGER = LoggerFactory.getLogger(ModelSaveRestResource.class);
@Autowired
private RepositoryService repositoryService;
@Autowired
private ObjectMapper objectMapper;
@RequestMapping(value="/model/{modelId}/save", method = RequestMethod.PUT)
@ResponseStatus(value = HttpStatus.OK)
public void saveModel(@PathVariable String modelId, @RequestParam MultiValueMap<String, String> values) {
try {
Model model = repositoryService.getModel(modelId);
ObjectNode modelJson = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
modelJson.put(MODEL_NAME, values.getFirst("name"));
modelJson.put(MODEL_DESCRIPTION, values.getFirst("description"));
model.setMetaInfo(modelJson.toString());
model.setName(values.getFirst("name"));
// 每次修改模型保存后,将版本号+1
model.setVersion(model.getVersion()+1);
ObjectNode jsonXml = (ObjectNode) objectMapper.readTree(values.get("json_xml").get(0));
model.setKey(jsonXml.get("properties").get("process_id").textValue());
repositoryService.saveModel(model);
repositoryService.addModelEditorSource(model.getId(), values.getFirst("json_xml").getBytes("utf-8"));
InputStream svgStream = new ByteArrayInputStream(values.getFirst("svg_xml").getBytes("utf-8"));
TranscoderInput input = new TranscoderInput(svgStream);
PNGTranscoder transcoder = new PNGTranscoder();
// Setup output
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
TranscoderOutput output = new TranscoderOutput(outStream);
// Do the transformation
transcoder.transcode(input, output);
final byte[] result = outStream.toByteArray();
repositoryService.addModelEditorSourceExtra(model.getId(), result);
outStream.close();
} catch (Exception e) {
LOGGER.error("Error saving model", e);
throw new ActivitiException("Error saving model", e);
}
}
}
- StencilsetRestResource
package com.oneconnect.sg.act.activiti;
import org.activiti.engine.ActivitiException;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.InputStream;
/**
* @author Tijs Rademakers
*/
@RestController
public class StencilsetRestResource {
@RequestMapping(value="/editor/stencilset", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
public @ResponseBody String getStencilset() {
InputStream stencilsetStream = this.getClass().getClassLoader().getResourceAsStream("static/stencilset.json");
try {
return IOUtils.toString(stencilsetStream, "utf-8");
} catch (Exception e) {
throw new ActivitiException("Error while loading stencil set", e);
}
}
}
将编辑器静态资源文件放在resources下面

2)创建模型
/**
* 新增流程模型
* @param req
* @return
* @throws Exception
*/
@Test
public String add(ModelAddREQ req) throws Exception {
int version = 0;
// 1. 初始空的模型
Model model = repositoryService.newModel();
model.setName(req.getName());
model.setKey(req.getKey());
model.setVersion(version);
// 封装模型json对象
ObjectNode objectNode = objectMapper.createObjectNode();
objectNode.put(ModelDataJsonConstants.MODEL_NAME, req.getName());
objectNode.put(ModelDataJsonConstants.MODEL_REVISION, version);
objectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, req.getDescription());
model.setMetaInfo(objectNode.toString());
// 保存初始化的模型基本信息数据
repositoryService.saveModel(model);
// 封装模型对象基础数据json串
// {"id":"canvas","resourceId":"canvas","stencilset":{"namespace":"http://b3mn.org/stencilset/bpmn2.0#"},"properties":{"process_id":"未定义"}}
ObjectNode editorNode = objectMapper.createObjectNode();
ObjectNode stencilSetNode = objectMapper.createObjectNode();
stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");
editorNode.replace("stencilset", stencilSetNode);
// 标识key
ObjectNode propertiesNode = objectMapper.createObjectNode();
propertiesNode.put("process_id", req.getKey());
editorNode.replace("properties", propertiesNode);
repositoryService.addModelEditorSource(model.getId(), editorNode.toString().getBytes("utf-8"));
return model.getId();
}
将具体要绑定的业务bussinekey和routes绑定到此模型上
3) 查询流程模型模板
/**
* 查询所有流程定义模板
*/
@Test
public void modelList() {
ModelQuery modelQuery = repositoryService.createModelQuery();
List<Model> modelList = modelQuery.orderByCreateTime().desc().list();
modelList.stream().forEach(m -> {
System.out.print(" 模型id: " + m.getId());
System.out.print(", 模型名称: " + m.getName());
System.out.print(", 模型描述: " + m.getMetaInfo());
System.out.print(", 模型标识key: " + m.getKey());
System.out.print(", 模型版本号: " + m.getVersion());
});
}
4)删除流程定义模板
/**
* 删除流程定义模板
*/
@Test
public void deleteModel() {
String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
repositoryService.deleteModel(modelId);
//ACT_RE_MODEL、ACT_GE_BYTEARRAY
System.out.println("删除成功");
}
5)导出模型zip方式
/**
* 将模型以zip的方式导出
* @param modelId
* @param response
*/
@Override
public void exportZip(HttpServletResponse response) {
String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
ZipOutputStream zipos = null;
try {
// 实例化zip输出流
zipos = new ZipOutputStream(response.getOutputStream());
// 压缩包文件名
String zipName = "模型不存在";
// 1. 查询模型基本信息
Model model = repositoryService.getModel(modelId);
if(model != null) {
// 2. 查询流程定义模型的json字节码
byte[] bpmnJsonBytes = repositoryService.getModelEditorSource(modelId);
// 2.1 将json字节码转换为xml字节码
byte[] xmlBytes = bpmnJsonXmlBytes(bpmnJsonBytes);
if(xmlBytes == null) {
zipName = "模型数据为空-请先设计流程定义模型,再导出";
}else {
// 压缩包文件名
zipName = model.getName() + "." + model.getKey() + ".zip";
// 将xml添加到压缩包中(指定xml文件名:请假流程.bpmn20.xml )
zipos.putNextEntry(new ZipEntry(model.getName() + ".bpmn20.xml"));
zipos.write(xmlBytes);
// 3. 查询流程定义模型的图片字节码
byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);
if(pngBytes != null) {
// 图片文件名(请假流程.leaveProcess.png)
zipos.putNextEntry(new ZipEntry(model.getName() + "." + model.getKey() + ".png"));
zipos.write(pngBytes);
}
}
}
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition",
"attachment; filename=" + URLEncoder.encode(zipName, "UTF-8") + ".zip");
// 刷出响应流
response.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(zipos != null) {
try {
zipos.closeEntry();
zipos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
6)部署流程
/**
* 通过模型数据部署流程定义
* @param modelId
* @return
* @throws Exception
*/
@Test
public String deploy() throws Exception {
String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
// 1. 查询流程定义模型json字节码
byte[] jsonBytes = repositoryService.getModelEditorSource(modelId);
if(jsonBytes == null) {
return "模型数据为空,请先设计流程定义模型,再进行部署";
}
// 将json字节码转为 xml 字节码,因为bpmn2.0规范中关于流程模型的描述是xml格式的,而activiti遵守了这个规范
byte[] xmlBytes = bpmnJsonXmlBytes(jsonBytes);
if(xmlBytes == null) {
return "数据模型不符合要求,请至少设计一条主线流程";
}
// 2. 查询流程定义模型的图片
byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);
// 查询模型的基本信息
Model model = repositoryService.getModel(modelId);
// xml资源的名称 ,对应act_ge_bytearray表中的name_字段
String processName = model.getName() + ".bpmn20.xml";
// 图片资源名称,对应act_ge_bytearray表中的name_字段
String pngName = model.getName() + "." + model.getKey() + ".png";
// 3. 调用部署相关的api方法进行部署流程定义
Deployment deployment = repositoryService.createDeployment()
.name(model.getName()) // 部署名称
.addString(processName, new String(xmlBytes, "UTF-8")) // bpmn20.xml资源
.addBytes(pngName, pngBytes) // png资源
.deploy();
// 更新 部署id 到流程定义模型数据表中
model.setDeploymentId(deployment.getId());
repositoryService.saveModel(model);
return "部署成功";
}
5.3 流程操作—流程部署
一个流程部署有两种方式,一是通过程定义模型数据,部署流程定义;二是通过 .zip 流程压缩包进行部署的流程定义。
- 通过流程定义模型数据,部署流程定义
@Test
public void deploy() throws Exception {
//1、查询流程定义模型的json字节码
// 将json字节码转为xml字节码,因为bpmn2.0规范中关于流程模型的描述是xml格式的,而activiti遵守了这个规范
String modelId = "dc3m9c9b-1ea6-11ec-9a56-28d0ea7310b3";
byte[] jsonBytes = repositoryService.getModelEditorSource(modelId);
if (jsonBytes == null) {
System.out.println("模型数据为空,请先设计流程定义模型,再进行部署");
return;
}
JsonNode jsonNode = objectMapper.readTree(jsonBytes);
BpmnModel bpmnModel = new BpmnJsonConverter().convertToBpmnModel(jsonNode);
byte[] xmlByte = null;
if (bpmnModel.getProcesses().size() != 0) {
xmlByte = new BpmnXMLConverter().convertToXML(bpmnModel);
if (xmlByte == null) {
System.out.println("数据模型不符合要求,请至少设计一条主线流程");
return;
}
}
//2、查询流程定义模型的图片
byte[] pngBytes = repositoryService.getModelEditorSourceExtra(modelId);
// 查询模型的基本信息
Model model = repositoryService.getModel(modelId);
// xml资源的名称,对应ACT_GE_BYTEARRAY表中的name_字段
String processName = model.getName() + ".bpmn20.xml";
// 图片资源名称,对应ACT_GE_BYTEARRAY表中的name_字段
String pngName = model.getName() + "." + model.getKey() + ".png";
//3、调用部署相关的api方法进行部署流程定义
// 操作一下表
// ACT_RE_PROCDEF 新增数据: 流程定义数据
// ACT_RE_DEPLOYMENT 新增数据: 流程部署数据
// ACT_GE_BYTEARRAY 新增数据:将当前流程图绑定到此流程定义部署数据上
Deployment deployment = repositoryService.createDeployment()
.name(model.getName()) //部署名称
.addString(processName, new String(xmlByte)) //bpmn2.0资源
.addBytes(pngName, pngBytes) //png资源
.deploy();
// 更新 部署id 到流程定义模型表中
// ACT_RE_MODEL 更新部署id
model.setDeploymentId(deployment.getId());
repositoryService.saveModel(model);
System.out.println("部署成功");
}
- 通过 .zip 流程压缩包进行部署的流程定义
@Test
public void deployByZip() throws Exception {
File file = new File("D:/请假流程test01.leave.zip");
String filename = file.getName();
// 压缩包输入流
ZipInputStream zipis = new ZipInputStream(new FileInputStream(file));
// 创建部署实例
DeploymentBuilder deployment = repositoryService.createDeployment();
// 添加zip流
deployment.addZipInputStream(zipis);
// 部署名称
deployment.name(filename.substring(0, filename.indexOf(".")));
// 执行部署流程定义
deployment.deploy();
System.out.println("zip压缩包方式部署流程定义完成");
}
2)删除部署信息
/**
* 根据部署ID删除流程定义部署信息:
* ACT_GE_BYTEARRAY、
* ACT_RE_DEPLOYMENT、
* ACT_RE_PROCDEF、
* -----下面是流程实例时产生的数据会被一起删除
* ACT_RU_IDENTITYLINK、
* ACT_RU_EVENT_SUBSCR
*/
@Test
public void delete() {
try {
// 部署ID ACT_RE_DEPLOYMENT
String deploymentId = "d298a2f5-1db4-11ec-9048-28d0ea7310b3";
// 不带级联的删除:如果有正在执行的流程,则删除失败抛出异常;不会删除 ACT_HI_和 历史表数据
repositoryService.deleteDeployment(deploymentId);
// 级联删除:不管流程是否启动,都能可以删除;并删除历史表数据。
//repositoryService.deleteDeployment(deploymentId, true);
System.out.println("删除流程定义部署信息成功");
} catch (Exception e) {
e.printStackTrace();
if (e.getMessage().indexOf("a foreign key constraint fails") > 0) {
System.out.println("有正在执行的流程,不允许删除");
} else {
System.out.println("删除失败,原因:" + e.getMessage());
}
}
}
3)查询部署的流程定义数据
/**
* 查询部署的流程定义数据 ACT_RE_PROCDEF
* 需求:如果有多个相同流程定义标识key的流程时,只查询其最新版本
*/
@Test
public void getProcessDefinitionList() {
// 1. 获取 ProcessDefinitionQuery
ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
// 条件查询
query.processDefinitionNameLike("%请假%");
// 有多个相同标识key的流程时,只查询其最新版本
query.latestVersion();
// 按流程定义key升序排列
query.orderByProcessDefinitionKey().asc();
// 当前查询第几页
int current = 1;
// 每页显示多少条数据
int size = 5;
// 当前页第1条数据下标
int firstResult = (current - 1) * size;
// 开始分页查询
List<ProcessDefinition> definitionList = query.listPage(firstResult, size);
for (ProcessDefinition pd : definitionList) {
System.out.print("流程部署ID:" + pd.getDeploymentId());
System.out.print(",流程定义ID:" + pd.getId());
System.out.print(",流程定义Key:" + pd.getKey());
System.out.print(",流程定义名称:" + pd.getName());
System.out.print(",流程定义版本号:" + pd.getVersion());
System.out.println(",状态:" + (pd.isSuspended() ? "挂起(暂停)" : "激活(开启)"));
}
// 用于前端显示页面,总记录数
long total = query.count();
System.out.println("满足条件的流程定义总记录数:" + total);
}
4)挂起或者激活流程定义
/**
* 通过流程定义id,挂起或激活流程定义
*/
@Test
public void updateProcessDefinitionState() {
// 流程定义ID ACT_RE_PROCDEF
String definitionId = "leave:2:b9227310-c8f4-11eb-acbf-aa2e2e85fc05";
// 流程定义对象
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(definitionId)
.singleResult();
// 获取当前状态是否为:挂起
boolean suspended = processDefinition.isSuspended();
//对应 act_re_procdef 表中的 SUSPENSION_STATE_ 字段,1是激活,2是挂起
if (suspended) {
// 如果状态是:挂起,将状态更新为:激活,
// 参数1: 流程定义id;参数2:是否级联激活该流程定义下的流程实例;参考3:设置什么时间激活这个流程定义,如果 null 则立即激活)
repositoryService.activateProcessDefinitionById(definitionId, true, null);
} else {
// 如果状态是:激活,将状态更新为:挂起
// 参数 (流程定义id,是否挂起,激活时间)
repositoryService.suspendProcessDefinitionById(definitionId, true, null);
}
}
5)导出流程定义相关文件
/**
* 导出下载流程定义相关的文件(.bpmn20.xml流程描述或.png图片资源)
* @throws Exception
*/
@Test
public void exportProcDefFile() throws Exception {
// 流程定义id ACT_RE_PROCDEF
String processDefinitionId = "leave:2:4ccffb81-1b68-11ec-9d87-28d0ea7310b3";
ProcessDefinition processDefinition = repositoryService.getProcessDefinition(processDefinitionId);
//获取的是 xml 资源名
String resourceName = processDefinition.getResourceName();
//获取的是 png 资源名
String diagramResourceName = processDefinition.getDiagramResourceName();
//查询到相关的资源输入流
InputStream input = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), resourceName);
File file = new File("D:/" + resourceName);
FileOutputStream out = new FileOutputStream(file);
// 输入流,输出流的转换
IOUtils.copy(input, out);
// 关闭流
out.close();
input.close();
System.out.println("下载流程定义 资源(xml、png) 文件成功");
}
5.4 流程操作—流程实例
1)启动流程
/**
* 启动流程实例
* ACT_HI_TASKINST 任务实例
* ACT_HI_PROCINST 流程实例
* ACT_HI_ACTINST 节点实例
* ACT_HI_IDENTITYLINK 流程实例相关办理人
* ACT_RU_EXECUTION 运行时流程执行实例表
* ACT_RU_TSK 运行时流程任务实例
* ACT_RU_IDENTITYLINK 运行时流程实例相关办理人
*/
@Test
public void startProcessInstance() {
// 流程定义唯一标识key ACT_RE_PROCDEF 字段KEY_
String processKey = "leave";
//业务id
String businessKey = "10000";
//启动当前流程实例的用户
Authentication.setAuthenticatedUserId("zhangsan");
//启动流程实例(流程定义唯一标识key,业务id),采用流程key对应的最新版本的流程定义数据
ProcessInstance pi = runtimeService.startProcessInstanceByKey(processKey, businessKey);
//将流程定义名称 作为 流程实例名称
runtimeService.setProcessInstanceName(pi.getProcessInstanceId(), pi.getProcessDefinitionName());
System.out.println("启动流程实例成功: " + pi.getProcessInstanceId());
}
启动流程实例时可以将业务配置绑定到当前流程上,办理人以实际业务办理人或候选人
2)查询正在运行的流程实例
/**
* 查询正在运行中的流程实例
*/
@Test
public void getProcInstListRunning() {
// //查询正在运行的流程实例 所有
// List<ProcessInstance> list = runtimeService.createProcessInstanceQuery().list();
//查询正在运行的流程实例 按照条件查询
List<ProcessInstance> list = runtimeService.createProcessInstanceQuery().processInstanceNameLike("%请假%").list();
list.stream().forEach(l -> {
System.out.println("流程定义key: " + l.getProcessDefinitionKey());
System.out.println("流程定义版本号: " + l.getProcessDefinitionVersion());
System.out.println("流程实例Id: " + l.getProcessInstanceId());
System.out.println("流程实例名称: " + l.getName());
System.out.println("业务key(业务主键id): " + l.getBusinessKey());
System.out.println("发起人: " + l.getStartUserId());
System.out.println("流程实例状态: " + (l.isSuspended() ? "已挂起(暂停)" : "已激活(启动)"));
});
}
当前运行实例只有一个任务在运行
3)挂起或者激活流程实例
/**
* 挂起或激活流程实例
*/
@Test
public void updateProInstState() {
//流程实例id ACT_RU_EXECUTION 字段 ROOT_PROC_INST_ID_
String proInstId = "d2f5ca6c-1c01-11ec-ba31-28d0ea7310b3";
//查询流程实例对象
ProcessInstance processInstance =
runtimeService.createProcessInstanceQuery().processInstanceId(proInstId).singleResult();
//获取当前流程实例状态是否为:挂起(暂停)
boolean suspended = processInstance.isSuspended();
//判断
if (suspended) {
//如果状态是:挂起,则更新为激活状态 ACT_RU_EXECUTION 字段 SUSPENSION_STATE_ 1 激活
runtimeService.activateProcessInstanceById(proInstId);
System.out.println("激活流程实例");
} else {
//如果状态是:激活,则更新为挂起状态 CT_RU_EXECUTION 字段 SUSPENSION_STATE_ 2 挂起
runtimeService.suspendProcessInstanceById(proInstId);
System.out.println("挂起流程实例");
}
}
4)删除流程实例
/**
* 删除流程实例
* ACT_RU_IDENTITYLINK
* ACT_RU_INTEGRATION
* ACT_RU_TASK
*/
@Test
public void deleteProcInst() {
//流程实例id ACT_RU_EXECUTION 字段 ROOT_PROC_INST_ID_
String proInstId = "7bf4bfe2-1db6-11ec-ba03-28d0ea7310b3";
//查询流程实例对象
ProcessInstance processInstance =
runtimeService.createProcessInstanceQuery().processInstanceId(proInstId).singleResult();
if (processInstance == null) {
System.out.println("该实例不存在");
return;
}
//删除流程实例(流程实例id,删除原因),不会删除流程实例相关历史数据
runtimeService.deleteProcessInstance(proInstId, "XXX作废了当前流程申请");
//删除流程实例相关历史数据
historyService.deleteHistoricProcessInstance(proInstId);
}
5)查询已结束流程实例
/**
* 查询已结束的流程实例
*
* @param req
* @return
*/
@Test
public void getProcInstFinish() {
HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery()
.finished() //已结束
.orderByProcessInstanceEndTime()
.desc();
List<HistoricProcessInstance> list =
query.list();
list.stream().forEach(l -> {
System.out.println(l.getId());//流程实例id
System.out.println(l.getName());//流程名称
System.out.println(l.getProcessDefinitionKey());//流程定义key
System.out.println(l.getProcessDefinitionVersion());//流程定义版本
System.out.println(l.getStartUserId());//流程发起人
System.out.println(l.getBusinessKey());//业务ID
System.out.println(l.getStartTime()));//流程实例开始时间
System.out.println(l.getEndTime()));//流程实例结束时间
System.out.println(l.getDeleteReason());//删除原因
});
}
5.5 流程操作—流程任务
1)查询当前用户办理人或候选人任务
/**
* 查询当前用户是办理人或候选人的待办任务 pc
*
* @param req
* @return
*/
@Test
public void findWaitTask() {
//办理人(当前用户)
String assignee = "1132";
TaskQuery query = taskService.createTaskQuery()
.taskCandidateOrAssigned(assignee)// 作为办理人或候选人
.orderByTaskCreateTime().desc();
if (StringUtils.isNotEmpty(req.getTaskName())) {
query.taskNameLike("%" + req.getTaskName() + "%");
}
//分页查询
List<Task> taskList = query.list;
taskList.stream().forEach(l -> {
System.out.println(l.getId());//任务ID
System.out.println(l.getName());//任务名称
System.out.println(l.isSuspended() );//状态
System.out.println(l.getAssignee());//流程定义版本
System.out.println(l..getAssignee());//办理人
System.out.println(l.getProcessInstanceId());//流程实例ID
System.out.println(l.getCreateTime()));//任务创建时
System.out.println(l.getExecutionId());//执行对象ID
System.out.println(l.getProcessDefinitionId());//流程定义ID
});
2)获得下一节点任务
@Tes
public List<Map<String, Object>> getNextNodeInfo() {
String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3"
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null) {
return null;
}
//获取当前模型
BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
//根据任务节点id获取当前节点
FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
//封装下一个节点信息
List<Map<String, Object>> nextNodes = new ArrayList<>();
getNextNodes(flowElement, nextNodes);
return nextNodes;
}
获取当前审批节点,用来设置下一节点办理人,如果有排他或并行网关,需要获取其集合
/**
* 判断当前节点的下一节点是人工任务的集合
*
* @param flowElement 当前节点
* @param nextNodes 下节点名称集合
*/
public void getNextNodes(FlowElement flowElement, List<Map<String, Object>> nextNodes) {
//获取当前节点的连线信息
List<SequenceFlow> outgoingFlows = ((FlowNode) flowElement).getOutgoingFlows();
for (SequenceFlow outgoingFlow : outgoingFlows) {
//下一节点
FlowElement nextFlowElement = outgoingFlow.getTargetFlowElement();
if (nextFlowElement instanceof EndEvent) {
//结束节点
break;
} else if (nextFlowElement instanceof UserTask) {
Map<String, Object> node = new HashMap<>();
//用户任务 获取节点id和名称
node.put("id", nextFlowElement.getId());
node.put("name", nextFlowElement.getName());
nextNodes.add(node);
} else if (nextFlowElement instanceof ParallelGateway || //并行网关
nextFlowElement instanceof ExclusiveGateway) { //排他网关
//递归 继续找下一节点
getNextNodes(nextFlowElement, nextNodes);
}
}
}
3)拾取任务
/**
* 签收(拾取)任务
*/
@Test
public String claimTask() {
String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3";
String userId = "zhangsan";
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
if (StringUtils.isBlank(task.getAssignee())) {
taskService.claim(taskId, userId);
return "操作成功";
} else {
return "操作失败";
}
}
这里取消拾取任务只需要在claim时将user置为null,就可以取消拾取任务
4)任务转交
/**
* 任务转交
*/
@Test
public void turnTask() {
String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3";
String assigneeUserKey = "校长";
String userId = "zhangsan";
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
taskService.setAssignee(taskId, assigneeUserKey)
//添加处理意见
taskService.addComment(taskId, task.getProcessInstanceId(), message);
}
5)完成任务
/**
* 执行任务
*/
@Test
public void completeTask() {
//每个节点就是一个任务, ACT_RU_TASK ID_
String taskId = "b10ec157-1c78-11ec-93ec-28d0ea7310b3";
//执行任务 complete 这个方法还有别的参数, complete(String taskId, Map<String, Object> variables);
//variables 表示流程变量,可以修改,覆盖之前流程实例初始化时的流程变量
taskService.complete(taskId);
}
6)设置下一节点人
/**
* 完成当前节点时,设置下一节点任务办理人(上面输入框指定的那个办理人)
*/
@Test
public void completeTaskSetNextAssignee() {
String taskId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null) {
System.out.println("任务id错误,无法查询到相关任务");
} else {
//执行任务
taskService.complete(taskId);
//查询下一个任务
List<Task> taskList = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list();
//下一个节点任务
if (!CollectionUtils.isEmpty(taskList)) {
//针对每个任务分配审批人
for (Task t : taskList) {
//当前任务有审批人,则不设置新的审批人
if (StringUtils.isNotEmpty(t.getAssignee())) {
System.out.println("当前任务有审批人,不设置新的审批人");
continue;
}
//分配新的审批人
String assignee = "lisi";
taskService.setAssignee(t.getId(), assignee);
}
}
}
}
7)获取已完成任务用于节点跳转
@Test
public List<Map<String, Object>> getBackNodes() {
String taskId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
String userId = "zhangsan";
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(username)
.singleResult();
if (task == null) {
return null;
}
// 不把当前节点查询出来, 没有办理完的节点不查询,每条数据都有一个唯一值,我们使用随机数
String sql = "select rand() AS ID_, t2.* from " +
" ( select distinct t1.TASK_DEF_KEY_, t1.NAME_ from " +
" ( select ID_, RES.TASK_DEF_KEY_, RES.NAME_, RES.START_TIME_, RES.END_TIME_ " +
" from ACT_HI_TASKINST RES " +
" WHERE RES.PROC_INST_ID_ = #{processInstanceId} and TASK_DEF_KEY_ != #{taskDefKey}" +
" and RES.END_TIME_ is not null order by RES.START_TIME_ asc " +
" ) t1 " +
" ) t2";
List<HistoricTaskInstance> list =
historyService.createNativeHistoricTaskInstanceQuery()
.sql(sql)
.parameter("processInstanceId", task.getProcessInstanceId())
.parameter("taskDefKey", task.getTaskDefinitionKey())
.list();
List<Map<String, Object>> list = new ArrayList<>();
list.forEach(hit -> {
Map<String, Object> data = new HashMap<>();
data.put("activityId", hit.getTaskDefinitionKey());
data.put("activityName", hit.getName());
list.add(data);
});
return list;
}
8)节点跳转
/**
* 1. 取得当前节点信息
* 2. 获取驳回的目标节点信息
* 要考虑并行网关:从选中的目标节点的上级节点(如:并行网关),找到其上级节点的所有子节点,并行 网关就会有多条子节点
* 3. 将当前节点出口指定为驳回的目标节点,(并行网关是多条)
* 4. 完成当前节点任务,删除执行表 is_active_=0数据,不然并行汇聚不向后流转;删除其他并行任务,
* 5. 分配目标节点原办理人。
*
* @param taskId 当前任务id
* @param targetActivityId 回滚节点 TaskDefinitionKey (getBackNodes这个方法设置)
* @param userId
* @return
*/
@Test
public void backProcess(String taskId, , String userId) {
String taskId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
String userId = "zhangsan";
String targetActivityId = "fb7643c2-01ea-08ac-b098-82c0da3710f3"
//----------------------第一部分
// 1. 查询当前任务信息
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(username)
.singleResult();
if (task == null) {
return "Error:当前任务不存在或你不是任务办理人";
}
// 2. 获取流程模型实例 BpmnModel
BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
// 3. 当前节点信息
FlowNode curFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(task.getTaskDefinitionKey());
// 4. 获取当前节点的原出口连线
List<SequenceFlow> sequenceFlowList = curFlowNode.getOutgoingFlows();
// 5. 临时存储当前节点的原出口连线
List<SequenceFlow> oriSequenceFlows = new ArrayList<>();
oriSequenceFlows.addAll(sequenceFlowList);
// 6. 将当前节点的原出口清空
sequenceFlowList.clear();
//----------------------第二部分
// 7. 获取目标节点信息
FlowNode targetFlowNode = (FlowNode) bpmnModel.getFlowElement(targetActivityId);
// 8. 获取驳回的新节点
// 获取目标节点的入口连线
List<SequenceFlow> incomingFlows = targetFlowNode.getIncomingFlows();
// 存储获取驳回的新的流向
List<SequenceFlow> allSequenceFlow = new ArrayList<>();
for (SequenceFlow incomingFlow : incomingFlows) {
// 找到入口连线的源头(获取目标节点的父节点)
FlowNode source = (FlowNode) incomingFlow.getSourceFlowElement();
// 获取目标节点的父组件的所有出口,
List<SequenceFlow> sequenceFlows;
if (source instanceof ParallelGateway) {
// 并行网关的出口有多条连线:根据目标入口连线的父节点的出口连线,其所有出口连线才是驳回的真实节点
sequenceFlows = source.getOutgoingFlows();
} else {
// 其他类型,将目标入口作为当前节点的出口
sequenceFlows = targetFlowNode.getIncomingFlows();
}
// 找到后把它添加到集合作为新方向
allSequenceFlow.addAll(sequenceFlows);
}
// 9. 将当前节点的出口设置为新节点
curFlowNode.setOutgoingFlows(allSequenceFlow);
//----------------------第三部分
//流程实例id
String procInstId = task.getProcessInstanceId();
// 10. 完成当前任务,流程就会流向目标节点创建新目标任务
// 删除已完成任务,删除已完成并行任务的执行数据 act_ru_execution
List<Task> list = taskService.createTaskQuery().processInstanceId(procInstId).list();
list.forEach(t -> {
// 当前任务id
if (taskId.equals(t.getId())) {
// 当前任务,完成当前任务
String message = String.format("【%s 驳回任务 %s => %s】",
username, task.getName(), targetFlowNode.getName());
taskService.addComment(t.getId(), procInstId, message);
// 完成任务,就会进行驳回到目标节点,产生目标节点的任务数据
taskService.complete(taskId);
// 删除执行表中 is_active_ = 0的执行数据, 使用command自定义模型
DelelteExecutionCommand deleteExecutionCMD = new DelelteExecutionCommand(task.getExecutionId());
managementService.executeCommand(deleteExecutionCMD);
} else {
// 删除其他未完成的并行任务
// taskService.deleteTask(taskId); // 注意这种方式删除不掉,会报错:流程正在运行中无法删除。
// 使用command自定义命令模型来删除,直接操作底层的删除表对应的方法,对应的自定义是否删除
DeleteTaskCommand deleteTaskCMD = new DeleteTaskCommand(t.getId());
managementService.executeCommand(deleteTaskCMD);
}
});
// 11. 查询目标任务节点历史办理人
List<Task> newTaskList = taskService.createTaskQuery().processInstanceId(procInstId).list();
for (Task newTask : newTaskList) {
// 取之前的历史办理人
HistoricTaskInstance oldTargerTask = historyService.createHistoricTaskInstanceQuery()
.taskDefinitionKey(newTask.getTaskDefinitionKey()) // 节点id
.processInstanceId(procInstId)
// .finished() // 已经完成才是历史
.processFinished()
.orderByTaskCreateTime().desc() // 最新办理的在最前面
.list().get(0);
taskService.setAssignee(newTask.getId(), oldTargerTask.getAssignee());
}
//----------------------第四部分
// 12. 完成驳回功能后,将当前节点的原出口方向进行恢复
curFlowNode.setOutgoingFlows(oriSequenceFlows);
}
5.6 流程操作—流程历史
1)查询审批历史记录
@Test
public void getHistoryInfoList() {
String procInstId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3";
//查询流程人工任务历史数据
List<HistoricActivityInstance> historicActivityInstances =
historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricActivityInstanceStartTime().asc()
.list();
for (HistoricTaskInstance hti : historicTaskInstanceList)
System.out.println(hti.getId()); //任务id
System.out.println(hti.getName()); //任务名称
System.out.println(hti.getProcessInstanceId()); //流程实例ID
System.out.println(hti.getStartTime()); //开始时间
System.out.println(hti.getEndTime()); //结束时间
System.out.println(hti.getAssignee()); //办理人
}
这里也可以配置业务的businessKey来查询相应的历史任务
2)获取审批历史记录图
/**
* 获取流程实例审批历史
*/
@Test
public void getHistoryProcessImage(HttpServletResponse response) {
String procInstId = "ab7567f5-1cfb-11ec-a067-28d0ea7310b3",
InputStream imageStream = null;
try {
// 通过流程实例ID获取历史流程实例
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery()
.processInstanceId(procInstId)
.singleResult();
// 获取流程定义Model对象
BpmnModel bpmnModel =
repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
// 创建流程图生成器
CustomProcessDiagramGenerator generator = new CustomProcessDiagramGenerator();
//通过流程实例ID获取流程中已经执行的节点,按照执行先后顺序排序
List<HistoricActivityInstance> historicActivityInstances =
historyService.createHistoricActivityInstanceQuery()
.processInstanceId(procInstId)
.orderByHistoricActivityInstanceStartTime().desc()
.list();
// 将已经执行的节点id放入高亮显示节点集合
List<String> highLightedActivityIdList =
historicActivityInstances.stream().map(HistoricActivityInstance::getActivityId)
.collect(Collectors.toList());
// 通过流程实例ID获取流程中正在执行的节点
List<Execution> runningActivityInstanceList =
runtimeService.createExecutionQuery().processInstanceId(procInstId).list();
List<String> runningActivityIdList = new ArrayList<>();
for (Execution execution : runningActivityInstanceList) {
if (!StringUtils.isEmpty(execution.getActivityId())) {
runningActivityIdList.add(execution.getActivityId());
}
}
// 获取已经流经的流程线,需要高亮显示流程已经发生流转的线id集合
List<String> highLightedFlowsIds =
generator.getHighLightedFlows(bpmnModel, historicActivityInstances);
// 使用自定义配置获得流程图表生成器,并生成追踪图片字符流
imageStream =
generator.generateDiagramCustom(bpmnModel,
highLightedActivityIdList,
runningActivityIdList,
highLightedFlowsIds,
"宋体",
"微软雅黑",
"黑体");
// 输出资源内容到相应对象
response.setContentType("image/svg+xml");
byte[] bytes = IOUtils.toByteArray(imageStream);
OutputStream outputStream = response.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (imageStream != null) {
try {
imageStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3)查询指定用户的已处理任务
@Test
public void findCompleteTask() {
//用户id
String userId = "meng";
List<HistoricTaskInstance> historicTaskInstances =
historyService.createHistoricTaskInstanceQuery()
.taskAssignee(userId)
.orderByTaskCreateTime().desc()
.finished()
.list();
historicTaskInstances.forEach(h -> {
System.out.println("任务id:" + h.getId());
System.out.println("任务名称:" + h.getName());
System.out.println("任务的办理任:" + h.getAssignee());
System.out.println("任务的开始时间:" + h.getStartTime());
System.out.println("任务的结束时间" + h.getEndTime());
System.out.println("流程实例ID:" + h.getProcessInstanceId());
System.out.println("流程定义ID:" + h.getProcessDefinitionId());
System.out.println("业务唯一标识:" + h.getBusinessKey());
});
}
4)删除已结束的流程实例
/**
* 删除已结束的流程实例
* ACT_HI_DETAIL
* ACT_HI_VARINST
* ACT_HI_TASKINST
* ACT_HI_PROCINST
* ACT_HI_ACTINST
* ACT_HI_IDENTITYLINK
* ACT_HI_COMMENT
*/
@Test
public void deleteFinishProcInst() {
String procInstId = "4c772281-1c75-11ec-b888-28d0ea7310b3";
//查询流程实例是否已结束
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery()
.processInstanceId(procInstId)
.finished()
.singleResult();
if (historicProcessInstance == null) {
System.out.println("流程实例不存在或未结束");
return;
}
historyService.deleteHistoricProcessInstance(procInstId);
}
六、Activiti监听器
6.1 监听器分配
监听器分配,分配的是什么,是任务执行人的分配。
- Event(Create、Assignment、Delete、All)
- Create: 任务创建后触发
- Assignment:任务分配后触发
- Delete: 任务完成后触发
- All: 所有事件发生都触发
在我们绘制流程图时,可以对这个岗位进行事件监听,设置它触发条件,当节点任务发生变化时做出相应的处理
- 使用监听器需要实现TaskListener接口
public class ManagementTakListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
//任务id
String taskId = delegateTask.getId();
//监听事件 create\delete\assignment\complete
String eventName = delegateTask.getEventName();
HistoryService historyService =
SpringUtil.getBean(HistoryServiceImpl.class);
//任务创建时执行
if ("班主任".equals(delegateTask.getName())
&& "create".equalsIgnoreCase(delegateTask.getEventName()))
String processInstanceId = delegateTask.getProcessInstanceId()
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery()
.processInstanceId(delegateTask.getProcessInstanceId())
.singleResult();
String userId = historicProcessInstance.getStartUserId();
delegateTask.addCandidateUser(userId);
}
}
//任务完成时
if ("班主任".equals(delegateTask.getName())
&& "complete".equalsIgnoreCase(delegateTask.getEventName())) {
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery()
.processInstanceId(delegateTask.getProcessInstanceId())
.singleResult();
//TODO审批意见回调
}
}
七、Activiti安全验证
7.1 去除activiti内置的sercurity
acitiviti模型设计器整合到Spring boot原工程中时需要排除security安全验证,启动项目后访问所有界面都被拦截到登录界面
- 没有排除security安全验证出现错误如下
Caused by: java.io.FileNotFoundException: class path
resource [org/springframework/security/config/annotation/authentication
/configurers/GlobalAuthenticationConfigurerAdapter.class]
cannot be opened because it does not exist
- 去除方法
1)在启动类上加上如下注解
2)继承WebSecurityConfigurerAdapter
@SpringBootApplication(exclude ={
org.activiti.spring.boot.SecurityAutoConfiguration.class,
SecurityAutoConfiguration.class,
ManagementWebSecurityAutoConfiguration.class})
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/**").permitAll();
}
八、总结
总结来说工作流的工作流程从流程模型—>流程部署—>流程启动。在配置模型时可以配置业务路由绑定实际业务,在流程启动时可以设置业务办理人。通过模型ID到流程定义ID,从流程定义ID到流程实例ID,从流程实例ID获取当前实例任务ID,历史节点信息,从而了解整个流程的流动方向和流动状态,流程定义绑定业务businessKey,从而嵌入实际业务逻辑。