文章目录

  • 楔子
  • Activiti 介绍
    • Activiti 官网
    • BPMN 建模语言
  • Activiti 使用步骤
    • 引入依赖
    • 添加配置
      • 日志配置
      • Activiti 配置
    • 初始化数据库表
      • 新建数据库 activiti
      • 新建单元测试类 TestCreateTable
      • 加餐
    • 表结构解析
      • 总览
      • 详解
    • 核心类解析
    • 安装插件
  • 常用API介绍
    • 定制流程
    • 部署流程
    • 启动实例
    • 任务查询
    • 任务处理
    • 流程查询
    • 流程历史信息查询
    • 流程资源下载
    • Businesskey
    • 流程变量
      • 小例
      • 作用域
      • 加餐
    • 网关
      • 排他网关Exclusive Gateway
      • 并行网关Parallel Gateway
      • 包含网关Inclusive Gateway
      • 事件网关Event Gateway
    • 任务组
  • Activiti与Spring整合
  • Activiti与SpringBoot整合
  • 附件

楔子

本文主要介绍了 Activiti 的相关使用与集成,适合想要学习 Activiti 的读者食用。

Activiti 介绍

Activiti 是目前使用最为广泛的开源工作流引擎之一,在小七 2017 年走向程序员这一条不归路的时候,它就已经是开源工作流引擎的老大哥了。

Activiti 官网

学习一门新技术/新框架,我们第一件事就是从它的官网入手,下面贴出 Activiti 的官网

Open Source Business Automation | Activiti

从官网的描述,我们可以知道 Activiti 是领先的轻量级的、以 java 为中心的开源 BPMN 引擎,可以支持现实世界的流程自动化需求。 Activiti Cloud 是新一代的业务自动化平台,提供了一组旨在在分布式基础架构上运行的云原生构建块。

BPMN 建模语言

官网既然提到了 BPMN,那么首先我们必须要知道什么是 BPM。BPM 即 Business Process Managemenet,业务流程管理。是一种规范化的构造端到端的业务流程,以持续的提高组织业务效率。在常见的商业管理教育如 EMBA、MBA 中都包含了 BPM 的课程。 说人话就是,比如你请假,需要向你老板提交一个申请,你老板批准后,你才能休假,这就是一个请假流程。

而 BPMN 是 Business Process Model And Notation 业务流程模型和符号,用来描述业务流程的一种建模标准。说人话,就是“书同文,车同轨”,大家用统一的符号,来描述我们的各个业务流程,比如说请假流程,就可以抽象如下如所示:

这里贴出常用的图形所代表的意义,让各位读者先混个眼熟。

Activiti 使用步骤

新建一个 Maven 项目,项目目录结构如下:

引入依赖

在最外层 pom 中统一定义依赖

<properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><slf4j.version>1.6.6</slf4j.version><log4j.version>1.2.12</log4j.version><activiti.version>7.1.0.M6</activiti.version><activiti.cloud.version>7.0.0.Beta1</activiti.cloud.version><mysql.version>8.0.20</mysql.version><java.version>1.8</java.version><slf4j.version>1.6.6</slf4j.version><log4j.version>1.2.12</log4j.version><mybatis.version>3.4.5</mybatis.version><junit.version>4.12</junit.version><commonsio.version>2.6</commonsio.version><dbcp.version>1.4</dbcp.version>
</properties>

在 BaseActivitiDemo 项目下的 pom 中引入依赖

