工作流学习日志 -(Activiti7进阶)
作者:互联网
工作流学习日志 -(Activiti7进阶)
文章目录
前言
开发OA人力资源管理系统时在人员调动时、请假审批等需要使用到工作流,以此作为学习契机。
工作流学习日志(基础-概念、基本api使用)
一、流程实例
流程实例(ProcessInstance)通过流程定义(ProcessDefinition)创建,代表了流程定义的实例。
1. 添加业务标识(BusinessKey)
BusinessKey:业务标识,通常是业务表的主见id,业务标识和流程实例一一对应。存储业务标识就是根据业务标识来关联查询业务系统的数据。其中的业务和流程是分开的,activiti主要是控制流程,而不是控制所有的的业务,所以需要使用有一张其他的表表示业务,最后可以进行关联。
例如:出差流程是一个流程实例,可以将出差单(业务)id作为BusinessKey存储到activiti中。将来查询activiti流程实例的信息就可以获取到对应的出差单(业务)id从而关联到业务系统数据库中的出差单信息。
添加businessKey并启动流程实例代码:
//1.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
//3.添加businessKey(可以指某用户Id,开启一个新的工作流程 例如为1001)
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("myBusinessTrip", "1001");
System.out.println("流程业务名称="+processInstance.getBusinessKey());
添加成功后可以在Activiti数据库act_ru_execution中查看到存储的businessKey。
查询关联的businessKey:
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取TaskService
TaskService taskService = processEngine.getTaskService();
// 获取RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 查询流程定义的对象
Task task = taskService.createTaskQuery()
.processDefinitionKey("myEvection1")
.taskAssignee("张三") .singleResult();
// 使用task对象获取实例id
String processInstanceId = task.getProcessInstanceId();
// 使用实例id,获取流程实例对象
ProcessInstance processInstance = runtimeService
.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
// 使用processInstance,得到 businessKey
String businessKey = processInstance.getBusinessKey();
System.out.println("businessKey=="+businessKey); }
2. 查询流程实例
流程在运行的过程中可以查询流程实例的状态,当前运行节点等信息
// 流程定义key
String processDefinitionKey = "evection"; ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取RunTimeService
RuntimeService 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());
}
3. 挂起、激活流程实例
某些情况下流程变更需要当前运行的流程暂停而不是直接删除,那么可以将流程进行挂起,流程挂起后将不能继续执行流程,需要激活后才能继续执行。
例如:当审批员有变更的时候,那么审批的流程需要进行暂停,待审批员变更完成后才能继续进行执行;或者在非工作日时不能进行流程执行操作。
①. 全部流程实例挂起、激活
- 将整个流程定义为挂起状态,该流程定义下的所有流程实例将全部挂起。
- 流程定义为挂起状态后,该流程定义将不允许启动新的流程实例,同时该流程定义定义下的所有流程实例将全部挂起暂停执行。
代码如下:
//1.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取RepositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
//3.获取到流程定义的查询对象
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("myBusinessTrip")
.singleResult();
//4.查询processDefinition流程定义的状态
boolean suspended = processDefinition.isSuspended();
//5.如果挂起,那么激活流程定义
String definitionId = processDefinition.getId();
if (suspended) {
//参数1:流程定义id,参数2:流程是否激活,参数3:流程激活时间
repositoryService.activateProcessDefinitionById(definitionId,true,null);
System.out.println("流程定义id为:"+definitionId+"激活成功");
} else{
//6.如果正在活动,那么将流程定义挂起
//参数1:流程定义id,参数2:流程是否挂起,参数3:流程挂起时间
repositoryService.suspendProcessDefinitionById(definitionId,true,null);
System.out.println("流程定义id为:"+definitionId+"挂起成功");
}
②. 单个流程实例挂起、激活
操作流程实例的对象,针对单个流程执行挂起操作,某个流程实例挂起则此流程不再继续进行执行,完成该流程实例的当前任务将报异常。
代码如下:
//1.获取processEngine 引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取runtimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
//3.根据processInstanceId获取指定的流程实例查询对象
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId("2501")
.singleResult();
//4.根据processInstance流程实例获取其状态
boolean suspended = processInstance.isSuspended();
//5.如果为挂起状态,那么激活
String instanceId = processInstance.getId();
if(suspended){
runtimeService.activateProcessInstanceById(instanceId);
System.out.println("流程实例对象Id="+instanceId+",已经激活");
}else {
//6.如果流程实例的状态为激活状态,那么将其挂起
runtimeService.suspendProcessInstanceById(instanceId);
System.out.println("流程实例对象Id="+instanceId+",已经挂起");
}
- 测试如果任务实例被挂起之后那么是否可以完成任务
- 如果任务激活状态:那么执行成功
- 如果为挂起状态:抛出异常Cannot complete a suspended task
//1.获取流程引擎processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取对应的taskService
TaskService taskService = processEngine.getTaskService();
//3.根据taskService获取对应的流程任务查询
Task task = taskService.createTaskQuery()
.processInstanceId("2501")//根据流程实例id进行查询
.taskAssignee("lisi")
.singleResult();
System.out.println("流程实例id="+task.getProcessInstanceId());
System.out.println("任务Id="+task.getId());
System.out.println("任务负责人="+task.getAssignee());
System.out.println("任务名称="+task.getName());
//4.根据taskId完成对应的任务
taskService.complete(task.getId());
}
二、个人任务
1. 分配任务负责人
①. 固定分配方式
直接在BPMN editor中将property为Assignee的值value设置为固定值,例:张三
②. 表达式分配
表达式分配可以解决固定分配情况下的assignee无法动态获取值的问题。
UEL表达式:
Activiti使用UEL表达式,UEL是java EE6规范的一部分,UEL(Unified Expression Language)即统一表达式语言,activiti支持两个UEL表达式:UEL-value和UEL-method。
1)UEL-value:
可以直接设置一个变量assignee0,该变量为activiti的一个流程变量。
也可以通过调用某个类中的属性获取值,例如user是activiti中一个流程变量,user.assignee表示通过调用user的getter方法获取值。
2)UEL-method方式
UserBean是spring容器中的一个bean,表示调用该bean的getUserId()方法。
3)UEL-method与UEL-value结合
再比如: ${ldapService.findManagerForEmployee(emp)} ldapService 是 spring 容器的一个 bean, findManagerForEmployee 是该 bean 的一个方法,emp 是 activiti 流程变量, emp 作为参数传到
ldapService.findManagerForEmployee 方法中。
4)其他
表达式支持解析基础类型、 bean、 list、 array 和 map,也可作为条件判断。 如下: ${order.price > 100 && order.price < 250}
在启动流程实例时设置流程变量,如下:
//1.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取runtimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
//3.启动定义的流程,startProcessInstanceByKey两参数,一:流程实例的key,二:流程中定义的负责人名称使用map进行封装
//使用map封装负责人的值,可以在act_ru_variable数据表中查看设置的map值
Map map = new HashMap();
map.put("assignee0","张三");
map.put("assignee1","李四");
map.put("assignee2","王五");
map.put("assignee3","赵六");
runtimeService.startProcessInstanceByKey("myProcess_1",map);
//4.输出
System.out.println("流程启动成功="+processEngine.getName());
执行成功后可以在数据库act_ru_variable表中看到刚才map中的数据
注:由于使用了表达式分配,必须保证在任务执行过程表达式执行成功,比如: 某个任务使用了表达式${order.price > 100 && order.price < 250},当执行该任务时必须保证 order 在 流程变量中存在,否则 activiti 异常。
③. 监听器分配
Activiti中也有内置的监听器可以用来完成流程中的业务。可以使用任务监听器(Task Listener)指定负责人,那么在流程设计时可以不需要指定assignee。任务监听器是发生对应的任务相关事件时执行自定义java逻辑或表达式。
Event事件中包含了4个触发事件:
- create:任务创建后触发
- Assignment:任务分配后触发
- Delete:任务完成后触发
- ALL:所有事件发生都触发
代码如下:(定义任务监听类必须实现org.activiti.engine.delegate.TaskListener接口)
public class TestListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
//判断
if("创建申请".equals(delegateTask.getName()) && "create".equals(delegateTask.getEventName())){
delegateTask.setAssignee("张三");
}
}
}
注:使用监听器分配的方式,按照监听事件去执行监听类的notify方法,如果不能正常执行也会影响任务的执行。
2. 查询任务
查询任务负责人的待办任务,代码如下:
//选择负责人
String assignee = "zhangsan";
//获取到processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//获取到taskService
TaskService taskService = processEngine.getTaskService();
//使用TaskService根据流程的key和任务负责人查询任务
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("myBusinessTrip")//流程的key
.taskAssignee(assignee)//查询该负责人的任务
.list();
//遍历获取任务内容
for (Task task : list) {
System.out.println("流程实例:"+task.getProcessInstanceId());
System.out.println("任务id:"+task.getId());
System.out.println("任务负责人:"+task.getAssignee());
System.out.println("任务名称:"+task.getName());
}
代码如下(示例):
data = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv')
print(data.head())
3. 办理任务
注:实际应用中,完成任务前需要检验任务的负责人是否具有该任务的办理权限
String assignee = "zhangsan";
//1.创建processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取到TaskService
TaskService taskService = processEngine.getTaskService();
//完成任务前,需要检验该负责人是否可以完成当前任务
//检验方法:根据任务的id和任务负责人查询当前任务,如果查到该用户有权限,那么就完成。
//3.使用TaskService中的方法(使用任务id)完成任务
//taskService.complete("2505");
//4.根据流程的key以及负责人名称获取对应的任务id,动态进行完成任务
Task task = taskService.createTaskQuery()
.processDefinitionKey("myBusinessTrip")//流程的key
.taskAssignee(assignee)//任务负责人名称
.singleResult();//获取单个task任务
System.out.println("任务的id="+task.getProcessDefinitionId());
System.out.println("任务id="+task.getId());
System.out.println("任务的名称="+task.getName());
System.out.println("任务的负责人="+task.getAssignee());
//5.动态获取任务id完成任务
taskService.complete(task.getId());
三. 流程变量
1. 概念
流程变量的activiti中是一个非常重要的角色,流程运转有时需要靠流程变量,业务系统和activiti结合时少不了流程变量,流程变量就是activiti在管理工作流时根据管理需要而设置的变量。比如:在出差申请流程流转时如果出差天数大于3天那么由总经理审核,否则由部门经理进行审核,在这里的出差天数就可以设置为流程变量,在流程运转时使用。
注:流程变量中可以存储业务数据,可以通过activiti的api查询流程变量从而实现查询业务流程,单一一般不这样做,因为业务数据查询由业务系统负责,activiti设置流程变量是为了流程执行需要而创建。
2. 流程变量类型
注:如果需要将pojo存储在流程变量中,那么必须实现序列化接口serializable。为了防止由于新增字段无法反序列化,需要生成serialVersionUID。
类型名称:
- string
- integer
- short
- long
- double
- Boolean
- date
- binary
- serializable
3. 流程变量作用域
流程变量作用域可以是一个流程实例(processInstance)、任务(task)、执行实例(execution)
①. global变量
流程变量的默认作用域是流程实例。当一个流程变量的作用域为流程实例时,可以称为 global 变量
注:
如: Global变量:userId(变量名)、zhangsan(变量值)
global 变量中变量名不允许重复,设置相同名称的变量,后设置的值会覆盖前设置的变量值。
②. local变量
任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大, 称为 local 变量。
Local 变量由于在不同的任务或不同的执行实例中,作用域互不影响,变量名可以相同没有影响。Local 变量名也可以和 global 变量名相同,没有影响。
4. 流程变量的使用方法
①. 在属性上使用UEL表达式
在assignee设置UEL表达式设置为任务的负责人
②. 在连线上使用UEL表达式
在连线上设置UEL表达式,可以决定流程的走向,UEL表达式结果为布尔类型。如图:
③. 设置global变量
通过设置global变量控制流程走向
在部门经理审核前设置流程变量,变量值为出差单信息(包括出差天数),部门经理审核后可以根据流程变量的值决定流程走向。
在设置流程变量时,可以在启动流程时设置,也可以在任务办理时设置
创建POJO对象:
/**
* 流程封装的业务类
* 出差申请pojo
*/
public class BusinessTrip implements Serializable {
/**
* 主键id
*/
private Long id;
/**
* 出差申请单名称
*/
private String businessName;
/**
* 出差天数
*/
private Double num;
/**
* 预计开始时间
*/
private Date beginDate;
/**
* 预计结束时间
*/
private Date endDate;
/**
* 目的地
*/
private String destination;
/**
* 出差事由
*/
private String reason;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getBusinessName() {
return businessName;
}
public void setBusinessName(String businessName) {
this.businessName = businessName;
}
public Double getNum() {
return num;
}
public void setNum(Double num) {
this.num = num;
}
public Date getBeginDate() {
return beginDate;
}
public void setBeginDate(Date beginDate) {
this.beginDate = beginDate;
}
public Date getEndDate() {
return endDate;
}
public void setEndDate(Date endDate) {
this.endDate = endDate;
}
public String getDestination() {
return destination;
}
public void setDestination(String destination) {
this.destination = destination;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}
启动流程时设置变量
在启动流程时设置流程变量,变量的作用域是整个流程实例。通过Map设置流程变量,map中可以设置多个变量,key就是流程变量的名字
流程变量作用域是一个流程实例,流程变量使用Map存储,同一个流程实例设置变量map中key相同,后者覆盖前者。
//1.创建流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取runtimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
//获取到流程的key
String processInstanceKey = "buiness-global1";
//设置任务负责人参数
BusinessTrip businessTrip = new BusinessTrip();
businessTrip.setNum(3d);
Map variable = new HashMap();
variable.put("businessTrip",businessTrip);
variable.put("assignee0","张三员工1");
variable.put("assignee1","李四部门经理1");
variable.put("assignee2","王五总经理1");
variable.put("assignee3","赵六财务1");
//3.根据processInstance的id以及传入的参数进行开启流程
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey(processInstanceKey, variable);
//4.输出
System.out.println("流程实例名称="+processInstance.getName());
System.out.println("流程定义id=="+processInstance.getProcessDefinitionId());
任务办理时设置变量:
在完成任务时设置流程变量,该流程变量只有在该任务完成后其他结点才可以使用该变量,它的作用域是整个流程实例,如果设置的流程变量的key在流程实例中已存在相同的名字,则后设置的变量替换前边设置的变量。
//1.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取到taskService
TaskService taskService = processEngine.getTaskService();
/*//3.获取需要进行查询的参数67501
String processInstanceId = "55001";
String assignee = "赵六财务";*/
String processInstanceId = "85001";
String assignee = "赵六财务1";
BusinessTrip businessTrip = new BusinessTrip();
businessTrip.setNum(2d);
Map<String, Object> map = new HashMap<>();
map.put("businessTrip",businessTrip);
//4.创建任务查询
Task task = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.taskAssignee(assignee)
.singleResult();
if (task != null){
//5.如果task不为空,那么完成任务。
taskService.complete(task.getId(),map);
System.out.println("任务执行完成");
}
通过当前流程实例设置:
通过流程实例id设置全局变量,该流程实例必须未执行完成
// 当前流程实例执行 id,通常设置为当前执行的流程实例
String executionId="2601";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 创建出差pojo对象
Evection evection = new Evection();
// 设置天数
evection.setNum(3d);
// 通过流程实例 id设置流程变量
runtimeService.setVariable(executionId, "evection", evection);
// 一次设置多个值
// runtimeService.setVariables(executionId, variables)
注:
- 如果UEL表达式中流程变量名不存在则报错
- 如果UEL表达式中流程变量值为空null,流程不安UEL表达式去执行,而流程结束。
- 如果UEL表达式都不符合条件,流程结束
- 如果连线不设置条件,会走flow需要小的那条线
四. 组任务
1. 需求
在流程定义中在任务结点的 assignee 固定设置任务负责人,在流程定义时将参与者固定设置在.bpmn 文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性差。
针对这种情况可以给任务设置多个候选人,可以从候选人中选择参与者来完成任务。
2. 设置任务候选人
在流程图中任务节点的配置中设置candidate-users(候选人),多个候选人之间用逗号分开。
3. 组任务
组任务办理流程
①. 查询组任务
指定候选人,查询该候选人当前的待办任务
//设置需要获取到的任务查询的条件
String processDefinitionKey = "testcandidate";
String candidateUser = "王副部门经理";
//1.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取taskService
TaskService taskService = processEngine.getTaskService();
//3.使用service获取到任务查询list
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey(processDefinitionKey)
.taskCandidateUser(candidateUser)
.list();
//4.输出信息
for (Task task : list) {
System.out.println("----------------------------");
System.out.println("流程实例id:" + task.getProcessInstanceId());
System.out.println("任务id:" + task.getId());
System.out.println("任务负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());
}
②. 拾取(claim)任务
该任务的所有候选人都能拾取。将候选人的组任务变成个人任务,原来候选人就变成该任务的负责人。
//1.设置需要获取的任务查询条件
String taskId = "105002";
String candidateUser = "王副部门经理";
//2.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//3.获取taskService
TaskService taskService = processEngine.getTaskService();
//4.创建任务查询
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskCandidateUser(candidateUser)
.singleResult();
//5.如果该任务查询出来存在
if(task != null){
//根据任务的id以及拾取候选人为负责人
taskService.claim(task.getId(),candidateUser);
System.out.println("任务id--"+taskId+"--的候选人--"+candidateUser+"拾取任务完成");
}
如果拾取后不想办理该任务,需要将已经拾取的个人任务归还到组里,将个人任务变成了组任务。
//1.设置需要获取的任务查询条件
String taskId = "105002";
String assignee = "王副部门经理";
//2.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//3.获取taskService
TaskService taskService = processEngine.getTaskService();
//4.创建任务查询
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(assignee)
.singleResult();
//5.如果该任务查询出来存在
if(task != null){
//根据任务的id设置负责人为null即为归还负责人
taskService.setAssignee(task.getId(),null);
System.out.println("任务id--"+taskId+"--的负责人--"+assignee+"归还任务完成");
}
任务的交接,实质上就是讲assignee进行修改
String taskId = "105002";
String assignee = "王副部门经理";
String candidateUser = "李部门经理";
//1.获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.获取taskService
TaskService taskService = processEngine.getTaskService();
//3.根据taskService获取到任务查询
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(assignee)
.singleResult();
//4.如果能够查询出来任务,那么就将任务的负责人进行交接
if(task != null){
taskService.setAssignee(task.getId(),candidateUser);
System.out.println("任务id--"+taskId+"--的负责人--"+assignee+"交接任务给"+candidateUser+"完成");
}
③. 查询个人任务
查询方法同个人人物部分,根据assignee查询用户负责的个人任务。
④. 办理个人任务
同以前的完成任务
五. 网关
网管主要是用来控制流程的流向。我们在前面学习了使用UEL表达式的方式直接在连线上来控制流程,但该方法并有时候会造成创建两次流程实例的情况,导致不能按照我们设定的要求进行走向;如果条件都不满足,流程就结束了(异常结束)。所以一般还需要网关进行控制流程走向。
1. 排他网关ExclusiveGateway
①. 概念
排他网关,用在流程中实现决策,当流程执行了该网关,所有的分支会判断条件是否为true。如果为true则执行该分支。
注:
- 排他网关只会选择一个为true的分支执行,如果有两个分支的条件都是true,那么排他网关会选择id值较小的一条分支进行执行。
- 如果排他网关控制的所有分支都不满足true,那么系统会跑出异常。
②. 用法
用法和以前一样,部署、开启流程实例、完成执行实例。
图片如下:
2. 并行网关ParallelGateway
并行网关允许多个分支并行执行,也可以将多条分支汇聚到一起,并行网关的功能基于进入和外出顺序流的:
- fork分支:通过并行网关后将一条流程分成多条分支成外出顺序流,
- join汇聚:所有到达并行网关后,再次等待的进入分支,直到所有进入顺序流的分支都到达之后,流程就会汇聚网关。
注:如果同一个并行网关有多个进入和多个外出顺序流,他就同时具有了分支和汇聚的功能,这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。与其他网关主要区别是,并行网关不会解析条件,即使顺序流定义了条件,也会忽略执行。
3. 包含网关InclusiveGetaway
包含网关可以看做排他网关和并行网关的结合体。
可以和排他网关一样,在顺序流上定义条件,包含网关会解析他们,但是主要区别是包含网关可以选择多与一条顺序流,这和并行网关一样。
如图所示:无论什么条件都会进入到人是审批中。接着从技术经理或项目经理满足条件的一个进行汇聚。
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。
标签:task,进阶,实例,流程,任务,Activiti7,日志,processEngine,id 来源: https://blog.csdn.net/weixin_46621774/article/details/115612158