[JavaEE] 工作流- Activiti7 框架详解

目录

1、Activiti介绍

1.1、BPMN设计器

1.2、常见流程符号

1.2.1、事件event

1.2.2、活动activiti

1.2.3、流向flow

2、入门案例

2.1、需求说明

2.2、初始环境

2.2.1、添加依赖

2.2.2、添加配置

2.2.3、添加引导类

2.2.4、启动项目

2.2.5、表结构

2.2.6、常见api

2.3、绘制流程

2.4、部署流程

2.5、操作流程

2.5.1、启动流程

2.5.2、查询待办任务

2.5.3、填写申请单

2.5.4、经理同意

2.5.5、经理不同意

2.5.6、查询历史任务

3、任务分配

3.1、表达式分配

3.1.2、绘制流程

3.1.3、执行流程

3.1.4、备注说明

3.2、候选人

3.2.1、绘制流程

3.2.2、部署并启动流程

3.2.3、拾取任务

3.2.4、归还任务

3.3、候选人组

3.3.1、绘制流程

3.3.2、部署并启动流程

3.3.3、拾取任务

3.3.4、归还/交接任务

4、流程网关

4.1、排他网关

4.2、并行网关

4.3、包容网关

5、附录

5.1、集成安全框架

5.1.1、添加工具类

5.2.2、创建对象

5.2、业务id对接


1、Activiti介绍

目前业界流行的工作流技术有JBPM、Activiti、Flowable、Camunda,其中以Activiti占有率为最高

Activiti是一个开源的轻量级工作流引擎,2010年基于jBPM4实现首次开源。官网地址:https://www.activiti.org

Activiti可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN进行定义

BPMN是一种用于图形化表示和描述业务流程的标准化标记语言,目前主流的版本是2.0

1.1、BPMN设计器

虽然BPMN是一个标记语言,但是在实际中,我们很少直接去用它语法进行开发,而是直接使用流程设计器来画

在前端开源项目中有一个叫bpmn-js的开源项目,现在几乎成了画流程的标准,市面上的流程设计器基本都是基于它改造的

比如Activiti7官网提供的:https://github.com/activiti/activiti-modeling-app设计器,就是基于bpmn-js实现的

需要安装nodejs

使用npm run dev命令启动bpmn-js

运行完自动弹出如下界面

1.2、常见流程符号

1.2.1、事件event

事件是业务流程模型中的重要元素之一,事件可以发生在流程的任何阶段,并且可以影响流程的执行。分为以下几类:

  • 开始事件(Start Event):表示流程的起点,通常用于触发流程的启动

  • 结束事件(End Event):表示流程的结束点,通常用于触发流程的结束

1.2.2、活动activiti

任务(Task)是最基本的活动类型,表示一个简单的、可执行的工作单元。任务通常由人工执行,并且需要指定执行者

用户任务是由人工执行的,需要指定执行的用户或角色,并提供相应的输入

手动任务是由系统自动执行的,不需要指定执行的用户或角色

1.2.3、流向flow

流是连接两个流程节点的连线。常见的流向包含以下几种

2、入门案例

2.1、需求说明

我们通过请假流程审批这样一个流程来学习工作流

在企业中,员工如果有事需要请假,一般都需要向上级请假,得到批准后,方可离开公司。需求如下:

  • 员工:请假的员工需要先填写请假单,填写的字段有:请假人、请假天数、开始请假时间、请假事由

  • 经理:审批员工的请假单,如果不同意,则需要说明不同意的理由

实现步骤

1、搭建环境:使用SpringBoot集成Activiti,把初始化环境做出来

2、绘制流程:按照BPMN的规范,使用流程定义工具,用流程符号把整个流程描述出来

3、部署流程:把画好的流程定义文件,加载到数据库中,生成表的数据

4、操作流程:使用java代码来操作数据库表中的内容

2.2、初始环境

2.2.1、添加依赖

创建一个新的项目 activiti-demo,导入以下依赖

    <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.4</version><relativePath/></parent>
​<dependencies><!--安全框架 spring security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--springboot与activiti7整合的starter--><dependency><groupId>org.activiti</groupId><artifactId>activiti-spring-boot-starter</artifactId><version>7.10.0</version></dependency>
​<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
​<!-- mybatis --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.2</version></dependency>
​<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency>
​<!-- mysql驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- 单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.22</version></dependency></dependencies>
​<!--如果activiti依赖下载不了,可以配置如下地址进行下载--><repositories><repository><id>activiti-releases</id><url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url></repository></repositories>

2.2.2、添加配置

配置文件application.yml

server:port: 8989
spring:datasource:username: rootpassword: ******url: jdbc:mysql://localhost:3306/activiti-db?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC&nullCatalogMeansCurrent=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driver#日志的相关配置activiti:# 记录所有历史数据history-level: full# 是否需要使用历史表,默认false不使用,而配置true是使用历史表db-history-used: true# 关闭流程自动部署,需要手动部署流程check-process-definitions: false# 如果部署过程遇到任何问题,服务不会失败deployment-mode: never-fail

注意:需要在自己的MySQL中创建一个新的数据库:activiti-db

2.2.3、添加引导类

准备一个引导类,然后启动项目,启动日志中,我们可以看到,activiti会自动创建表结构

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
​
@SpringBootApplication
public class ActivitiSampleApplication {public static void main(String[] args) {SpringApplication.run(ActivitiSampleApplication.class, args);}
}

2.2.4、启动项目

项目启动之后,观察日志信息,会自动创建出Activiti需要的25张数据表