<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></dependency><!-- mysql驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version></dependency><!-- mybatis --><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>${mybatis.version}</version></dependency><!-- 链接池 --><dependency><groupId>commons-dbcp</groupId><artifactId>commons-dbcp</artifactId><version>${dbcp.version}</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>${commonsio.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</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>

添加配置

日志配置

在 BaseActivitiDemo 项目的 resource 下新建 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=D:\activiti7.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

Activiti 配置

在 BaseActivitiDemo 项目的 resource 下新建 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"xmlns:context="http://www.springframework.org/schema/context"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://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"><!-- 这里可以使用 链接池 dbcp--><bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"><property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /><property name="url" value="jdbc:mysql://localhost:3306/activiti?serverTimezone=GMT%2B8" /><property name="username" value="root" /><property name="password" value="123456" /><property name="maxActive" value="3" /><property name="maxIdle" value="1" /></bean><bean id="processEngineConfiguration"class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration"><!-- 引用数据源 上面已经设置好了--><property name="dataSource" ref="dataSource" /><!-- activiti 数据库表处理策略 --><property name="databaseSchemaUpdate" value="true"/></bean>
</beans>

初始化数据库表

新建数据库 activiti

新建单元测试类 TestCreateTable

内容如下:

public class TestCreateTable {/*** 生成 activiti 的数据库表*/@Testpublic void testCreateDbTableByDefault() {// 默认创建方式ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();System.out.println(processEngine);}
}

执行测试代码后,控制台会打印许多 sql 语句,再次观察 activiti 数据库,我们会发现多了 25 张表。

加餐

我们没有指定配置文件,程序是怎么创建这些表的呢?

我们跟进 getDefaultProcessEngine 方法可以发现,它其实是个包装方法,内部调用的是 getProcessEngine(string)。

public static ProcessEngine getDefaultProcessEngine() {return getProcessEngine(NAME_DEFAULT);
}

继续看 getProcessEngine(string),因为我们现在并没有初始化,所以会调用 init()方法。

public static ProcessEngine getProcessEngine(String processEngineName) {if (!isInitialized()) {init();}return processEngines.get(processEngineName);
}

从 init()方法中圈红的地方我们可以知道,框架默认会去读取 classpath:下的 activiti.cfg.xml 和 activiti-context.xml 两个配置文件的。

表结构解析

总览

让我们再来看一下这25张表

虽然表很多,但是仔细观察,我们会发现Activiti 使用到的表都是 ACT_ 开头的。表名的第二部分用两个字母表明表的用途。

  • 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_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 存放运行时流程变量
其他 act_evt_log 存放事件日志
act_procdef_info 存放流程定义的动态变更信息

核心类解析

类名 解释
RepositoryService 是activiti的资源管理类,提供了管理和控制流程发布包和流程定义的操作。
RuntimeService Activiti的流程运行管理类。可以从这个类中获取流程执行的信息
TaskService Activiti的任务管理类。可以从这个类中获取任务的信息。
HistoryService Activiti的历史管理类,可以查询历史信息。
ManagerService Activiti的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能。

安装插件

这里主要讲idea的插件,直接在Plugins中搜索Activiti BPMN visualizer和JBoss jBPM,安装即可。

注:actiBPM在idea里的使用体验实在不好,这里就不推荐大家安装了。

常用API介绍

定制流程

首先我们定义一个简单的请假流程。

在resources下新建Leave.bpmn20.xml文件

右键选择view bpmn(Activiti) Diagram

可以看到以下界面

在红框中点击右键,开始画图,这里我们先选择一个开始事件

选择一个user task

选择user task方框填入以下属性

再创建一个user task,并填入以下属性

再创建一个user task,并填入以下属性

选择结束事件

最后我们得到了这样一个流程图

同时我们可以看到,左侧的xml文件发生了变化

部署流程

接下来新建测试类ActivitiDemo并添加以下内容

/*** 部署流程定义*/
@Test
public void testDeployment() {// 1、创建ProcessEngineProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、得到RepositoryService实例RepositoryService repositoryService = processEngine.getRepositoryService();// 3、使用RepositoryService进行部署Deployment deployment = repositoryService.createDeployment()// 添加bpmn资源.addClasspathResource("bpmn/Leave.bpmn20.xml")// 添加png资源.addClasspathResource("bpmn/Leave.png").name("请假申请流程").deploy();System.out.println("流程部署id:" + deployment.getId());System.out.println("流程部署名称:" + deployment.getName());
}

运行测试方法后,我们可以看到控制台打印了一些sql,整个部署过程操作了三张数据表:
act_ge_bytearray 流程资源表 ,每个流程定义对应两个资源记录,bpmn和png。

act_re_deployment 流程定义部署表,每部署一次增加一条记录 。

act_re_procdef 流程定义表,部署每个新的流程定义都会在这张表中增加一条记录。

启动实例

接下来我们启动流程实例

/*** 启动流程实例*/
@Test
public void testStartProcess() {// 1、创建ProcessEngineProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、获取RunTimeServiceRuntimeService runtimeService = processEngine.getRuntimeService();// 3、根据流程定义Id启动流程ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("Leave");System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());System.out.println("流程实例id:" + processInstance.getId());System.out.println("当前活动Id:" + processInstance.getActivityId());
}

同样我们观察控制台日志,发现涉及插入语句的的表如下

2022-08-15 21:45:39,919 1252  [           main] DEBUG ti.engine.impl.db.DbSqlSession  - inserting: org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntityImpl@3174cb09
2022-08-15 21:45:39,919 1252  [           main] DEBUG mpl.insertHistoricTaskInstance  - ==>  Preparing: insert into ACT_HI_TASKINST ( ID_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, NAME_, PARENT_TASK_ID_, DESCRIPTION_, OWNER_, ASSIGNEE_, START_TIME_, CLAIM_TIME_, END_TIME_, DURATION_, DELETE_REASON_, TASK_DEF_KEY_, FORM_KEY_, PRIORITY_, DUE_DATE_, CATEGORY_, TENANT_ID_ ) values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2022-08-15 21:45:39,921 1254  [           main] DEBUG mpl.insertHistoricTaskInstance  - ==> Parameters: 2505(String), Leave:1:4(String), 2501(String), 2502(String), 创建出差申请(String), null, 创建出差申请(String), null, employee(String), 2022-08-15 21:45:39.918(Timestamp), null, null, null, null, sid-d0797050-fe53-4ff7-8506-855ba3059d8d(String), null, 50(Integer), null, null, (String)
2022-08-15 21:45:39,922 1255  [           main] DEBUG mpl.insertHistoricTaskInstance  - <==    Updates: 1
2022-08-15 21:45:39,922 1255  [           main] DEBUG ti.engine.impl.db.DbSqlSession  - inserting: HistoricProcessInstanceEntity[superProcessInstanceId=null]
2022-08-15 21:45:39,922 1255  [           main] DEBUG .insertHistoricProcessInstance  - ==>  Preparing: insert into ACT_HI_PROCINST ( ID_, PROC_INST_ID_, BUSINESS_KEY_, PROC_DEF_ID_, START_TIME_, END_TIME_, DURATION_, START_USER_ID_, START_ACT_ID_, END_ACT_ID_, SUPER_PROCESS_INSTANCE_ID_, DELETE_REASON_, TENANT_ID_, NAME_ ) values ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2022-08-15 21:45:39,922 1255  [           main] DEBUG .insertHistoricProcessInstance  - ==> Parameters: 2501(String), 2501(String), null, Leave:1:4(String), 2022-08-15 21:45:39.893(Timestamp), null, null, null, sid-0302a100-d63d-4daa-8e3a-fbce9d28485a(String), null, null, null, (String), null
2022-08-15 21:45:39,923 1256  [           main] DEBUG .insertHistoricProcessInstance  - <==    Updates: 1
2022-08-15 21:45:39,934 1267  [           main] DEBUG InsertHistoricActivityInstance  - ==>  Preparing: insert into ACT_HI_ACTINST ( ID_, PROC_DEF_ID_, PROC_INST_ID_, EXECUTION_ID_, ACT_ID_, TASK_ID_, CALL_PROC_INST_ID_, ACT_NAME_, ACT_TYPE_, ASSIGNEE_, START_TIME_, END_TIME_, DURATION_, DELETE_REASON_, TENANT_ID_ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2022-08-15 21:45:39,935 1268  [           main] DEBUG InsertHistoricActivityInstance  - ==> Parameters: 2503(String), Leave:1:4(String), 2501(String), 2502(String), sid-0302a100-d63d-4daa-8e3a-fbce9d28485a(String), null, null, null, startEvent(String), null, 2022-08-15 21:45:39.909(Timestamp), 2022-08-15 21:45:39.91(Timestamp), 1(Long), null, (String), 2504(String), Leave:1:4(String), 2501(String), 2502(String), sid-d0797050-fe53-4ff7-8506-855ba3059d8d(String), 2505(String), null, 创建出差申请(String), userTask(String), employee(String), 2022-08-15 21:45:39.911(Timestamp), null, null, null, (String)
2022-08-15 21:45:39,936 1269  [           main] DEBUG InsertHistoricActivityInstance  - <==    Updates: 2
2022-08-15 21:45:39,936 1269  [           main] DEBUG ti.engine.impl.db.DbSqlSession  - inserting: org.activiti.engine.impl.persistence.entity.HistoricIdentityLinkEntityImpl@4d411036
2022-08-15 21:45:39,936 1269  [           main] DEBUG mpl.insertHistoricIdentityLink  - ==>  Preparing: insert into ACT_HI_IDENTITYLINK (ID_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_) values (?, ?, ?, ?, ?, ?)
2022-08-15 21:45:39,936 1269  [           main] DEBUG mpl.insertHistoricIdentityLink  - ==> Parameters: 2506(String), participant(String), employee(String), null, null, 2501(String)
2022-08-15 21:45:39,937 1270  [           main] DEBUG mpl.insertHistoricIdentityLink  - <==    Updates: 1
2022-08-15 21:45:39,938 1271  [           main] DEBUG EntityImpl.bulkInsertExecution  - ==>  Preparing: insert into ACT_RU_EXECUTION (ID_, REV_, PROC_INST_ID_, BUSINESS_KEY_, PROC_DEF_ID_, ACT_ID_, IS_ACTIVE_, IS_CONCURRENT_, IS_SCOPE_,IS_EVENT_SCOPE_, IS_MI_ROOT_, PARENT_ID_, SUPER_EXEC_, ROOT_PROC_INST_ID_, SUSPENSION_STATE_, TENANT_ID_, NAME_, START_TIME_, START_USER_ID_, IS_COUNT_ENABLED_, EVT_SUBSCR_COUNT_, TASK_COUNT_, JOB_COUNT_, TIMER_JOB_COUNT_, SUSP_JOB_COUNT_, DEADLETTER_JOB_COUNT_, VAR_COUNT_, ID_LINK_COUNT_, APP_VERSION_) values (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2022-08-15 21:45:39,939 1272  [           main] DEBUG EntityImpl.bulkInsertExecution  - ==> Parameters: 2501(String), 2501(String), null, Leave:1:4(String), null, true(Boolean), false(Boolean), true(Boolean), false(Boolean), false(Boolean), null, null, 2501(String), 1(Integer), (String), null, 2022-08-15 21:45:39.893(Timestamp), null, false(Boolean), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), null, 2502(String), 2501(String), null, Leave:1:4(String), sid-d0797050-fe53-4ff7-8506-855ba3059d8d(String), true(Boolean), false(Boolean), false(Boolean), false(Boolean), false(Boolean), 2501(String), null, 2501(String), 1(Integer), (String), null, 2022-08-15 21:45:39.908(Timestamp), null, false(Boolean), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), 0(Integer), null
2022-08-15 21:45:39,940 1273  [           main] DEBUG EntityImpl.bulkInsertExecution  - <==    Updates: 2
2022-08-15 21:45:39,940 1273  [           main] DEBUG ti.engine.impl.db.DbSqlSession  - inserting: Task[id=2505, name=创建出差申请]
2022-08-15 21:45:39,940 1273  [           main] DEBUG tity.TaskEntityImpl.insertTask  - ==>  Preparing: insert into ACT_RU_TASK (ID_, REV_, NAME_, BUSINESS_KEY_, PARENT_TASK_ID_, DESCRIPTION_, PRIORITY_, CREATE_TIME_, OWNER_, ASSIGNEE_, DELEGATION_, EXECUTION_ID_, PROC_INST_ID_, PROC_DEF_ID_, TASK_DEF_KEY_, DUE_DATE_, CATEGORY_, SUSPENSION_STATE_, TENANT_ID_, FORM_KEY_, CLAIM_TIME_, APP_VERSION_) values (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2022-08-15 21:45:39,941 1274  [           main] DEBUG tity.TaskEntityImpl.insertTask  - ==> Parameters: 2505(String), 创建出差申请(String), null, null, 创建出差申请(String), 50(Integer), 2022-08-15 21:45:39.911(Timestamp), null, employee(String), null, 2502(String), 2501(String), Leave:1:4(String), sid-d0797050-fe53-4ff7-8506-855ba3059d8d(String), null, null, 1(Integer), (String), null, null, null
2022-08-15 21:45:39,941 1274  [           main] DEBUG tity.TaskEntityImpl.insertTask  - <==    Updates: 1
2022-08-15 21:45:39,941 1274  [           main] DEBUG ti.engine.impl.db.DbSqlSession  - inserting: IdentityLinkEntity[id=2506, type=participant, userId=employee, processInstanceId=2501]
2022-08-15 21:45:39,941 1274  [           main] DEBUG kEntityImpl.insertIdentityLink  - ==>  Preparing: insert into ACT_RU_IDENTITYLINK (ID_, REV_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_, PROC_DEF_ID_) values (?, 1, ?, ?, ?, ?, ?, ?)
2022-08-15 21:45:39,942 1275  [           main] DEBUG kEntityImpl.insertIdentityLink  - ==> Parameters: 2506(String), participant(String), employee(String), null, null, 2501(String), null
2022-08-15 21:45:39,943 1276  [           main] DEBUG kEntityImpl.insertIdentityLink  - <==    Updates: 1

act_hi_actinst

act_hi_identitylink

act_hi_procinst

act_hi_taskinst

act_ru_execution

act_ru_identitylink

act_ru_task

任务查询

    /*** 查询当前个人待执行的任务*/@Testpublic void testFindPersonalTaskList() throws Exception {// 任务负责人
// String assignee = "employee";ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 创建TaskServiceTaskService taskService = processEngine.getTaskService();// 根据流程key和任务负责人查询任务List<Task> list = taskService.createTaskQuery()//流程Key.processDefinitionKey("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());}}

任务处理

    // 完成任务@Testpublic void completTask() {// 获取引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取taskServiceTaskService taskService = processEngine.getTaskService();Task task = taskService.createTaskQuery()// 流程Key.processDefinitionKey("Leave")// 要查询的负责人
//         .taskAssignee("employee")  .singleResult();taskService.complete(task.getId());}

流程查询

/*** 查询流程定义*/
@Test
public void queryProcessDefinition() {// 获取引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取repositoryServiceRepositoryService repositoryService = processEngine.getRepositoryService();ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("Leave").orderByProcessDefinitionVersion().desc().list();for (ProcessDefinition processDefinition : definitionList) {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 queryProcessInstance() {// 流程定义keyString processDefinitionKey = "Leave";ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取RunTimeServiceRuntimeService runtimeService = processEngine.getRuntimeService();List<ProcessInstance> list = runtimeService.createProcessInstanceQuery().processDefinitionKey(processDefinitionKey)//.list();for (ProcessInstance processInstance : list) {System.out.println("----------------------------");System.out.println("流程实例id:"+ processInstance.getProcessInstanceId());System.out.println("所属流程定义id:"+ processInstance.getProcessDefinitionId());System.out.println("是否执行完成:" + processInstance.isEnded());System.out.println("是否暂停:" + processInstance.isSuspended());System.out.println("当前活动标识:" + processInstance.getActivityId());System.out.println("业务关键字:" + processInstance.getBusinessKey());}
}

流程历史信息查询

/*** 查看历史信息*/
@Test
public void findHistoryInfo() {// 获取引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 获取HistoryServiceHistoryService historyService = processEngine.getHistoryService();HistoricActivityInstanceQuery instanceQuery = historyService.createHistoricActivityInstanceQuery();instanceQuery.processInstanceId("2501");instanceQuery.orderByHistoricActivityInstanceStartTime().asc();List<HistoricActivityInstance> activityInstanceList = instanceQuery.list();for (HistoricActivityInstance hi : activityInstanceList) {System.out.println("============="+hi.getActivityId()+" START=============");System.out.println(hi.getActivityId());System.out.println(hi.getActivityName());System.out.println(hi.getProcessDefinitionId());System.out.println(hi.getProcessInstanceId());System.out.println("============="+hi.getActivityId()+" END=============");}
}

流程资源下载

@Test
public void downBpmnFile() throws IOException {// 1、得到引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、获取repositoryServiceRepositoryService repositoryService = processEngine.getRepositoryService();// 3、得到查询器:ProcessDefinitionQueryProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("Leave").singleResult();// 4、通过流程定义信息,得到部署IDString deploymentId = processDefinition.getDeploymentId();// 5、通过repositoryService的方法,实现读取图片信息和bpmn信息InputStream pngInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getDiagramResourceName());// bpmn文件的流InputStream bpmnInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getResourceName());File file_png = new File("d:/Leave.png");File file_bpmn = new File("d:/Leave.bpmn20.xml");FileOutputStream bpmnOut = new FileOutputStream(file_bpmn);FileOutputStream pngOut = new FileOutputStream(file_png);IOUtils.copy(pngInput, pngOut);IOUtils.copy(bpmnInput, bpmnOut);pngOut.close();bpmnOut.close();pngInput.close();bpmnInput.close();
}

Businesskey

这一章节我们来介绍这个businessKey,顾名思义,也就是业务关键字的意思。它是Activiti给我们设计的一个拓展点,可以根据业务场景,设计成不同的数据格式,比如json等,但是要注意这个字段的长度为255。

/*** 添加业务key*/
@Test
public void addBusinessKey() {// 1、获取流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、获取RuntimeServiceRuntimeService runtimeService = processEngine.getRuntimeService();// 3、启动流程的过程中,添加businesskeyProcessInstance instance = runtimeService.startProcessInstanceByKey("Leave", "businessNo");System.out.println("businessKey==" + instance.getBusinessKey());
}

流程变量

小例

再次回顾一下我们的流程图:

之前我们定义的请假流程,每个步骤都是非常固定的,但是生活中的业务流程,往往比较复杂。这里我们完善一下请假流程,请假3天以内由组长审批,3天以上需要增加经理审批。

针对这样的流程,就需要用到流程变量了。首先我们双击以下流程连线,填入UEL表达式。

同时修改每个任务节点的责任人为UEL表达式。

添加测试代码

 /*** 启动流程的时候设置流程变量*/
@Test
public void testStartProcessAndSetVariables() {// 1、获取流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、获取RunTimeServiceRuntimeService runtimeService = processEngine.getRuntimeService();// 3、流程定义的KeyString key = "Leave";// 4、流程变量的mapMap<String, Object> variables = new HashMap<>();// 5、设置流程变量Evection evection = new Evection();// 6、设置请假日期evection.setDay(2d);// 7、把流程变量的pojo放入mapvariables.put("evection", evection);// 8、设定任务的负责人variables.put("employee", "小七");variables.put("TeamLeader", "第七人格");variables.put("Manager", "老李");// 9、启动流程runtimeService.startProcessInstanceByKey(key, variables);
}
/*** 完成个人任务*/
@Test
public void completTaskByVariables() {completTaskByVariables("小七");completTaskByVariables("第七人格");completTaskByVariables("老李");
}private void completTaskByVariables(String assingee) {// 1、流程定义的KeyString key = "Leave";// 2、获取流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 3、获取taskServiceTaskService taskService = processEngine.getTaskService();// 4、查询任务Task task = taskService.createTaskQuery().processDefinitionKey(key).taskAssignee(assingee).singleResult();if (task != null) {taskService.complete(task.getId());System.out.println(task.getId() + "----任务负责人:" + task.getAssignee() + "----任务已完成");}
}

作用域

上一节我们讲了一个关于流程变量的例子,这一节我们讲一讲流程变量的作用域。

  • Global变量
    这个是流程变量的默认作用域,作用于流程实例。上节中的例子就是Global变量变量,关键代码为:

启动流程时,设置变量

runtimeService.startProcessInstanceByKey(key,variables);

办理任务时,设置变量

taskService.complete(任务id,variables);

通过当前流程实例,设置变量

runtimeService.setVariable(流程实例id, key, pojo对象);

通过当前任务,设置变量

taskService.setVariable(任务id, "evection", pojo对象));
  • Local 变量
    这个作用域只针对一个任务或一个执行实例的范围,没有流程实例大。

关键代码为:

办理任务时,设置变量

taskService.setVariablesLocal(任务id,variables);

通过当前任务,设置变量

taskService.setVariableLocal(任务id, "evection", pojo对象));

加餐

1、读者把出差时间变为2d试一试,看老李有没有进行审批。

2、建议读者都实验一下上面的api,加深印象。

网关

在画布中单击右键,选择网关,我们可以看到有以下四种网关

排他网关Exclusive Gateway

排他网关,顾名思义就是最终只会选择一个分支执行,是排他的。

并行网关Parallel Gateway

并行网关不会解析连线上的条件,并行网关中所有分支完成后,他才会走向下一个节点。

包含网关Inclusive Gateway

包含网关可以看做是排他网关和并行网关的结合体。有条件的分支选择条件执行,没有条件的分支,则必须执行。最终待这些分支完成后,他才会走向下一个节点。

事件网关Event Gateway

事件网关允许根据事件判断流向。这个网关生产中用的并不多,小七这里就不再展开了。

任务组

在流程变量那一节,我们给任务设置了负责人,但是在日常生活中,这个负责人有可能是指一类角色。

我们去掉负责人,并添加候选人:组长1,组长2

启动流程

/*** 启动流程的时候设置流程变量*/
@Test
public void testStartProcessAndSetVariablesAboutGroup() {// 1、获取流程引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、获取RunTimeServiceRuntimeService runtimeService = processEngine.getRuntimeService();// 3、流程定义的KeyString key = "Leave";// 4、流程变量的mapMap<String, Object> variables = new HashMap<>();// 5、设置流程变量Evection evection = new Evection();// 6、设置请假日期evection.setDay(2d);// 7、把流程变量的pojo放入mapvariables.put("evection", evection);// 8、设定任务的负责人variables.put("employee", "小七");variables.put("Manager", "老李");// 9、启动流程runtimeService.startProcessInstanceByKey(key, variables);
}

完成任务

@Test
public void completTaskByVariablesAboutGroup() {// 测试组任务时,只执行这一行代码completTaskByVariables("小七");
}

查询任务

/*** 查询组任务*/
@Test
public void findGroupTaskList() {// 1、流程定义的KeyString key = "Leave";// 2、任务候选人String candidateUser = "组长1";// 3、获取引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 4、获取TaskServiceTaskService taskService = processEngine.getTaskService();// 5、查询组任务List<Task> taskList = taskService.createTaskQuery().processDefinitionKey(key)// 根据候选人查询任务.taskCandidateUser(candidateUser).list();for (Task task : taskList) {System.out.println("============查询组任务============");System.out.println("流程实例ID=" + task.getProcessInstanceId());System.out.println("任务id=" + task.getId());System.out.println("任务名称:" + task.getName());System.out.println("任务负责人=" + task.getAssignee());}
}

绑定任务

/*** 候选人绑定任务*/
@Test
public void claimTask() {// 1、获取引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、获取TaskServiceTaskService taskService = processEngine.getTaskService();// 3、当前任务的idString taskId = "5002";// 4、任务候选人String candidateUser = "组长1";// 5、查询任务Task task = taskService.createTaskQuery().taskId(taskId).taskCandidateUser(candidateUser).singleResult();if (task != null) {// 6、绑定任务taskService.claim(taskId, candidateUser);System.out.println("taskId-" + taskId + "-用户-" + candidateUser + "-绑定任务完成");}
}

归还任务

/*** 候选人归还任务*/
@Test
public void testAssigneeToGroupTask() {// 1、获取引擎ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();// 2、获取TaskServiceTaskService taskService = processEngine.getTaskService();// 3、当前任务的idString taskId = "5002";// 4、任务负责人String assignee = "组长1";// 5、根据key 和负责人来查询任务Task task = taskService.createTaskQuery().taskId(taskId).taskAssignee(assignee).singleResult();if (task != null) {// 6、归还任务 ,就是把负责人设置为空taskService.setAssignee(taskId, null);System.out.println("taskId-" + taskId + "-用户-" + assignee + "-归还任务完成");}
}

Activiti与Spring整合

Activiti与Spring整合的核心思想,就是将ProcessEngine的类交由Spring容器进行管理。

我们新建maven项目ActivitiSpring

在ActivitiSpring模块的pom中引入以下依赖

<dependencies><dependency><groupId>org.activiti</groupId><artifactId>activiti-engine</artifactId><version>${activiti.version}</version></dependency><!-- activiti 与 Sring整合关键依赖 --><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></dependency><!-- mysql驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version></dependency><!-- mybatis --><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>${mybatis.version}</version></dependency><!-- 链接池 --><dependency><groupId>commons-dbcp</groupId><artifactId>commons-dbcp</artifactId><version>${dbcp.version}</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>${commonsio.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</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><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.0.7.RELEASE</version></dependency>
</dependencies>

然后在resources下新建配置文件activiti-spring.xml,填入以下内容

<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 数据源 --><bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"><property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/activiti?serverTimezone=GMT"/><property name="username" value="root"/><property name="password" value="123456"/><property name="maxActive" value="3"/><property name="maxIdle" value="1"/></bean><!-- 工作流引擎配置bean --><bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration"><!-- 数据源 --><property name="dataSource" ref="dataSource"/><!-- 使用spring事务管理器 --><property name="transactionManager" ref="transactionManager"/><!-- 数据库策略 --><property name="databaseSchemaUpdate" value="drop-create"/></bean><!-- 流程引擎 --><bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean"><property name="processEngineConfiguration" ref="processEngineConfiguration"/></bean><!-- 资源服务service --><bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"/><!-- 流程运行service --><bean id="runtimeService" factory-bean="processEngine"  factory-method="getRuntimeService"/><!-- 任务管理service --><bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"/><!-- 历史管理service --><bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"/><!-- 事务管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!-- 通知 --><tx:advice id="txAdvice" transaction-manager="transactionManager"><tx:attributes><!-- 传播行为 --><tx:method name="save*" propagation="REQUIRED"/><tx:method name="insert*" propagation="REQUIRED"/><tx:method name="delete*" propagation="REQUIRED"/><tx:method name="update*" propagation="REQUIRED"/><tx:method name="find*" propagation="SUPPORTS" read-only="true"/><tx:method name="get*" propagation="SUPPORTS" read-only="true"/></tx:attributes></tx:advice>
</beans>

新建测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:activiti-spring.xml")
public class ActivitiTest {@Autowiredprivate RepositoryService repositoryService;@Testpublic void test01(){System.out.println("获取Spring代理的Activiti部署对象:"+repositoryService);}
}

执行测试方法,控制输出语句,说明Activiti集成Spring成功。

Activiti与SpringBoot整合

首先新建项目ActivitiSpringBoot

然后添加pom依赖

<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.1.0.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.activiti</groupId><artifactId>activiti-spring-boot-starter</artifactId><version>${activiti.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency>
</dependencies>

编写配置文件application.yml

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/activiti?useUnicode=true&characterEncoding=utf8&serverTimezone=GMTusername: rootpassword: 123456activiti:database-schema-update: truedb-history-used: truehistory-level: fullcheck-process-definitions: true

在resources下新建processes,并将我们上面用到的流程xml放在下面

编写启动类

@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class,args);}
}

启动程序后,我们可以从启动日志中找到以下记录说明集成成功。

Process deployed: {id: Leave:1:69859bff-1f03-11ed-9868-005056c00008, key: Leave, name: Leave }

附件

本文所示代码链接整合地址

https://gitee.com/diqirenge/seven-activiti

工作流实战之Activiti7相关推荐

  1. 视频教程-SharePoint 工作流实战教程-其他

    SharePoint 工作流实战教程 大家好,我是霖雨,从2010年开始致力于SharePoint相关的技术研究,精通SharePoint环境搭建.实施.开发.运维.排错等相关技术,从2014年至今连 ...

  2. 项目视频讲解_基于Activiti5工作流实战企业协同OA办公系统教程

    百度网盘地址:http://pan.baidu.com/s/11TiP5 分享一套Adam老师的教程,名为<基于Activiti5工作流实战企业协同OA办公系统(spring-data-jpa. ...

  3. Activiti工作流视频教程-基于Activiti5工作流实战企业协同OA办公系统

    Activiti工作流视频教程-基于Activiti5工作流实战企业协同OA办公系统(spring-data-jpa.uur前台组件) 一.Activiti工作流视频教程课程内容简介 在工作流方面,使 ...

  4. Flowable工作流实战快速入门(一)

    文章目录 1. 工作流入门介绍 1.1 什么是工作流? 1.2 工作流的原理 1.3 BPM 1.4 BPMN 1.5 Activiti 还是flowable? 2. flowable入门helloW ...

  5. 七万字掌握热门工作流引擎框架Activiti7,附带视频讲解哦

    Activiti7   工作流(Workflow),就是通过计算机对业务流程自动化执行管理.它主要解决的是"使在多个参与者之间按照某种预定义的规则自动进行传递文档.信息或任务的过程,从而实现 ...

  6. Activiti工作流实战-2

    2019独角兽企业重金招聘Python工程师标准>>> 两个基础知识: 工作流引擎  ProcessEngine对象,这是Activiti工作的核心.负责生成流程运行时的各种实例及数 ...

  7. 手摸手系列之SpringBoot+Vue整合snakeflow工作流实战

    前言 技术栈: SpringBoot: 2.3.5.RELEASE Vue: 2.6.10 snakerflow: 2.5.1 最近做集团内部的悦通关平台项目,台账管理的付款申请模块需要用到工作流审批 ...

  8. 工作流实战_14_flowable_已办任务列表查询

    项目地址:https://gitee.com/lwj/flowable.git 分支flowable-base 视频讲解地址 https://space.bilibili.com/485524575/ ...

  9. 工作流实战_28_flowable 任务多实例

    项目地址:https://gitee.com/lwj/flowable.git 分支flowable-base 任务多实例由2种形式: 第1种场景:当多实例中的每一个势力都办理完任务后,节点流转. 案 ...

最新文章

  1. 《程序员开发心理学》阅读笔记一
  2. html密码本源码,YoungxjPwd密码本 PHP版 v1.0
  3. java 多线程下载_使用java实现http多线程下载
  4. 计算机科学与技术_080702,电子科学与技术(专业代码;080702)专业介绍与解读
  5. Java——零基础速成学习
  6. web页面官网右侧悬浮固定在线客服代码
  7. noip模拟赛 Nephren Ruq Insania
  8. 电脑文件如何传到云服务器上,电脑文件如何传到云服务器上
  9. cadence16.6出odb++出不出来
  10. nslookup命令使用技巧
  11. Scott Hanselman的2006 Windows最终开发者和高级用户工具列表
  12. pico的学习之路(一)——MQ-2烟雾传感器模块(树莓派pico实现)
  13. VS使用FFmpeg被声明为已否决的解决方案
  14. SCCM 2007 R2 setp by setp详细部署流程(二)-环境准备
  15. 2018 Oracle NetSuite 中国峰会首度开幕,智驭云端生态未来
  16. xbox one无线手柄到底是蓝牙还是普通2.4G
  17. mysql语句查询慢造成mysql卡死_MySQL数据库之一次MySQL慢查询导致的故障
  18. Pytest学习笔记(4)-Fixture装置
  19. B站校园招聘后端笔试题(一)
  20. 【图像分辨率大探析】 关于图像尺寸、分辨率、像素密度、格式的理解

热门文章

  1. 企业要做好积分营销,提高用户忠诚度
  2. oracle系统表空间不足,oracle表空间不足相关问题解决办法
  3. 用Delphi写一个UTF8编码格式的文本文件
  4. Windows留后门--教程(五)——shift粘贴键后门
  5. 搞死虚拟机-永恒之蓝和shift后门
  6. 站长必读:防御DDOS攻击终极指南
  7. 那些年我们经历过的运维
  8. 【Mitmproxy】Mac + Python + mitmproxy透明代理配置,拦截所有网络请求
  9. “聚力创新”中国江苏•大院大所对接会台前幕后
  10. 永生生物_指导:通往永生的道路