刷新数据库,发现数据库中已经创建了25张表,目前说明Springboot已成功集成了activiti7

2.2.5、表结构

Activiti 的表都以ACT_ 开头。 第二部分是表示表的用途的两个字母标识。 用途也和服务的 API 对应。

  • ACT_GE :GE 表示 general, 通用数据

  • ACT_RE :RE表示 repository,这个前缀的表包含了流程定义信息

  • ACT_RU:RU表示 runtime,这些运行时的表,包含流程实例,任务,变量,异步任务等运行中的数据

  • ACT_HI:HI表示 history, 这些表包含历史数据,比如历史流程实例, 变量,任务等等

具体的表含义,如下

表分类表名解释
一般数据
[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]运行时变量表

2.2.6、常见api

25张表对应MybatisPlus25个Mapper,同样25个Service...

而在activiti7框架内部,已经对25张表的数据操作,已经封装了对应的4个service

  • RepositoryService:用于部署流程定义,可以添加、删除、查询和管理流程定义

  • RuntimeService:用于启动、查询和管理正在运行的流程实例

  • TaskService:用于查询和管理当前用户可以操作的任务,以及完成任务

  • HistoryService:用于查询历史数据,例如已完成的流程实例、已删除的流程实例、用户任务等

因为我们现在使用的是springboot集成了activiti,这些api也被spring容器进行了管理,需要用到以上api的时候,直接注入即可,例如

@Autowired
private RepositoryService repositoryService;

2.3、绘制流程

我们打开bpmn-js,可以直接在页面中画图,步骤如下:

① 定义流程编号(ID)和名称

② 新增一个用户任务,并指定代理人为:张三

③ 新增一个用户任务,并指定代理人为:李四,同时需要结束这个流程,最后需要有一个结束事件

④ 流程图画好之后,在页面的左下角有一个导出,就可以直接导出为bpmn文件(xml文件)

⑤ 把生成后的bpmn文件改名拷贝到idea中备用,存储位置:resource/bpmn/qingjia.bpmn

因为保存的文件都是xml文件,我们为了方便查看这些流程,也可以截个图一起放入bpmn目录下

2.4、部署流程

部署流程就是将生成好流程文件bpmn保存到数据库中,此时需要用到RepositoryService对象

RepositoryService用于部署流程定义,可以添加、删除、查询和管理流程定义

import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
@SpringBootTest
public class ActTest {//此对象可以完成流程的部署任务@Autowiredprivate RepositoryService repositoryService;
​//流程部署@Testpublic void deployProcess() {Deployment deployment = repositoryService.createDeployment()//创建流程部署.addClasspathResource("bpmn/qingjia.bpmn")//流程定义文件.addClasspathResource("bpmn/qingjia.png")//流程定义图片.name("qingjia")//指定流程名称.deploy();//开始部署
​//打印流程部署结果System.out.println("流程部署的id:" + deployment.getId());System.out.println("流程部署的名称:" + deployment.getName());}
}

注意: 图中两个地方需保持一致

上面就进行了一次流程的部署,这个过程中涉及到的数据表有:

  • act_re_deployment:流程部署,记录每次工作流的部署信息,包括部署名称和时间等

  • act_ge_bytearray:流程资源表,系统会将流程定义的两个文件保存到这张表中

  • act_re_procdef:流程定义表,记录每个流程定义的信息,包括流程定义的名称、版本号、部署ID等

注意:act_re_deployment和act_re_procdef一对多关系,一次部署在流程部署表生成一条记录,但一次部署可以部署多个流程定义

 

但是在一般情况下:一次部署一个流程,这样部署表和流程定义表是一对一有关系,方便读取流程部署及流程定义信息

    //查询流程部署信息@Testpublic void test2() {List<Deployment> list = repositoryService.createDeploymentQuery()//创建流程部署查询.deploymentNameLike("qingjia")//设置查询条件.list();//查询所有部署的流程//打印流程部署结果for (Deployment deployment : list) {System.out.println("流程部署的名称:" + deployment.getName());}}
​//查询部流程定义@Testpublic void test3() {//根据name查询流程定义的Key查询List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery().processDefinitionKeyLike("qingjia").list();for (ProcessDefinition processDefinition : list) {System.out.println(processDefinition.getId() + "==" + processDefinition.getName());}}

2.5、操作流程

2.5.1、启动流程

流程定义好了之后,相当于定义了一个模板,公司有很多人,每个人都可以按照这个模板填写自己的申请单,每个申请单就相当于是一个流程实例

流程实例的操作要使用RuntimeService来处理,它用于启动、查询和管理正在运行的流程实例

@Autowiredprivate RuntimeService runtimeService;
​//启动流程实例@Testpublic void test4() {//根据流程定义key启动流程实例ProcessInstance processInstance =runtimeService.startProcessInstanceByKey("qingjia");
​System.out.println("流程实例Id: " + processInstance.getId());//流程实例idSystem.out.println("流程定义Id: " + processInstance.getProcessDefinitionId());//流程定义id}

流程实例启动之后,也会操作相关的表结构

  • act_ru_task(运行时任务表):插入一条新的任务记录,表示流程实例的启动任务

  • act_ru_execution表(运行时流程实例表):插入一条新的流程实例的执行信息,包括流程实例ID、流程定义ID等

  • act_ru_identitylink (运行时身份关联表):插入一条新的身份关联记录,表示流程实例的启动任务与相关用户的关系

 

2.5.2、查询待办任务

流程启动起来之后,我们不同的人需要对流程进行不同的操作,主要有查询待办、填写申请单、审评、查询历史任务等等

这些操作都是通过TaskService来完成的,它主要用于查询和管理当前用户可以操作的任务,以及完成任务

当流程实例创建之后,就会分配给不同的人来去执行流程中的任务,每个操作人都可以进行查询我的待办任务

根据流程图的中的定义,第一个填写请假单是由张三负责的,我们就可以先查询张三的任务,主要就是从act_ru_task表中进行查询

    //任务1--查询张三的待办任务@Testpublic void test5() {List<Task> taskList = taskService.createTaskQuery()//任务查询条件.processDefinitionKey("qingjia")//流程名称.taskAssignee("张三")//操作人名称.list();for (Task task : taskList) {System.out.println("流程实例id:" + task.getProcessInstanceId());System.out.println("任务id:" + task.getId());System.out.println("任务名称:" + task.getName());System.out.println("任务负责人:" + task.getAssignee());}}

2.5.3、填写申请单

现在流程中的节点已经走到了张三这里,需要他进行处理,因为目前是请假申请单,所以他需要填写请假单的内容,并且提交数据,继续往下执行流程

调用complete方法,即可结束当前节点,并且流程会自动开启下一个节点的任务。

    //任务2--张三执行任务,完成申请单的编写和提交@Testpublic void test6() {//根据流程key 和 任务的负责人 查询任务,返回一个任务对象List<Task> list = taskService.createTaskQuery().processDefinitionKey("qingjia") //流程Key.taskAssignee("张三")  //要查询的负责人.list();
​//执行任务if (CollectionUtil.isNotEmpty(list)) {for (Task task : list) {Map<String, Object> variables = new HashMap<>();variables.put("userName", "张三");variables.put("startDate", "2024-01-01");variables.put("days", 1);variables.put("reason", "元旦回家探亲");
​//完成任务taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}}

张三完成自己的任务后,相关表的变化

test6()运行前的任务表如下:

test6()运行后的任务表如下:

test6()运行前后 act_ru_execution流程实例表不变

 test6()运行后act_hi_taskinst历史表发生改变:

  • act_ru_execution表中发现,新增了【经理审批】待执行的节点,而完成的【填写请假单】节点被删除了

  • act_ru_task表中,待办任务也变成了李四【经理审批】

  • act_ru_variable表中会存储代码中传入的表单数据

 注意:李四为绘图中的代理人,绘图的时候忘了加上

2.5.4、经理同意

经理同意这个操作其实就相当于完成当前节点,进入下一节点,因此处理方案跟上个流程类型

    //任务3--经理执行任务,同意申请@Testpublic void test7() {//查询到李四的当前任务Task task = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("李四").singleResult();
​//执行任务if (null != task) {Map<String, Object> variables = new HashMap<>();variables.put("approvalStatus", "同意");variables.put("approvalNote", "123");taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}

上述代码执行成功后,到数据库中查看act_ru_execution、act_ru_task、act_ru_variable中的数据,你会发现里面没有了相关流程实例的数据;

那是因为【经理审批】节点执行完成之后,后面的结束节点也会自动执行,因此流程就执行完成了。

而act_ru_开头的表只存储运行中的流程信息,不会存储流程结束了的信息,结束了的相关信息都转到act_hi开头的表中存储了

  • act_hi_procinst:历史的流程实例

  • act_hi_actinst:历史的活动实例

  • act_hi_taskinst:历史的任务实例

  • act_hi_identitylink:历史流程用户关系

  • act_hi_varinst:历史流程运行中的变量信息

2.5.5、经理不同意

经理不同意则流程会终止执行。比如下面流程中,如果经理1审批不同意,那么经理2就不用审批了,整个流程就应该直接结束

因此不同意,则应该是终止流程而不是完成节点,在删除流程时,同时也把审批不同意及理由,作为流程变量存储到流程变量中

    //任务3--经理执行任务,不同意申请@Testpublic void test8() {Task task = taskService.createTaskQuery().processDefinitionKey("qingjia") //流程Key.taskAssignee("李四")  //要查询的负责人.singleResult();
​if (null != task) {Map<String, Object> variables = new HashMap<>();variables.put("approvalStatus", "不同意");variables.put("approvalNode", "时间太久,不同意");
​//记录流程变量runtimeService.setVariables(task.getProcessInstanceId(), variables);//添加流程变量,删除流程实例,表示任务被拒绝runtimeService.deleteProcessInstance(task.getProcessInstanceId(), "时间太久,不同意");}}

上述代码执行成功后,同样在数据库中act_ru_execution、act_ru_task、act_ru_variable中的看不到数据

因为整个流程都被删除了,也结束了,信息也被转到了act_hi开头的表中

2.5.6、查询历史任务

历史任务的查询需要使用HistoryService完成,主要就是根据各种条件从前面讲过的一堆历史表中查询数据

    @Autowiredprivate HistoryService historyService;
​@Testpublic void test9(){HistoricTaskInstanceQuery instanceQuery = historyService.createHistoricTaskInstanceQuery().includeProcessVariables()//包含流程变量(配合下面使用).orderByHistoricTaskInstanceEndTime().desc()//按历史任务实例结束时间排序.finished()//只查询已完成的任务.taskAssignee("张三");//根据执行人查询
​//自定义流程变量  条件查询//instanceQuery.processVariableValueGreaterThan("days", "1");
​//查询历史流程List<HistoricTaskInstance> list = instanceQuery.list();for (HistoricTaskInstance history : list) {System.out.println("Id: " + history.getId());System.out.println("ProcessInstanceId: " + history.getProcessInstanceId());System.out.println("StartTime: " + history.getStartTime());System.out.println("Name: " + history.getName());Map<String, Object> processVariables = history.getProcessVariables();System.out.println(processVariables.get("days").toString());System.out.println(processVariables.get("reason").toString());System.out.println("=======================================");}}

查询条件API说明

方法名称
processInstanceBusinessKey(String processInstanceBusinessKey)根据流程实例业务Key查询
taskId(String taskId)根据任务ID查询
taskAssignee(String taskAssignee) | taskAssigneeLike(String taskAssignee)根据执行人查询
finished()已完成的(申请过、同意过)
unfinished()未完成任务
orderByHistoricTaskInstanceEndTime().desc()按照执行时间排序
taskName(String var1) | taskNameLike(String var1)根据节点任务名称查询
list()返回分页数据
includeProcessVariables()包含流程变量(配合下面使用)
processVariableValueEquals(String variableName, Object variableValue)两个值相等
processVariableValueNotEquals(String variableName, Object variableValue)两个值不相等
processVariableValueGreaterThan(String name, Object value)大于
processVariableValueLessThan(String name, Object value)小于

3、任务分配

上一章的案例中,在指派用户任务的执行人时,使用的都是直接指派给固定账号,这样流程设计审批的灵活性就很差

因此,Activiti提供了各种不同的分配方式,这章我们就来详细研究下其它任务分配方式,主要有:表达式分配、监听器分配

3.1、表达式分配

值表达式就是使用UEL表达式(一种占位符)来替换具体的分配人,在使用的时候只需要对表达式中的变量进行赋值即可

UEL表达式,是一种用于在流程定义中评估和计算表达式的语言。可以用来做流程条件判断、变量赋值等

  • 定界符:${assignee} | ${user.assignee}

  • 数学运算:${ 5+5 }

  • 逻辑判断:${amount > 100}

  • 方法调用:${ list('参数') },调用的是list方法,这个方法是Activiti内置的方法,把参数转成一个集合

3.1.2、绘制流程

重新绘制前面的流程,但是在代理人的位置不再直接写死为张三、李四,而是使用${assingee0}、${assingee1}来代替

3.1.3、执行流程

为了观察数据更方便,可以将目前库中的所有表都删除,然后让其重建

import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.history.HistoricTaskInstanceQuery;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest1 {@Autowiredprivate RepositoryService repositoryService;
​@Autowiredprivate RuntimeService runtimeService;
​@Autowiredprivate TaskService taskService;
​@Autowiredprivate HistoryService historyService;
​//流程部署@Testpublic void test1() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("bpmn/qingjia1.bpmn").name("qingjia").deploy();
​System.out.println("流程部署的id:" + deployment.getId());}
​//启动流程实例@Testpublic void test2() {//此时在启动流程实例之前,必须要给流程中定义的变量赋值Map<String, Object> variables = new HashMap<>();variables.put("assingee0", "张三");variables.put("assingee1", "李四");
​ProcessInstance processInstance =runtimeService.startProcessInstanceByKey("qingjia", variables);
​System.out.println("流程实例Id: " + processInstance.getId());}
​//查询张三的待办任务并完成@Testpublic void test3() {//主要就是从act_ru_task表中进行查询List<Task> taskList = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("张三").list();
​
​for (Task task : taskList) {System.out.println("任务id:" + task.getId());System.out.println("任务名称:" + task.getName());
​//请假原因,根据业务自由设置Map<String, Object> variables = new HashMap<>();variables.put("userName", "张三");variables.put("startDate", "2024-01-01");variables.put("days", "1");variables.put("reason", "元旦回家探亲");
​//完成任务taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
​//查询李四的待办任务并完成,同意申请@Testpublic void test4() {//查询到李四的当前任务List<Task> list = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("李四").list();
​//执行任务for (Task task : list) {Map<String, Object> variables = new HashMap<>();variables.put("approvalStatus", "同意");variables.put("approvalNote", "123");
​taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
}

3.1.4、备注说明

刚才的例子只是UEL表达式中最基本的设置变量的方式,UEL还支持其他很多类型

  1. 使用pojo对象赋值

  2. spring对象的方法

我们上面使用了变量来设置代理人,在activiti中这称为流程变量

  • 流程变量的默认作用域是流程实例,当一个流程变量的作用域为流程实例时,可以称为global变量

  • 除了global变量外,activiti还支持local变量,这种变量仅仅是针对一个任务,随着一个任务的结束会被删除

3.2、候选人

在前面的流程定义中在任务结点的assignee都是设置了一个负责人,但是在企业中,每个节点上都可能有多个负责人

下面我们就需要使用候选人或者候选人组做为身份标识替换掉前面的单个参与者来完成任务

一个审批节点可能有多个人同时具有审批的权限,这时我们就可以通过候选人来处理。

3.2.1、绘制流程

这次绘制流程时,对于经理审批,不再设置代理人,而是设置候选人,多个候选人是,分隔

3.2.2、部署并启动流程

import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest3 {@Autowiredprivate RepositoryService repositoryService;
​@Autowiredprivate RuntimeService runtimeService;
​@Autowiredprivate TaskService taskService;
​
​//流程部署@Testpublic void test1() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("bpmn/qingjia3.bpmn").name("qingjia").deploy();
​System.out.println("流程部署的id:" + deployment.getId());}
​//启动流程实例@Testpublic void test2() {//此时在启动流程实例之前,必须要给流程中定义的变量赋值Map<String, Object> variables = new HashMap<>();variables.put("a1", "张三");variables.put("c1", "赵经理");variables.put("c2", "钱经理");variables.put("c3", "孙经理");
​ProcessInstance processInstance =runtimeService.startProcessInstanceByKey("qingjia",variables);
​System.out.println("流程实例Id: " + processInstance.getId());}//查询张三的待办任务并完成@Testpublic void test3() {//主要就是从act_ru_task表中进行查询List<Task> taskList = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("张三").list();
​
​for (Task task : taskList) {System.out.println("任务id:" + task.getId());System.out.println("任务名称:" + task.getName());
​//请假原因,根据业务自由设置Map<String, Object> variables = new HashMap<>();variables.put("userName", "张三");variables.put("startDate", "2024-01-01");variables.put("days", "1");variables.put("reason", "元旦回家探亲");
​//完成任务taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
}

流程启动后任务,在act_ru_task表中的审批人是空的,但是act_ru_identitylink表保存了候选人信息

3.2.3、拾取任务

拾取任务的目的是将候选人提升为审批人

    //孙经理作为候选人进行查询,并拾取任务@Testpublic void test4() {//模拟登录,防止UsernameNotFoundException错误new SecurityUtil().logInAs("孙经理");
​//根据候选人查询任务List<Task> list = taskService.createTaskQuery().taskCandidateUser("孙经理") // 根据候选人查询审批任务.list();//任务拾取: 将指定用户从候选人提升为审批人for (Task task : list) {taskService.claim(task.getId(), "孙经理");}}

3.2.4、归还任务

拾取任务后如果不想操作那么可以归还任务

    //归还:拾取的用户不审批了。就放弃审批人的操作@Testpublic void test5() {//模拟登录,防止UsernameNotFoundException错误new SecurityUtil().logInAs("孙经理");
​List<Task> list = taskService.createTaskQuery().taskCandidateOrAssigned("孙经理") // 根据 审批人或者候选人 来查询待办任务.list();for (Task task : list) {// 归还操作的本质其实就是设置审批人为空taskService.unclaim(task.getId());}}

3.3、候选人组

当候选人很多的情况下,我们可以分组来处理。也就是先创建组,然后把用户分配到这个组中,整组人就都成了候选人

3.3.1、绘制流程

3.3.2、部署并启动流程

import com.itheima.util.SecurityUtil;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest4 {@Autowiredprivate RepositoryService repositoryService;
​@Autowiredprivate RuntimeService runtimeService;
​@Autowiredprivate TaskService taskService;
​//流程部署@Testpublic void test1() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("bpmn/qingjia4.bpmn").name("qingjia").deploy();
​System.out.println("流程部署的id:" + deployment.getId());}
​//启动流程实例@Testpublic void test2() {ProcessInstance processInstance =runtimeService.startProcessInstanceByKey("qingjia");System.out.println("流程实例Id: " + processInstance.getId());}
​//查询张三的待办任务并完成@Testpublic void test3() {//主要就是从act_ru_task表中进行查询List<Task> taskList = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("张三").list();
​for (Task task : taskList) {System.out.println("任务id:" + task.getId());System.out.println("任务名称:" + task.getName());
​//请假原因,根据业务自由设置Map<String, Object> variables = new HashMap<>();variables.put("userName", "张三");variables.put("startDate", "2024-01-01");variables.put("days", "1");variables.put("reason", "元旦回家探亲");
​//完成任务taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
}

流程启动后任务,在act_ru_task表中的审批人是空的,但是act_ru_identitylink表保存了候选人信息

3.3.3、拾取任务

拾取任务的目的是将候选人提升为审批人

    //查询经理部门的任务@Testpublic void test4() {//模拟登录,防止UsernameNotFoundException错误//new SecurityUtil().logInAs("孙经理");
​//根据候选人查询任务List<Task> list = taskService.createTaskQuery().taskCandidateGroup("manageGroup") // 查询经理部门的任务.list();
​//任务拾取: 将指定用户从候选人提升为审批人for (Task task : list) {taskService.claim(task.getId(), "孙经理");}}

3.3.4、归还/交接任务

拾取任务后如果不想操作那么可以归还任务,也可以将任务交接给其他用户

    //任务归还和交接@Testpublic void test5() {//模拟登录,防止UsernameNotFoundException错误new SecurityUtil().logInAs("孙经理");
​List<Task> list = taskService.createTaskQuery().taskCandidateOrAssigned("孙经理") // 根据 审批人或者 候选人来查询待办任务.list();for (Task task : list) {// 归还操作的本质其实就是设置审批人为空// taskService.unclaim(task.getId());
​//任务交接taskService.setAssignee(task.getId(), "刘经理");}}

4、流程网关

网关用于控制流程的执行流向,它的作用是在流程执行时进行决策,决定流程的下一个执行步骤。Activiti7中,有以下几种类型的网关:

  1. 排他网关:用于在流程中进行条件判断,根据不同的条件选择不同的分支路径,只有满足条件的分支会被执行,其他分支会被忽略

  2. 并行网关:用于将流程分成多个并行的分支,这些分支可以同时执行,当所有分支都执行完毕后,流程会继续向下执行

  3. 包容网关:用于根据多个条件的组合情况选择分支路径,可以选择满足任意一个条件的分支执行,或者选择满足所有条件的分支执行

4.1、排他网关

排他网关用于对流程中分支进行决策,当执行到达这个网关时,会按照所有出口顺序流定义的顺序对它们进行计算,选择第一个条件为true的顺序流继续流程

import com.itheima.util.SecurityUtil;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest5 {@Autowiredprivate RepositoryService repositoryService;
​@Autowiredprivate RuntimeService runtimeService;
​@Autowiredprivate TaskService taskService;
​//流程部署@Testpublic void test1() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("bpmn/gateway-paita.bpmn").name("qingjia").deploy();System.out.println("流程部署的id:" + deployment.getId());}
​//启动流程实例@Testpublic void test2() {ProcessInstance processInstance =runtimeService.startProcessInstanceByKey("qingjia");System.out.println("流程实例Id: " + processInstance.getId());}
​//查询张三的待办任务并完成@Testpublic void test3() {//主要就是从act_ru_task表中进行查询List<Task> taskList = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("张三").list();
​for (Task task : taskList) {System.out.println("任务id:" + task.getId());System.out.println("任务名称:" + task.getName());
​//请假原因,根据业务自由设置Map<String, Object> variables = new HashMap<>();variables.put("userName", "张三");variables.put("startDate", "2024-01-01");variables.put("days", "10");//此处的天数决定了下一步会流转到哪个审批人手中variables.put("reason", "元旦回家探亲");
​//完成任务taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
}

4.2、并行网关

并行网关用于将流程分成多个并行的分支,这些分支可以同时执行,当所有分支都执行完毕后,流程会继续向下执行

  • fork分支:并行后的所有外出顺序流,为每个顺序流都创建一个并发分支

  • join汇聚: 所有到达并行网关,在此等待的进入分支,直到所有进入顺序流的分支都到达以后, 流程就会通过网关

import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest6 {@Autowiredprivate RepositoryService repositoryService;
​@Autowiredprivate RuntimeService runtimeService;
​@Autowiredprivate TaskService taskService;
​//流程部署@Testpublic void test1() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("bpmn/gateway-bingxing.bpmn").name("qingjia").deploy();System.out.println("流程部署的id:" + deployment.getId());}
​//启动流程实例@Testpublic void test2() {ProcessInstance processInstance =runtimeService.startProcessInstanceByKey("qingjia");System.out.println("流程实例Id: " + processInstance.getId());}
​//查询张三的待办任务并完成@Testpublic void test3() {//主要就是从act_ru_task表中进行查询List<Task> taskList = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("张三").list();
​for (Task task : taskList) {System.out.println("任务id:" + task.getId());System.out.println("任务名称:" + task.getName());
​//请假原因,根据业务自由设置Map<String, Object> variables = new HashMap<>();variables.put("userName", "张三");variables.put("startDate", "2024-01-01");variables.put("days", "10");//此处的天数决定了下一步会流转到哪个审批人手中variables.put("reason", "元旦回家探亲");
​//完成任务taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
​//查询技术经理(李四)的任务,并处理@Testpublic void test4() {//查询到李四的当前任务List<Task> list = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("李四").list();
​//执行任务for (Task task : list) {Map<String, Object> variables = new HashMap<>();variables.put("approvalStatus", "同意");variables.put("approvalNote", "123");
​taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
​//查询人事经理(王五)的任务,并处理@Testpublic void test5() {//查询到李四的当前任务List<Task> list = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("王五").list();
​//执行任务for (Task task : list) {Map<String, Object> variables = new HashMap<>();variables.put("approvalStatus", "同意");variables.put("approvalNote", "123");
​taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
​//查询总经理(赵六)的任务,并处理@Testpublic void test6() {//查询到李四的当前任务List<Task> list = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("赵六").list();
​//执行任务for (Task task : list) {Map<String, Object> variables = new HashMap<>();variables.put("approvalStatus", "同意");variables.put("approvalNote", "123");
​taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
}

在并行网关中我们需要注意的是执行实例的概念

  • 主流程实例:流程启动就会维护的一条实例, 在ACT_RU_EXECUTION表中parent_id_为null

  • 子流程实例:流程的每一步操作都会更新子流程实例,表示当前流程的执行进度

4.3、包容网关

包含网关用于根据多个条件的组合情况选择分支路径,可以选择满足任意一个条件的分支执行(有条件必须执行,无条件的必须执行)

import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
@SpringBootTest
public class ActTest7 {@Autowiredprivate RepositoryService repositoryService;
​@Autowiredprivate RuntimeService runtimeService;
​@Autowiredprivate TaskService taskService;
​//流程部署@Testpublic void test1() {Deployment deployment = repositoryService.createDeployment().addClasspathResource("bpmn/gateway-baorong.bpmn").name("qingjia").deploy();System.out.println("流程部署的id:" + deployment.getId());}
​//启动流程实例@Testpublic void test2() {ProcessInstance processInstance =runtimeService.startProcessInstanceByKey("qingjia");System.out.println("流程实例Id: " + processInstance.getId());}
​//查询张三的待办任务并完成@Testpublic void test3() {//主要就是从act_ru_task表中进行查询List<Task> taskList = taskService.createTaskQuery().processDefinitionKey("qingjia").taskAssignee("张三").list();
​for (Task task : taskList) {System.out.println("任务id:" + task.getId());System.out.println("任务名称:" + task.getName());
​//请假原因,根据业务自由设置Map<String, Object> variables = new HashMap<>();variables.put("userName", "张三");variables.put("startDate", "2024-01-01");variables.put("days", "10");//此处的天数决定了下一步会流转到哪个审批人手中variables.put("reason", "元旦回家探亲");
​//完成任务taskService.complete(task.getId(), variables);System.out.println("任务完成...");}}
​//完成后发现任务流转到人事经理审批 和 部门经理审批
}

5、附录

5.1、集成安全框架

由于Activiti7整合了SpringSecurity框架,在拾取候选人的时候,需要验证身份信息,否则机会报错:UsernameNotFoundException

我们可以临时模拟一个登录

5.1.1、添加工具类

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.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.stereotype.Component;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;@Component
public class SecurityUtil {private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);private static InMemoryUserDetailsManager inMemoryUserDetailsManager;//查询实例public static InMemoryUserDetailsManager findInstance(){if(inMemoryUserDetailsManager==null){inMemoryUserDetailsManager = new InMemoryUserDetailsManager();}return inMemoryUserDetailsManager;}//createUser() 方法用于创建一个新的用户,如果该用户已经存在则先删除再创建。新创建的用户需要设置两个角色:"ROLE_ACTIVITI_USER" 和 "GROUP_activitiTeam"public void createUser(String userName) {inMemoryUserDetailsManager = findInstance();if(inMemoryUserDetailsManager.userExists(userName)) {inMemoryUserDetailsManager.deleteUser(userName);}//SimpleGrantedAuthority 是一个自定义的权限类,用于设置角色的名称。List<SimpleGrantedAuthority> roles = new ArrayList<SimpleGrantedAuthority>(){{add(new SimpleGrantedAuthority("ROLE_ACTIVITI_USER"));add(new SimpleGrantedAuthority("GROUP_activitiTeam"));}};//passwordEncoder() 方法用于设置密码加密方式为 BCryptPasswordEncoderinMemoryUserDetailsManager.createUser(new User(userName, passwordEncoder().encode("password"),roles));}//logInAs() 方法用于以指定用户名登录系统,并获取当前用户的用户名和权限信息public void logInAs(String username) {createUser(username);UserDetails user = findInstance().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 是一个 Spring Security 提供的上下文处理器,用于在请求处理过程中维护用户的认证状态和权限信息SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return user.getAuthorities();}@Overridepublic Object getCredentials() {return user.getPassword();}@Overridepublic Object getDetails() {return user;}@Overridepublic Object getPrincipal() {return user;}@Overridepublic boolean isAuthenticated() {return true;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {}@Overridepublic String getName() {return user.getUsername();}}));org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);}public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

5.2.2、创建对象

在启动类中添加下面代码

    @Beanpublic UserDetailsService myUserDetailsService() {return SecurityUtil.findInstance();}

5.2、业务id对接

目前我们已经基本完成了activiti的学习,我们发现目前的工作流其实是脱离我们的实际业务存在的

如果想将activiti与实际业务联系起来,需要用到它提供的一个字段:buinessId,这个字段用来记录业务表的主键

我们可以在启动流程的时候设置

ProcessInstance startProcessInstanceByKey(String processDefinitionKey, String businessKey, Map<String, Object> variables);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/407725.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决 mfc140.dll 文件丢失的问题,5种mfc140.dll解决方法

当您尝试启动一个用 Microsoft Visual Studio 2015 开发的 Windows 应用程序时&#xff0c;如果出现“无法找到 mfc140.dll 文件”的错误&#xff0c;请不要紧张。这类问题通常由DLL文件缺失、损坏或未正确安装引起。好消息是&#xff0c;存在多种解决方案可以帮助您解决这一挑…

Vue指令:v-cloak、v-once、v-pre 指令

Vue 指令系列文章&#xff1a; 《Vue插值&#xff1a;双大括号标签、v-text、v-html、v-bind 指令》 《Vue指令&#xff1a;v-cloak、v-once、v-pre 指令》 《Vue条件判断&#xff1a;v-if、v-else、v-else-if、v-show 指令》 《Vue循环遍历&#xff1a;v-for 指令》 《Vue事件…

【机器学习练习】糖尿病预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、数据预处理 1. 数据导入 import numpy as np import pandas as pd import seaborn as sns from sklearn.model_selec…

css 将背景图片居中显示,并且显示全,不拉伸的效果实现,图片设置宽度,高度自适应,并且显示全。

1、背景自适应的css&#xff1a; .bg-tns {background-image: url(img/xxx.jpg);background-size: cover;background-position: center; /* 将图片居中显示 */min-height: calc(100vh-100px); /* 确保至少为视口高度 */} 这个布局是这样的&#xff1a; 这里的背景图的高度&am…

unity Standard Assests资源商店无法安装解决方案

Unity游戏开发 “好读书&#xff0c;不求甚解&#xff1b;每有会意&#xff0c;便欣然忘食。” 本文目录&#xff1a; Unity游戏开发 Unity游戏开发unity中国 嗨嗨嗨&#xff0c;我来啦。 这几天的川渝之旅已经圆满结束了&#xff0c;今天开始正常给大家更新&#xff1a; Unity…

Unity与UE,哪种游戏引擎适合你?

PlayStation vs Xbox&#xff0c;Mario vs Sonic&#xff0c;Unreal vs Unity&#xff1f;无论是游戏主机、角色还是游戏引擎&#xff0c;人们总是热衷于捍卫他们在游戏行业中的偏爱。 专注于游戏引擎&#xff0c;Unity和Unreal Engine&#xff08;简称UE4&#xff09;是目前市…

利用漏洞实现 Outlook 的 RCE:第 2 部分

## 攻击面 Outlook 要播放的声音文件是波形音频文件格式( WAV)。它通过接收声音文件路径的PlaySound函数播放。PlaySound将加载文件、解析它,然后调用soundOpen,后者将调用不同的波形函数,例如waveOutOpen。 WAV 文件充当多个音频编解码器的容器(或包装器)。编解码器是一…

Shire 0.5 发布:构建数据安全 RAG,充分整合研发资产

最近&#xff0c;我们发布了新版本的 Shire&#xff0c;在这个新的发布&#xff08;Shire 0.5&#xff09;里&#xff0c;你可以更好地融合本地研发资产&#xff0c;同时构建数据安全 RAG。在这次版本中&#xff0c;我们增加了&#xff1a; 对 SonarQube 的 issue 支持。可以直…

从0到1构建视频汇聚生态:EasyCVR视频汇聚平台流媒体协议支持的前瞻性布局

TSINGSEE青犀EasyCVR视频汇聚平台是一款基于云-边-端一体化架构的视频融合AI智能分析平台&#xff0c;广泛应用于工地、仓储、工厂、社区、校园、楼宇等多个领域。平台凭借其强大的数据接入、处理、转码及分发能力&#xff0c;在视频监控领域展现出显著的技术优势和应用前景。本…

python项目在linux中的启动脚本(shell脚本)

背景&#xff1a; 在linux环境&#xff0c;使用shell脚本&#xff0c;实现对某个服务的启动、停止功能。 shell脚本的功能&#xff1a; 启动&#xff08;start&#xff09;&#xff1a;通过参数 start &#xff0c;实现启动服务。如果该服务已经启动&#xff0c;给出已经启动…

松下弧焊机器人维修 控制柜故障 连接线修复

一、Panasonic焊接机器人控制柜与机器人的接线 机器人的控制箱&#xff0c;一定要配对使用。松下焊接机器人控制柜已经记忆了机器人的绝对原点(机器人位置控制原点)。 二、编码器电缆 (圆形连接器) 1. 接口的插头插座要注意&#xff0c;插头要插到插座中。 2. 用一手握住电缆&a…

《黑神话:悟空》发售后快手游戏笔记本电脑GMV日环比增长40%

一、美联储9月降息预期升温 昨夜美股三大股指在震荡中收涨&#xff0c;市场情绪受到美联储7月会议纪要提振。纪要显示&#xff0c;美联储官员普遍倾向于9月降息&#xff0c;多位官员认为当前利率水平已对经济构成限制。此消息推动美股上涨&#xff0c;并带动金价创新高&#xf…

鸿蒙HarmonyOS编程开发:TLS单向认证通讯示例

1.TLS简介 TLS&#xff08;Transport Layer Security&#xff09;协议的前身是SSL&#xff08;Secure Socket Layer&#xff09;安全套接层协议&#xff0c;由Netscape公司于1994年提出&#xff0c;是一套网络通信安全协议。IETF&#xff08;The Internet Engineering Task Fo…

前端开发中的大屏布局方案:使用 rem 单位与动态设置 html 的 font-size

使用 rem 单位与动态设置 html 的 font-size 前言 随着设备尺寸的多样化&#xff0c;网页需要能够在不同大小的屏幕上提供良好的用户体验。传统的布局方式&#xff08;如使用 px&#xff09;在不同分辨率下可能会导致布局失真。为了解决这个问题&#xff0c;我们可以通过动态…

【UE5】基于摄像机距离逐渐剔除角色

效果 步骤 1. 新建一个工程&#xff0c;在内容浏览器中添加第三人称游戏内容包 2. 找到第三人称角色的材质实例“MI_Quinn_01”并打开 找到材质实例的父项材质“M_Mannequin” 打开材质“M_Mannequin” 在材质图表中添加如下节点 此时运行效果如文章开头所示。 参考视频&#…

flutter 中 ssl 双向证书校验

SSL 证书&#xff1a; 在处理 https 请求的时候&#xff0c;通常可以使用 中间人攻击的方式 获取 https 请求以及响应参数。应为通常我们是 SSL 单向认证&#xff0c;服务器并没有验证我们的客户端的证书。为了防止这种中间人攻击的情况。我么可以通过 ssl 双向认证的方式。即…

创意无限,尽在掌握:热门视频剪辑软件一览

我们记录生活、分享故事、传播信息用视频的频率越来越高了。而这些视频往往都是通过剪辑之后才能展示出当前的效果。那这次我们就来探索剪辑视频的时候都会用到什么工具吧。 1.福昕视频剪辑 连接直达>>https://www.pdf365.cn/foxit-clip/ 这是一款专为追求高效与创意…

【React】为什么Hooks不能出现在判断中

前言 在 React 中&#xff0c;Hooks 不能写在条件语句中&#xff0c;如下面这段代码点击button后则会报错。 import { useEffect, useState } from "react"export default () > {const [count, setCount] useState(0)if (count > 0) {useEffect(() > {co…

2-74 基于matlab的图像k-means聚类GUI

基于matlab的图像k-means聚类GUI&#xff0c;可对彩色图像进行Kmeans和meanshift进行聚类分析&#xff0c;生成最后的聚类图像以及聚类中心的迭代轨迹。程序已调通&#xff0c;可直接运行。 2-74 matlab GUI - 小红书 (xiaohongshu.com)

iOS 通知

iOS 通知分为本地推送和远程推送两类 一. 本地推送使用流程 1. 注册通知 //请求通知权限 UNUserNotificationCenter *center [UNUserNotificationCenter currentNotificationCenter];[center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizati…