文章目录
1. 流程实例
1.1 什么是流程实例
流程实例(ProcessInstance)代表流程定义的执行实例
一个流程实例包括了所有的运行节点,我们可以利用这个对象来了解当前流程实例的进度等信息
例如:用户或者程序安装流程定义的内容发起了一个流程,这个就是一个流程实例
1.2 业务管理
流程定义部署在Activiti后,我们就可以在系统中通过Activiti去管理流程的执行,但是如果我们要将我们的 流程实例
和 业务数据
关联,这时我们需要使用到Activiti中预留的 BusinessKey (业务标识) 来关联(就是activiti通过预设定好的流程帮助我们自动管理流程,但是流程实例有时需要和具体的业务挂钩,所以可以使用activiti给我们预留的businessKey)
实现代码
先回顾一下部署流程定义的代码(自己手动的)
(注意:1次部署,就会在act_re_deploy表中插入1条数据,假设2次都部署同1个文件,并且参数都不改的情况下—> 即2次调用部署的方法,那么这2次部署产生的2条数据的name_和key_都是相同的,但是它们的id_不同,其中name和key都是在方法中指定的。在act_re_procdef表中也会插入2条数据,它们的name_和key_也都是相同的,但是它们的id_是不同的,其中name和key是在bpmn.xml中的name属性和id属性指定的,id_是key:{0}:{1})的形式,并且这2条数据都是1个deploy_id_字段来标识是哪1次部署生成的流程定义,还有需要注意的就是,这2条流程定义信息的key如果一样的话,还有个版本号version_字段,当使用流程引擎提供的startProcessInstanceByKey方法时,会选择指定key中最大版本的那条流程定义数据来开启1个流程实例
@Test
public void test01() {
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
Deployment deploy = processEngine.getRepositoryService().createDeployment()
.addClasspathResource("bpmn/evection.png")
.addClasspathResource("bpmn/evection.bpmn")
.name("流程申请单阿")
.key("evectionah")
.category("haa")
.deploy();
/*
流程部署的id: 1
流程部署的name: 流程申请单阿
流程部署的key: evectionah
流程部署的category: haa
*/
log.info("流程部署的id: {}", deploy.getId());
log.info("流程部署的name: {}", deploy.getName());
log.info("流程部署的key: {}", deploy.getKey());
log.info("流程部署的category: {}", deploy.getCategory());
// 根据流程部署id,去查询刚刚部署的流程定义
ProcessDefinition processDefinition = processEngine.getRepositoryService().createProcessDefinitionQuery()
.deploymentId(deploy.getId()).singleResult();
/*
流程定义的id: evection2:1:4 - 它的前面部分是 (创建bpmn文件时,点击空白位置时) 填入的id
流程定义的key: evection2 - 创建bpmn文件时,点击空白位置时,填入的id
流程定义的name: 出差申请单1 - 创建bpmn文件时,点击空白位置时,填入的name
*/
log.info("流程定义的id: {}", processDefinition.getId());
log.info("流程定义的key: {}", processDefinition.getKey());
log.info("流程定义的name: {}", processDefinition.getName());
}
开启1个流程实例, 并且指定businessKey(案例中的)
/**
* 启动流程实例,添加businessKey
*/
@Test
public void test01() {
// 1.获取ProcessEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2.获取RuntimeService对象
RuntimeService runtimeService = processEngine.getRuntimeService();
// 3.启动流程实例
// (第 1 个参数是:创建bpmn流程图文件时,点击空白位置时,填入的流程定义id)
// (第 2 个参数是:一般是先把业务数据保存好后, 把业务数据的主键放到这, 将activiti的流程实例与业务关联起来)
// (这个businessKey保存在act_ru_execution表的BUSINESS_KEY_字段中)
// (这个businessKey也会保存在act_hi_procinst表的BUSINESS_KEY_字段中)
ProcessInstance instance = runtimeService.startProcessInstanceByKey("evection", "1001");
// 4.输出processInstance相关属性
System.out.println("businessKey = " + instance.getBusinessKey());
}
(下图:act_ru_execution表中的BUSINESS_KEY_字段)
1.3 流程实例的挂起和激活
在实际场景中可能由于流程变更需要将当前运行的流程暂停,而不是删除,流程暂停后将不能继续执行。
全部流程挂起
操作流程的定义为挂起状态,该流程定义下边所有的流程实例全部暂停。
流程定义为挂起状态,该流程定义将不允许启动新的流程实例
,同时该流程定义下的所有的流程实例都 将全部挂起暂停执行
。
/**
* 全部流程挂起实例与激活
*/
@Test
public void test02() {
// 1.获取ProcessEngine对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取RepositoryService对象
RepositoryService repositoryService = engine.getRepositoryService();
// 3.查询流程定义的对象(根据流程定义key,去查询流程定义对象)
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("evection")
.singleResult();
// 4.获取当前流程定义的状态
boolean suspended = processDefinition.isSuspended();
String id = processDefinition.getId();
// 5.如果挂起就激活,如果激活就挂起
if (suspended) {
// 表示当前定义的流程状态是 挂起的
repositoryService.activateProcessDefinitionById(
id // 流程定义的id
, true // 是否激活
, null // 激活时间
);
System.out.println("流程定义:" + id + ",已激活");
} else {
// 非挂起状态,激活状态 那么需要挂起流程定义
repositoryService.suspendProcessDefinitionById(
id // 流程id
, true // 是否挂起
, null // 挂起时间
);
System.out.println("流程定义:" + id + ",已挂起");
}
}
挂起流程定义后,对于的实例对象中的状态会修改为2
(补充:对某个流程定义挂起后,表act_re_procdef表的SUSPENSION_STATE_字段会修改为2,同时,表act_ru_task表的SUSPENSION_STATE_字段也会修改为2)
然后再去操作对于的流程实例的话(完成该实例的当前任务),会抛如下异常信息
我们再将挂起的流程转变为激活状态,对于的状态值会从2更新为1
然后就是业务流程可以正常处理了
单个实例挂起
操作流程实例对象,针对单个流程执行挂起操作,某个流程实例挂起则此流程不再继续执行,当前流程 定义的其他流程实例是不受干扰的。完成该流程实例的当前任务会抛异常
/**
* 单个流程实例挂起与激活
*/
@Test
public void test03() {
// 1.获取ProcessEngine对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取RuntimeService
RuntimeService runtimeService = engine.getRuntimeService();
// 3.获取流程实例对象(根据流程实例id,去查询指定的流程实例对象)
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId("25001")
.singleResult();
// 4.获取相关的状态操作
boolean suspended = processInstance.isSuspended();
String id = processInstance.getId();
if (suspended) {
// 挂起--》激活
runtimeService.activateProcessInstanceById(id);
System.out.println("流程定义:" + id + ",已激活");
} else {
// 激活--》挂起
// (taskService.createTaskQuery().processInstanceId("10001").active().list(); 查询时, 是不包括挂起的流程实例的)
// (可以观察到act_ru_task表中SUSPENSION_STATE_字段由1改为了2, 该流程实例对应的任务状态为从激活状态变为了挂起状态)
runtimeService.suspendProcessInstanceById(id);
System.out.println("流程定义:" + id + ",已挂起");
}
}
然后我们可以在数据库中查看到状态的更新
1.4 BpmnModel
查询流程元素FlowElement
根据流程定义Id查询ACT_RE_PROCDEF.DEPLOYMENT_ID_,进而查询ACT_GE_BYTEARRAY.BYTES_,然后解析.bpmn文件,封装成Model对象。
@Test
public void testQueryFlowElement(){
BpmnModel bpmnModel = repositoryService.getBpmnModel("helloworld:1:3");
// Process就是.bpmn文件中的<process>节点
Process process = bpmnModel.getProcesses().get(0);
Collection<FlowElement> flowElements = process.getFlowElements();
for (FlowElement flowElement : flowElements) {
if (flowElement instanceof UserTask) {
UserTask userTask = (UserTask)flowElement;
System.out.println(userTask);
}
}
// 直接根据Id属性获取UserTask
UserTask userTask = (UserTask)bpmnModel.getFlowElement("_4");
System.out.println(userTask);
}
获取指定节点的下一个节点
根据流程定义Id查询ACT_RE_PROCDEF.DEPLOYMENT_ID_,进而查询ACT_GE_BYTEARRAY.BYTES_,然后解析.bpmn文件,封装成Model对象。
/**
* 获取指定节点的下一个节点
* 排它网关:获取所有分支
*/
@Test
public void getNextNode() {
BpmnModel bpmnModel = repositoryService.getBpmnModel("leaveProcess:1:3");
UserTask userTask = (UserTask) bpmnModel.getFlowElement("pmApprove");
// 获取该节点出去的SequenceFlow
List<SequenceFlow> outgoingFlows = userTask.getOutgoingFlows();
for (SequenceFlow sequenceFlow : outgoingFlows) {
FlowElement flowElement = bpmnModel.getFlowElement(sequenceFlow.getTargetRef());
if (flowElement instanceof ExclusiveGateway) {
ExclusiveGateway exclusiveGateway = (ExclusiveGateway) flowElement;
List<SequenceFlow> gatewayOutgoingFlows = exclusiveGateway.getOutgoingFlows();
for (SequenceFlow gatewaySequenceFlow : gatewayOutgoingFlows) {
FlowElement flowElementItem = bpmnModel.getFlowElement(gatewaySequenceFlow.getTargetRef());
System.out.println(flowElementItem.getId() + "-" + flowElementItem.getName());
}
} else {
System.out.println(flowElement.getId() + "-" + flowElement.getName());
}
}
}
2. 个人任务
2.1 分配任务责任人
2.1.1 固定分配
在进行业务流程建模的时候指定固定的任务负责人:
在Properties视图中,填写Assiginee项为任务负责人
2.1.2 表达式分配
在Activiti中支持使用 UEL表达式,UEL表达式是Java EE6 规范的一部分, UEL(Unified Expression Language) 即 统一表达式语言
, Activiti支持两种UEL表达式: UEL-value
和 UEL-method
。
表达式可以用于例如Java服务任务 Java Service tasks , 执行监听器 Execution Listeners , 任务监听器Task Listeners 与 条件流 Conditional sequence flows。尽管有值表达式与方法表达式两种表达式,通过Activiti的抽象,使它们都可以在需要 expression (表达式)的地方使用。
UEL-value
在assignee中使用流程变量处理
(创建请假单的Assignee设置为${assignee0}
)
(经理审批的Assignee设置为${assignee1}
)
(经理审批的Assignee设置为${assignee2}
)
(经理审批的Assignee设置为${assignee3}
)
然后我们可以来操作
首先我们需要将定义的流程部署到Activiti数据库中
/**
* 先将新定义的流程部署到Activiti中数据库中
*/
@Test
public void test01() {
// 1.获取ProcessEngine对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取RepositoryService进行部署操作
RepositoryService service = engine.getRepositoryService();
// 3.使用RepositoryService进行部署操作
Deployment deploy = service.createDeployment()
.addClasspathResource("bpmn/evection-uel.bpmn") // 添加bpmn资源
.addClasspathResource("bpmn/evection-uel.png") // 添加png资源
.name("出差申请流程-UEL")
.deploy();// 部署流程
// 4.输出流程部署的信息
System.out.println("流程部署的id:" + deploy.getId());
System.out.println("流程部署的名称:" + deploy.getName());
}
部署成功后我们需要启动一个新的流程实例,然后在流程实例创建的时候关联UEL表达式
/**
* 创建一个流程实例
* 给流程定义中的 UEL表达式赋值
*/
@Test
public void test02() {
// 获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取RuntimeService对象
RuntimeService runtimeService = processEngine.getRuntimeService();
// 设 置 assignee 的 取 值 ,
Map<String, Object> map = new HashMap<>();
map.put("assignee0", "张三");
map.put("assignee1", "李四");
map.put("assignee2", "王五");
map.put("assignee3", "赵财务");
// 创建流程实例
runtimeService.startProcessInstanceByKey("evection-uel", map);
}
启动成功后我们在 act_ru_variable中可以看到UEL表达式对应的赋值信息
(在act_ru_task表中,我们可以看到多了一条当前流程实例的当前任务信息,并且这个任务的ASSIGNEE_字段为张三,并且在act_ru_variable表中,我们可以看到我们在启动一个流程实例时在map中所有指定的变量。在activiti中,如果下1个节点使用了uel表达式,但是前面又没有提供计算所需要的变量,那么activiti是会报错的)
UEL-method
方法表达式 Method expression: 调用一个方法,可以带或不带参数。当调用不带参数的方法时,要
确保在方法名后添加空括号(以避免与值表达式混淆)。传递的参数可以是字面值(literal value),也可以是表达式,它们会被自动解析。例如:
${printer.print()}
${myBean.getAssignee()}
${myBean.addNewOrder('orderName')}
${myBean.doSomething(myVar, execution)}
图中:userBean 是 spring 容器中的一个 bean,表示调用该 bean 的 getUserId()方法。
然后我们发起新的流程。注意在这块我们可以不用添加流程变量信息了。因为 创建请假单 的审批人是通过流程方法来赋值的
UEL-method 与 UEL-value 结合
再比如:${ldapService.findManagerForEmployee(emp)}
ldapService 是 spring 容器的一个 bean,findManagerForEmployee 是该 bean 的一个方法,emp 是activiti流程变量, emp 作为参数传到 ldapService.findManagerForEmployee 方法中。
其它
表达式支持解析基础类型、 bean、 list、 array 和 map,也可作为条件判断。如下:${order.price > 100 && order.price < 250}
2.1.3 监听器分配
可以使用监听器来完成很多Activiti的流程业务。
我们在此处使用监听器来完成负责人的指定,那么我们在流程设计的时候就不需要指定assignee
Event选项
create : 任务创建后触发
assignment: 任务分配后触发
complete : 任务完成后触发
delete : 任务删除后触发
All : 所有事件都触发
(idea的actiBpm插件ta喵的有问题)
自定义的任务监听器
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
public class MyTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
//(注意,这里需要添加判断条件)
if("创建请假单".equals(delegateTask.getName())
&& "create".equals(delegateTask.getEventName())){
// 表示是Task的创建事件被触发了
// 指定当前Task节点的处理人
delegateTask.setAssignee("张三-Listener");
}
}
}
测试代码
/**
* 先将新定义的流程部署到Activiti中数据库中
*/
@Test
public void test01() {
// 1.获取ProcessEngine对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取RepositoryService进行部署操作
RepositoryService service = engine.getRepositoryService();
// 3.使用RepositoryService进行部署操作
Deployment deploy = service.createDeployment()
.addClasspathResource("bpmn/evection-listener.bpmn") // 添加bpmn资源
.addClasspathResource("bpmn/evection-listener.png") // 添加png资源
.name("出差申请流程-UEL")
.deploy();// 部署流程
// 4.输出流程部署的信息
System.out.println("流程部署的id:" + deploy.getId());
System.out.println("流程部署的名称:" + deploy.getName());
}
/**
* 创建一个流程实例
* 给流程定义中的 UEL表达式赋值
*/
@Test
public void test02() {
// 获取流程引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取RuntimeService对象
RuntimeService runtimeService = processEngine.getRuntimeService();
// 创建流程实例
runtimeService.startProcessInstanceByKey("evection-listener");
}
如下图(act_ru_task表中刚刚创建的流程实例的当前任务的指定负责人就是设定的张三-Listener
)
2.2 查询任务
查询任务负责人的待办任务
代码如下:
// 查询当前个人待执行的任务
@Test
public void findPersonalTaskList() {
// 流程定义key
String processDefinitionKey = "myEvection1";
// 任务负责人
String assignee = "张三";
// 获取TaskService
TaskService taskService = processEngine.getTaskService();
List<Task> taskList = taskService.createTaskQuery()
.processDefinitionKey(processDefinitionKey)
.includeProcessVariables()
.taskAssignee(assignee)
.list();
for (Task task : taskList) {
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());
}
}
关联 businessKey
需求:
在 activiti 实际应用时,查询待办任务可能要显示出业务系统的一些相关信息。
比如:
查询待审批出差任务列表需要将出差单的日期、 出差天数等信息显示出来。出差天数等信息在业务系统中存在,而并没有在 activiti 数据库中存在,所以是无法通过 activiti 的 api查询到出差天数等信息。
实现:
在查询待办任务时,通过 businessKey(业务标识 )关联查询业务系统的出差单表,查询出出差天数等信息
。
(下面先查询张三的待办任务,然后根据待办任务查询流程实例,从流程实例中获取businessKey)
@Test
public void findProcessInstance() {
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取TaskService
TaskService taskService = processEngine.getTaskService();
// 获取RuntimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 查询流程定义的对象
// (当查询道德任务不止1个时,使用singleResult()会抛出异常!!!)
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.3办理任务
注意:在实际应用中,完成任务前需要校验任务的负责人是否具有该任务的办理权限 。
(在act_ru_task表中就记录了当前的所有任务,并且记录了每个任务对应的负责人,如果根据当前这个人 + 任务id 找不到这个任务,那么说明这个任务不属于当前这个人)
/**
* 完成任务,判断当前用户是否有权限
*/
@Test
public void completTask() {
//任务id
String taskId = "15005";
// 任务负责人
String assingee = "张三";
//获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 创建TaskService
TaskService taskService = processEngine.getTaskService();
// 完成任务前,需要校验该负责人可以完成当前任务
// 校验方法:
// 根据任务id和任务负责人查询当前任务,如果查到该用户有权限,就完成
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(assingee)
.singleResult();
if(task != null){
taskService.complete(taskId); System.out.println("完成任务");
}
}
3.流程变量
3.1 什么是流程变量
流程变量在 activiti 中是一个非常重要的角色,流程运转有时需要靠流程变量,业务系统和 activiti 结合时少不了流程变量,流程变量就是 activiti 在管理工作流时根据管理需要而设置的变量。
比如:在出差申请流程流转时如果出差天数大于 3 天则由总经理审核,否则由人事直接审核, 出差天数就可以设置为流程变量,在流程流转时使用。
注意:虽然流程变量中可以存储业务数据可以通过activiti的api查询流程变量从而实现 查询业务数据, 但是不建议这样使用,因为业务数据查询由业务系统负责,activiti设置流程变量是为了流程执行需要而 创建。
3.2 流程变量类型
如果将 pojo 存储到流程变量中,必须实现序列化接口 serializable
,为了防止由于新增字段无法反序列化,需要生成 serialVersionUID
。
3.3、流程变量作用域
流程变量可以用将数据添加到流程的运行时状态中,或者更具体地说,变量作用域中。改变实体的各种API可以用来更新这些附加的变量。一般来说,一个变量由一个名称和一个值组成。名称用于在整个流程中识别变量。例如,如果一个活动(activity)设置了一个名为 var 的变量,那么后续活动中可以通过使用这个名称来访问它。变量的值是一个 Java 对象。
流程变量的作用域可以是一个流程实例(processInstance)
,或一个任务(task)
,或一个执行实例 (execution)
运行时变量
流程实例运行时的变量,存入act_ru_variable表中。在流程实例运行结束时,此实例的变量在表中删除。在流程实例创建及启动时,可设置流程变量。所有的 startProcessInstanceXXX 方法都有一个可选参数用于设置变量。
例如, RuntimeService 中
ProcessInstance startProcessInstanceById(String processDefinitionId,
Map<String,Object> variables);
也可以在流程执行中加入变量。例如,( RuntimeService ):
// 加入变量
void setVariable(String executionId, String variableName, Object value);
void setVariableLocal(String executionId, String variableName, Object value);
void setVariables(String executionId, Map<String, ? extends Object> variables);
void setVariablesLocal(String executionId, Map<String, ? extends Object> variables);
// 读取变量方法:
Map<String, Object> getVariables(String executionId);
Map<String, Object> getVariablesLocal(String executionId);
Map<String, Object> getVariables(String executionId, Collection<String> variableNames);
Map<String, Object> getVariablesLocal(String executionId, Collection<String> variableNames);
Object getVariable(String executionId, String variableName);
<T> T getVariable(String executionId, String variableName, Class<T> variableClass);
注意:由于流程实例结束时,对应在运行时表的数据跟着被删除。所以查询一个已经完结流程实例的变量,只能在历史变量表中查找。
当然运行时变量我们也可以根据对应的作用域把他分为 全局变量
和 局部变量
globa变量
流程变量的默认作用域是流程实例。当一个流程变量的作用域为流程实例时,可以称为 global 变量
注意:
如: Global变量:userId(变量名):zhangsan(变量值)
global 变量中变量名不允许重复,设置相同名称的变量,后设置的值会覆盖前设置的变量值。
示例
绘制流程图
自定义监听器
@Slf4j
public class MyTaskListener01 implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
System.out.println("myTaskListener-01 ");
Map<String, Object> variables = delegateTask.getVariables();
for (String key : variables.keySet()) {
log.info("key: {}, value: {}", key, variables.get(key));
// 覆盖原来的流程变量的值
delegateTask.setVariable(key, variables.get(key) + "-ojbk");
}
// 设置流程变量
delegateTask.setVariable("haha", "aiyo");
}
}
部署流程图
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-acti.xml")
public class Test01 {
@Autowired
private RepositoryService repositoryService;
@Test
public void test01() {
Deployment deploy = repositoryService
.createDeployment()
.addClasspathResource("process/holiday-01-name.bpmn20.xml")
.deploy();
log.info("部署id: {}", deploy.getId());
ProcessDefinition processDefinition = repositoryService
.createProcessDefinitionQuery()
.deploymentId(deploy.getId()).singleResult();
log.info("流程定义id: {}", processDefinition.getId());
}
}
开启流程,并设置变量
开启流程后,此时会执行自定义任务监听器的notify方法,在上面自定义任务监听器中:能够得到此处传入的流程变量,并且能够设置流程变量
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-acti.xml")
public class Test01 {
@Autowired
private RuntimeService runtimeService;
@Test
public void test02() {
HashMap<String, Object> variables = new HashMap<>();
variables.put("k1", "v1");
variables.put("assignee1", "a1111");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holiday-01-key", variables);
log.info("流程实例id: {}", processInstance.getId());
}
}
在该流程实例还未结束时,可以访问设置的流程变量
这些变量保存在act_ru_variable表中
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-acti.xml")
public class Test01 {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Test
public void test03() {
// 先获取任务
Task task = taskService.createTaskQuery().processInstanceId("2501").singleResult();
// 再从任务上获取执行实例id
String executionId = task.getExecutionId();
// 这里需要传入的是执行实例id
Map<String, Object> variables = runtimeService.getVariables(executionId);
log.info("变量: {}", JSON.toJSONString(variables));
}
}
在流程未结束前,这些设置的变量在流程的每个节点都可以访问到,但是流程实例结束后,表act_ru_execution表中相关数据就会被删除,同时act_ru_variable表中的相关数据也会被删除掉。因此,当流程实例结束后,只能通过历史查看流程变量
local变量
任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大, 称为 local 变量。
Local 变量由于在不同的任务或不同的执行实例中,作用域互不影响,变量名可以相同没有影响。Local 变量名也可以和 global 变量名相同,没有影响。
示例
我们通过RuntimeService 设置的Local变量绑定的是 executionId。在该流程中有效
@Test
public void test4(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 待办查询 执行中的任务处理通过 TaskService来实现
TaskService taskService = engine.getTaskService();
RuntimeService runtimeService = engine.getRuntimeService();
// 这里的第1个参数是executionId
runtimeService.setVariableLocal("60004","orderId","100001");
runtimeService.setVariableLocal("60004","price",6666);
}
我们还可以通过TaskService来绑定本地流程变量。需要指定对应的taskId
@Test
public void test41(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 待办查询 执行中的任务处理通过 TaskService来实现
TaskService taskService = engine.getTaskService();
// 这里的第1个参数是taskId
taskService.setVariableLocal("60007","wechat","boge3306");
}
然后通过测试演示我们可以看到通过TaskService绑定的Local变量的作用域只是在当前的Task有效(意思就是:当调用taskService.complete(taskId)方法后,前面通过taskService.setVariableLocal(taskId,…)设置的local流程变量会从act_ru_variable表中移除掉)。而通过RuntimeService绑定的Local变量作用的访问是executionId(意思就是:当调用runTimeService.complete(taskId)方法后,前面通过runtimeService.setVariableLocal(executionId,…)设置的local流程变量仍然保存在act_ru_variable表中,流转到下一个节点仍然可以使用)。
/**
* 读取本地流程变量
*/
@Test
public void testLocalVal(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 读取RuntimeService设置的局部变量
RuntimeService runtimeService = engine.getRuntimeService();
Map<String, Object> variablesLocal = runtimeService.getVariablesLocal("60004");
System.out.println(variablesLocal);
// 读取TaskService设置的局部变量
TaskService taskService = engine.getTaskService();
Map<String, Object> variablesLocal1 = taskService.getVariablesLocal("60007");
System.out.println(variablesLocal1);
}
executionId和processInstanceId的区别
需要注意:executionId和processInstanceId的区别
简单理解:在Activiti中,executionId和processInstanceId是两个不同的概念。executionId是执行实例的ID,代表流程中的一个执行路径,而processInstanceId是流程实例的ID,代表整个流程实例。当流程中没有分支时,executionId等同于processInstanceId,甚至连ID也相同。但是当流程中有分支时,executionId和processInstanceId就不相同了,因为一个流程实例可能会有多个执行路径。因此,executionId和processInstanceId的区别在于,executionId代表流程中的一个执行路径,而processInstanceId代表整个流程实例。
工作流-流程实例【ProcessInstance】与执行实例【Execution】
历史变量
历史变量,存入 act_hi_varinst
表中。在流程启动时,流程变量会同时存入历史变量表中;在流程结束时,历史表中的变量仍然存在。可理解为“永久代”的流程变量。
获取已完成的、id为’XXX’的流程实例中,所有的HistoricVariableInstances (历史变量实例),并以变量名排序。
historyService.createHistoricVariableInstanceQuery()
.processInstanceId("XXX")
.orderByVariableName.desc()
.list();
3.4、流程变量的使用方法
在属性上使用UEL表达式
可以在 assignee 处设置 UEL 表达式,表达式的值为任务的负责人,比如: ${assignee}
, assignee 就是一个流程变量名称。
Activiti获取UEL表达式的值,即流程变量assignee的值 ,将assignee的值作为任务的负责人进行任务分配
在连线上使用UEL表达式
可以在连线上设置UEL表达式,决定流程走向。
比如:${price<10000}
。price就是一个流程变量名称,uel表达式结果类型为布尔类型。
如果UEL表达式是true,要决定 流程执行走向。
3.5流程变量使用
3.5.1需求
员工创建出差申请单,由部门经理审核,部门经理申请通过后3天以下由财务直接申批,3天以上先由 总经理审批,总经理审批通过后再由财务审批。
3.5.2 流程定义
先通过UEL-value来设置负责人
然后在分支线上来设置条件(点击这个连线,在condition中写上条件${num>=3}
)
那么还可以通过对象参数命名,比如 ${evection.num>=3}
:
另一根线对应的设置 ${num<3}
或如下图设置为: ${evection.num<3}
然后可以将相关的资源文件拷贝到项目中,
3.5.3 使用Global变量
接下来使用Global变量控制流程
3.5.3.1 POJO创建
首先创建POJO对象(需要实现Serializable接口,最好在加上序列号版本)
/**
* 出差申请的POJO对象
*/
@Data
public class Evection implements Serializable {
private long id;
private String evectionName;
/**
* 出差的天数
*/
private double num;
private Date beginDate;
private Date endDate;
private String destination;
private String reson;
}
3.5.3.2 流程的部署
/**
* 部署流程
*/
@Test
public void test01() {
// 1.获取ProcessEngine对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取RepositoryService进行部署操作
RepositoryService service = engine.getRepositoryService();
// 3.使用RepositoryService进行部署操作
Deployment deploy = service.createDeployment()
.addClasspathResource("bpmn/evection-variable.bpmn") // 添加bpmn资源
.addClasspathResource("bpmn/evection-variable.png") // 添加png资源
.name("出差申请流程-流程变量")
.deploy();// 部署流程
// 4.输出流程部署的信息
System.out.println("流程部署的id:" + deploy.getId());
System.out.println("流程部署的名称:" + deploy.getName());
}
3.5.3.3 设置流程变量
a.启动时设置流程变量
在启动流程时设置流程变量,变量的作用域是整个流程实例
。
/**
* 启动流程实例,设置流程变量
*/
@Test
public void test02() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = engine.getRuntimeService();
// 流程定义key
String key = "evection-variable";
// 创建变量集合
Map<String, Object> variables = new HashMap<>();
// 创建出差对象 POJO
Evection evection = new Evection();
// 设置出差天数
evection.setNum(4d);
// 定义流程变量到集合中(evection这个key需要和流程变量里的uel表达式一致)
variables.put("evection", evection);
// 设置assignee的取值(设置userTask中的流程变量对应的值)
variables.put("assignee0", "张三1");
variables.put("assignee1", "李四1");
variables.put("assignee2", "王五1");
variables.put("assignee3", "赵财务1");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, variables);
// 输出信息
System.out.println("获取流程实例名称:" + processInstance.getName());
System.out.println("流程定义ID:" + processInstance.getProcessDefinitionId());
}
完成任务
/**
* 完成任务
*/
@Test
public void test03() {
String key = "evection-variable";
String assignee = "李四1";
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
Task task = taskService.createTaskQuery()
.processDefinitionKey(key)
.taskAssignee(assignee)
.singleResult();
if (task != null) {
// 因为上面设置的是4天, 所以流程就会从经理审批到总经理审批节点, 再到财务审批
// (如果上面设置的是2天, 流程就会从经理审批直接到财务审批)
taskService.complete(task.getId());
System.out.println("任务执行完成...");
}
}
通过startProcessInstanceByKey方法设置流程变量的作用域是一个流程实例,流程变量使用Map存储, 同一个流程实例map中的key相同,后者会覆盖前者
b.任务办理时设置
在完成任务时设置流程变量,该流程变量只有在该任务完成后其它结点才可使用该变量,它的作用域 是整个流程实例
,如果设置的流程变量的key在流程实例中已存在相同的名字,则后设置的变量 替换 前边设置 的变量
。
这里需要在创建出差单任务完成时设置流程变量
/**
* 启动流程实例,设置流程变量
*/
@Test
public void test02() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = engine.getRuntimeService();
// 流程定义key
String key = "evection-variable";
// 创建变量集合
Map<String, Object> variables = new HashMap<>();
// 设置assignee的取值
variables.put("assignee0", "张三1");
variables.put("assignee1", "李四1");
variables.put("assignee2", "王五1");
variables.put("assignee3", "赵财务1");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(key, variables);
// 输出信息
System.out.println("获取流程实例名称:" + processInstance.getName());
System.out.println("流程定义ID:" + processInstance.getProcessDefinitionId());
}
/**
* 完成任务
*/
@Test
public void test03() {
String key = "evection-variable";
String assignee = "李四1";
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
Task task = taskService.createTaskQuery()
.processDefinitionKey(key)
.taskAssignee(assignee)
.singleResult();
Map<String, Object> variables = new HashMap<>();
// 创建出差对象 POJO
Evection evection = new Evection();
// 设置出差天数
evection.setNum(4d);
// 定义流程变量到集合中
variables.put("evection", evection);
if (task != null) {
taskService.complete(task.getId(), variables);
System.out.println("任务执行完成...");
}
}
说明:
通过当前任务设置流程变量,需要指定当前任务id,如果当前执行的任务id不存在则抛出异常。 任务办理时也是通过map<key,value>设置流程变量,一次可以设置多个变量
。
c.当前流程实例设置
通过 流程实例id设置全局变量
,该流程实例必须未执行完成
。
@Test
public void setGlobalVariableByExecutionId() {
// 当前流程实例执行 id,通常设置为当前执行的流程实例(act_ru_task表中的EXECUTION_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)
}
注意:
executionId必须是当前 未结束的 流程实例的 执行id
,通常此id设置流程实例 的id。也可以通runtimeService.getVariable()获取流程变量。
d.当前任务设置
@Test
public void setGlobalVariableByTaskId() {
//当前待办任务id
String taskId = "1404";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
Evection evection = new Evection();
evection.setNum(3);
//通过任务设置流程变量
taskService.setVariable(taskId, "evection", evection);
//一次设置多个值
//taskService.setVariables(taskId, variables)
}
注意:
任务id必须是当前待办任务id,act_ru_task中存在。如果该任务已结束,会报错 也可以通过taskService.getVariable()获取流程变量。
3.5.4设置local流程变量
3.5.4.1、任务办理时设置
任务办理时设置local流程变量,当前运行的流程实例只能在该任务结束前使用,任务结束该变量无法在 当前流程实例使用,可以通过查询历史任务查询。
/*
*处理任务时设置local流程变量
*/
@Test
public void completTask() {
// 任务id
String taskId = "1404";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
// 定义流程变量
Map<String, Object> variables = new HashMap<String, Object>();
Evection evection = new Evection();
evection.setNum(3d);
// 定义流程变量
Map<String, Object> variables = new HashMap<String, Object>();
// 变量名是holiday,变量值是holiday对象
variables.put("evection", evection);
// 设置local变量,作用域为该任务
taskService.setVariablesLocal(taskId, variables);
// 完成任务
taskService.complete(taskId);
}
说明:
设置作用域为任务的local变量,每个任务可以设置同名的变量,互不影响。
3.5.4.2、通过当前任务设置
@Test
public void setLocalVariableByTaskId() {
// 当前待办任务id
String taskId = "1404";
// 获取processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
Evection evection = new Evection();
evection.setNum(3d);
// 通过任务设置流程变量
taskService.setVariableLocal(taskId, "evection", evection);
// 一次设置多个值
//taskService.setVariablesLocal(taskId, variables)
}
注意:
任务id必须是当前待办任务id,act_ru_task中存在。
3.5.4.3、 Local变量测试1
如果上边例子中设置global变量改为设置local变量是否可行?为什么?
Local变量在任务结束后无法在当前流程实例执行中使用,如果后续的流程执行需要用到此变量则会报 错。
3.5.4.4、 Local变量测试2
在部门经理审核、总经理审核、财务审核时设置local变量,可通过historyService查询每个历史任务时 将流程变量的值也查询出来。
代码如下:
// 创建历史任务查询对象
HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
// 查询结果包括 local变量
historicTaskInstanceQuery.includeTaskLocalVariables();
for (HistoricTaskInstance historicTaskInstance : list) {
System.out.println("==============================");
System.out.println(" 任 务 id:" + historicTaskInstance.getId());
System.out.println(" 任 务 名 称 :" + historicTaskInstance.getName());
System.out.println("任务负责人:" + historicTaskInstance.getAssignee());
System.out.println("任务local变量:" + historicTaskInstance.getTaskLocalVariables());
}
注意:查询历史流程变量,特别是查询pojo变量需要经过反序列化,不推荐使用。
4. 身份服务
在流程定义中在任务结点的 assignee 固定设置任务负责人,在流程定义时将参与者固定设置在.bpmn 文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性差。
针对这种情况可以给任务设置多个候选人
或者候选人组
,可以从候选人中选择参与者来完成任务
审批人
前面案例中直接指派审批的用户的处理,即直接指定的assignee
候选人
一个审批节点可能有多个人同时具有审批的权限。这时我们就可以通过候选人来处理。
绘制流程图
人事审批中我们设置多个候选人来处理,分别是 张三 , 李四 , 王五
在总经理的位置我们统一设置几个候选人来审批
创建的对应的流程图的xml文件中内容如下:
部署和启动流程
流程图设计好后我们就可以部署流程和启动流程实例了。
部署流程
/**
* 流程部署操作
*/
@Test
public void test1(){
// 1.获取ProcessEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2.完成流程的部署操作 需要通过RepositoryService来完成
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3.完成部署操作
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("flow/test5.bpmn20.xml")
.name("候选人")
.deploy(); // 是一个流程部署的行为 可以部署多个流程定义的
System.out.println(deploy.getId());
System.out.println(deploy.getName());
}
启动流程
/**
* 发起一个流程
*/
@Test
public void test3(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 发起流程 需要通过 runtimeService来实现
RuntimeService runtimeService = engine.getRuntimeService();
// 通过流程定义ID来启动流程 返回的是流程实例对象
ProcessInstance processInstance = runtimeService.startProcessInstanceById("holiday1:2:90003");
System.out.println("processInstance.getId() = " + processInstance.getId());
System.out.println("processInstance.getDeploymentId() = " + processInstance.getDeploymentId());
System.out.println("processInstance.getDescription() = " + processInstance.getDescription());
}
启动流程实例后。在 act_ru_task 中的审批人是空的,
但是在对应的 act_ru_identitylink 中我们可以看到对应的候选人信息
任务的拾取
候选要操作我们需要通过 拾取
的行为把 候选人
转换为 处理人
.那么候选人登录后需要能查询出来他可以 拾取 的任务。
/**
* 候选人 审批任务查询
* 张三 登录OA系统
*/
@Test
public void test4(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery()
.taskCandidateUser("张三") // 根据候选人查询审批任务
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
System.out.println("task.getId() = " + task.getId());
//taskService.complete(task.getId());
}
}
}
一个任务如果被拾取后。其他的候选人就查询不到该任务信息了
/**
* 待办任务的 拾取 操作
* 从候选人 --> 处理人
* 一个任务如果被拾取后。其他的候选人就查询不到该任务信息了
* (注意:拾取操作影响的就只是任务的assignee负责人,之后,以该assignee负责人作为任务的查询条件能够查询到该任务)
*/
@Test
public void test5(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery()
.taskCandidateUser("王五") // 根据候选人查询审批任务
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
// 李四 拾取了 这个任务的审批权限 --> 变成了这个任务的审批人
taskService.claim(task.getId(),"王五");
}
}
}
任务的归还
拾取任务后如果不想操作那么可以归还任务
/**
* 归还:拾取的用户 不审批了。就放弃审批人的操作
* 其他的候选人可以重新拾取人了
*/
@Test
public void test6(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery()
.taskCandidateOrAssigned("李四") // 根据 审批人或者 候选人来查询待办任务
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
// System.out.println("task.getId() = " + task.getId());
// 归还操作的本质其实就是设置审批人为空
taskService.unclaim(task.getId());
}
}
}
候选人组
当候选人很多的情况下,我们可以分组来处理。先创建组,然后把用户分配到这个组中
绘制流程图
然后在设置审批人的时候通过候选人组来设定
对应的流程图xml中的定义信息
部署和启动流程
部署流程
/**
* 流程部署操作
*/
@Test
public void test1(){
// 1.获取ProcessEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2.完成流程的部署操作 需要通过RepositoryService来完成
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3.完成部署操作
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("flow/test6.bpmn20.xml")
.name("候选人组")
.deploy(); // 是一个流程部署的行为 可以部署多个流程定义的
System.out.println(deploy.getId());
System.out.println(deploy.getName());
}
启动流程
/**
* 发起一个流程
*/
@Test
public void test3(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 发起流程 需要通过 runtimeService来实现
RuntimeService runtimeService = engine.getRuntimeService();
// 通过流程定义ID来启动流程 返回的是流程实例对象
ProcessInstance processInstance = runtimeService
.startProcessInstanceById("holiday1:3:97503");
System.out.println("processInstance.getId() = " + processInstance.getId());
System.out.println("processInstance.getDeploymentId() = " + processInstance.getDeploymentId());
System.out.println("processInstance.getDescription() = " + processInstance.getDescription());
}
任务的拾取
先查询当前用户所属组的可拾取(还没被组内的其它人拾取)的任务
/**
* 候选人组:
* 具体的用户。比如 张三 登录了系统
* 查询张三对应的 组 根据 组来查询待办的任务
*/
@Test
public void test4(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String group = "销售部"; // 根据当前登录用户查询到的
List<Task> list = taskService.createTaskQuery()
.taskCandidateGroup(group)
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
System.out.println("task.getId() = " + task.getId());
//taskService.complete(task.getId());
}
}
}
拾取任务
/**
* 待办任务的 拾取 操作
* 从候选人 --> 处理人
* 一个任务如果被拾取后。其他的候选人就查询不到改任务信息了
*/
@Test
public void test5(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String group = "销售部"; // 根据当前登录用户查询到的
List<Task> list = taskService.createTaskQuery()
.taskCandidateGroup(group) // 根据组来查询
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
// 张三1 拾取了 这个任务的审批权限 --> 变成了这个任务的审批人
// 但是注意,这里的userId不一定必须是候选人里面去拾取这个任务,可以任意userId去拾取,
// 但是拾取完成之后,不能调用taskService#claim(..)再次拾取, 因为这个任务此时已经有任务负责人了
// 所以,候选人组这个东西只是作为查询条件有用罢了
// 如果一定需要改任务负责人, 可以使用taskService#setAssignee(..)为任务指定对应的负责人,这个只会修改当前指定任务的assignee字段
// 而taskService#delegateTask(..)任务委派除了会修改assignee字段还会把原来的assignee保存到owner字段上(注意,只会在owner初始为null并且调用taskService#delegateTask时仅会修改1次)
taskService.claim(task.getId(),"张三1");
}
}
}
任务的归还
归还:拾取的用户 不审批了。就放弃审批人的操作,其他的候选人可以重新拾取了
@Test
public void test6(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String group = "销售部"; // 根据当前登录用户查询到的
List<Task> list = taskService.createTaskQuery()
.taskAssignee("张三1")
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
// System.out.println("task.getId() = " + task.getId());
// 归还操作的本质其实就是设置审批人为空(即设置act_ru_task表中该任务数据的ASSIGNEE_字段为null)
taskService.unclaim(task.getId());
}
}
}
任务的交接
获取用户审批权限的用户没有时间审批了,但是他也可以不用归还而是做任务的交接。把这个任务让另一个人来审批,另外的这个人就可以审批该任务了
(完成任务交接就是改变该任务的assignee值,这样指定的人使用assingee作为查询条件能够查询道该任务)
@Test
public void test8(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String group = "销售部"; // 根据当前登录用户查询到的
List<Task> list = taskService.createTaskQuery()
.taskAssignee("张三1")
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
// System.out.println("task.getId() = " + task.getId());
// 任务交接
taskService.setAssignee(task.getId(),"李四1");
}
}
}
转签(转交 / 转办)
任务的交接也有叫这个操作为:转签
(也叫转交任务,也叫转办),转签就是将任务的负责人直接设置为别人。即本来由自己办理,改为别人办理。参考自:加签和转签
void setAssignee(String taskId, String userId);
调用此方法会修改:ACT_HI_ACTINST、ACT_RU_TASK、ACT_HI_TASKINST表对应数据中的assignee字段为指定的userId
任务的审批
办理该任务
(即便当前任务没有设置assignee,也是可以调用taskService.complete(taskId)完成任务审批的,这只是从代码角度来看,只是知道这样做并没有报错)
@Test
public void test7(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
taskService.complete("92505");
}
对应的流程中的 候选人组 信息同样记录在 act_ru_identitylink
任务的委派
A由于某些原因不能处理该任务可以把任务委派给用户B代理,当B决绝完之后再次回到用户A这里,然后由A去完成任务。在这个过程中A是任务的所有者OWNER_,B是该任务的办理人Assignee。A->B->A。
加签
任务的委派也叫加签
,加签就是委派任务delegateTask
,然后去解决任务resolveTask
(并不是去真正的去完成任务),解决任务之后,任务回到owner,仍然由owner来完成该任务
任务委派
主要是将act_ru_task表的owner字段(任务的持有人/所有者)改为原来的assignee字段,原来的assignee字段改为了委派指定的人,delegation字段改为了pending状态
/**
* 任务委派
*/
@Test
public void test4_1() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery().taskAssignee("zs").list();
Task task = list.get(0);
taskService.delegateTask(task.getId(), "zzhua");
}
执行的sql,修改了ACT_HI_ACTINST 、ACT_RU_TASK 、ACT_HI_TASKINST
updating: HistoricActivityInstanceEntity[id=2504, activityId=sid-62F29B3A-800B-4143-9A95-EA5899FECDE4, activityName=人事审批]
==> Preparing: update ACT_HI_ACTINST set EXECUTION_ID_ = ?, ASSIGNEE_ = ?, END_TIME_ = ?, DURATION_ = ?, DELETE_REASON_ = ? where ID_ = ?
==> Parameters: 2502(String), zzhua(String), null, null, null, 2504(String)
<== Updates: 1
updating: Task[id=2505, name=人事审批]
==> Preparing: update ACT_RU_TASK SET REV_ = ?, NAME_ = ?, PARENT_TASK_ID_ = ?, PRIORITY_ = ?, CREATE_TIME_ = ?, OWNER_ = ?, ASSIGNEE_ = ?, DELEGATION_ = ?, EXECUTION_ID_ = ?, PROC_DEF_ID_ = ?, DESCRIPTION_ = ?, DUE_DATE_ = ?, CATEGORY_ = ?, SUSPENSION_STATE_ = ?, FORM_KEY_ = ?, CLAIM_TIME_ = ? where ID_= ? and REV_ = ?
==> Parameters: 2(Integer), 人事审批(String), null, 50(Integer), 2023-11-05 17:31:34.646(Timestamp), zs(String), zzhua(String), PENDING(String), 2502(String), holiday-01-key:1:3(String), null, null, null, 1(Integer), null, null, 2505(String), 1(Integer)
<== Updates: 1
updating: org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntityImpl@4cee7fa0
==> Preparing: update ACT_HI_TASKINST set PROC_DEF_ID_ = ?, EXECUTION_ID_ = ?, NAME_ = ?, PARENT_TASK_ID_ = ?, DESCRIPTION_ = ?, OWNER_ = ?, ASSIGNEE_ = ?, CLAIM_TIME_ = ?, END_TIME_ = ?, DURATION_ = ?, DELETE_REASON_ = ?, TASK_DEF_KEY_ = ?, FORM_KEY_ = ?, PRIORITY_ = ?, DUE_DATE_ = ?, CATEGORY_ = ? where ID_ = ?
==> Parameters: holiday-01-key:1:3(String), 2502(String), 人事审批(String), null, null, null, zzhua(String), null, null, null, null, sid-62F29B3A-800B-4143-9A95-EA5899FECDE4(String), null, 50(Integer), null, null, 2505(String)
<== Updates: 1
插入的sql,插入了表:ACT_HI_IDENTITYLINK 、ACT_RU_IDENTITYLINK
==> Preparing: insert into ACT_HI_IDENTITYLINK (ID_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_) values (?, ?, ?, ?, ?, ?)
==> Parameters: 5001(String), participant(String), zzhua(String), null, null, 2501(String)
<== Updates: 1
inserting: IdentityLinkEntity[id=5001, type=participant, userId=zzhua, processInstanceId=2501]
==> Preparing: insert into ACT_RU_IDENTITYLINK (ID_, REV_, TYPE_, USER_ID_, GROUP_ID_, TASK_ID_, PROC_INST_ID_, PROC_DEF_ID_) values (?, 1, ?, ?, ?, ?, ?, ?)
==> Parameters: 5001(String), participant(String), zzhua(String), null, null, 2501(String), null
<== Updates: 1
被委派人处理任务
代理人在完成任务之前需要先resolveTask。(如果在1个任务的delegating为pending的状态下,去完成这个任务,会报错:A delegated task cannot be completed, but should be resolved instead.)。
resolveTask做的:主要是将act_ru_task的Assignee_设置为原来的owner_,将DELEGATION_改为RESOLVED解决委托。之后,就可以complete这个任务了
/**
* resolveTask
*/
@Test
public void test4_2() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery().taskAssignee("zzhua").list();
Task task = list.get(0);
taskService.resolveTask(task.getId());
}
执行的sql
修改了表:ACT_HI_ACTINST 、ACT_HI_TASKINST 、ACT_RU_TASK
updating: HistoricActivityInstanceEntity[id=2504, activityId=sid-62F29B3A-800B-4143-9A95-EA5899FECDE4, activityName=人事审批]
==> Preparing: update ACT_HI_ACTINST set EXECUTION_ID_ = ?, ASSIGNEE_ = ?, END_TIME_ = ?, DURATION_ = ?, DELETE_REASON_ = ? where ID_ = ?
==> Parameters: 2502(String), zs(String), null, null, null, 2504(String)
<== Updates: 1
updating: org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntityImpl@36c281ed
==> Preparing: update ACT_HI_TASKINST set PROC_DEF_ID_ = ?, EXECUTION_ID_ = ?, NAME_ = ?, PARENT_TASK_ID_ = ?, DESCRIPTION_ = ?, OWNER_ = ?, ASSIGNEE_ = ?, CLAIM_TIME_ = ?, END_TIME_ = ?, DURATION_ = ?, DELETE_REASON_ = ?, TASK_DEF_KEY_ = ?, FORM_KEY_ = ?, PRIORITY_ = ?, DUE_DATE_ = ?, CATEGORY_ = ? where ID_ = ?
==> Parameters: holiday-01-key:1:3(String), 2502(String), 人事审批(String), null, null, null, zs(String), null, null, null, null, sid-62F29B3A-800B-4143-9A95-EA5899FECDE4(String), null, 50(Integer), null, null, 2505(String)
<== Updates: 1
updating: Task[id=2505, name=人事审批]
==> Preparing: update ACT_RU_TASK SET REV_ = ?, NAME_ = ?, PARENT_TASK_ID_ = ?, PRIORITY_ = ?, CREATE_TIME_ = ?, OWNER_ = ?, ASSIGNEE_ = ?, DELEGATION_ = ?, EXECUTION_ID_ = ?, PROC_DEF_ID_ = ?, DESCRIPTION_ = ?, DUE_DATE_ = ?, CATEGORY_ = ?, SUSPENSION_STATE_ = ?, FORM_KEY_ = ?, CLAIM_TIME_ = ? where ID_= ? and REV_ = ?
==> Parameters: 3(Integer), 人事审批(String), null, 50(Integer), 2023-11-05 17:31:34.646(Timestamp), zs(String), zs(String), RESOLVED(String), 2502(String), holiday-01-key:1:3(String), null, null, null, 1(Integer), null, null, 2505(String), 2(Integer)
<== Updates: 1
审批意见
添加审批意见
我们可以通过TaskService的addComment方法来给任务审批意见
/**
* 添加审批意见
*/
@Test
public void test5_1() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery().taskAssignee("zs").list();
Task task = list.get(0);
log.info("taskId: {}", task.getId());
taskService.addComment(task.getId(), task.getProcessInstanceId(), "添加了个审批意见2");
}
执行的sql
==> Preparing: insert into ACT_HI_COMMENT (ID_, TYPE_, TIME_, USER_ID_, TASK_ID_, PROC_INST_ID_, ACTION_, MESSAGE_, FULL_MSG_) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
==> Parameters: 10001(String), comment(String), 2023-11-05 20:01:14.613(Timestamp), null, 2505(String), 2501(String), AddComment(String), 添加了个审批意见2(String), java.io.ByteArrayInputStream@89c65d5(ByteArrayInputStream)
<== Updates: 1
查询审批意见
我们可以通过TaskService的addComment方法来给任务审批意见
/**
* 查询审批意见
*/
@Test
public void test5_2() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery().taskAssignee("zs").list();
Task task = list.get(0);
log.info("taskId: {}", task.getId());
List<Comment> taskComments = taskService.getTaskComments(task.getId());
for (Comment taskComment : taskComments) {
log.info("审批意见id: {}", taskComment.getId());
log.info("审批意见任务id: {}", taskComment.getTaskId());
log.info("审批意见流程实例id: {}", taskComment.getProcessInstanceId());
log.info("审批意见time: {}", DateUtils.fmtDate(taskComment.getTime()));
log.info("审批意见type: {}", taskComment.getType());
log.info("审批意见userId: {}", taskComment.getUserId());
log.info("审批意见内容: {}", taskComment.getFullMessage());
log.info("----------------------------");
}
}
执行结果
审批意见id: 10001
审批意见任务id: 2505
审批意见流程实例id: 2501
审批意见time: 2023-11-05 20:01:14
审批意见type: comment
审批意见userId: null
审批意见内容: 添加了个审批意见2
----------------------------
审批意见id: 7501
审批意见任务id: 2505
审批意见流程实例id: 2501
审批意见time: 2023-11-05 19:54:02
审批意见type: comment
审批意见userId: null
审批意见内容: 添加了个审批意见
----------------------------
执行的sql
查询的是:ACT_HI_COMMENT表,注意看这条sql默认类型type就取得是"comment"
==> Preparing: select * from ACT_HI_COMMENT where TASK_ID_ = ? and TYPE_ = 'comment' order by TIME_ desc
==> Parameters: 2505(String)
<== Total: 2
也可以使用historyService去查询,跟上面的查询sql是一样的
/**
* 查询审批意见
*/
@Test
public void test5_3() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
HistoryService historyService = engine.getHistoryService();
TaskService taskService = engine.getTaskService();
List<HistoricTaskInstance> historicTaskInstanceList = historyService.createHistoricTaskInstanceQuery()
.processInstanceId("2501")
.list();
for (HistoricTaskInstance historicTaskInstance : historicTaskInstanceList) {
log.info("任务id: {}", historicTaskInstance.getId());
taskService.getTaskComments(historicTaskInstance.getId());
List<Comment> taskComments = taskService.getTaskComments(historicTaskInstance.getId());
for (Comment taskComment : taskComments) {
log.info("审批意见id: {}", taskComment.getId());
log.info("审批意见任务id: {}", taskComment.getTaskId());
log.info("审批意见流程实例id: {}", taskComment.getProcessInstanceId());
log.info("审批意见time: {}", DateUtils.fmtDate(taskComment.getTime()));
log.info("审批意见type: {}", taskComment.getType());
log.info("审批意见userId: {}", taskComment.getUserId());
log.info("审批意见内容: {}", taskComment.getFullMessage());
log.info("========================");
}
log.info("----------------------------");
}
}
5.网关
网关
可控制流程的执行流向
,常用于拆分或合并复杂的流程场景。
在Activiti7中,有以下几种类型的网关:
排他网关
(Exclusive Gateway):用于在流程中进行条件判断,根据不同的条件选择不同的分支路径。只有满足条件的分支会被执行,其他分支会被忽略。并行网关
(Parallel Gateway):用于将流程分成多个并行的分支,这些分支可以同时执行。当所有分支都执行完毕后,流程会继续向下执行。包容网关
(Inclusive Gateway):用于根据多个条件的组合情况选择分支路径。可以选择满足任意一个条件的分支执行,或者选择满足所有条件的分支执行。事件网关
(Event Gateway):用于根据事件的触发选择分支路径。当指定的事件触发时,流程会选择对应的分支执行。
这些网关可以根据实际需求灵活地组合使用,以实现不同的流程控制逻辑。Activiti7提供了直观的图形化界面,用户可以通过拖拽和连接网关来定义流程的分支和合并。同时,Activiti7还提供了丰富的API和扩展点,方便开发人员进行二次开发和定制。接下来我们分别介绍下各种网关的应用。
1. 排他网关
排他网关(exclusive gateway)(也叫异或网关 XOR gateway,或者更专业的,基于数据的排他网关exclusive data-based gateway),用于对流程中的决策建模。当执行到达这个网关时,会按照所有出口顺序流定义的顺序对它们进行计算。选择第一个条件计算为true的顺序流(当没有设置条件时,会认为该顺序流为true
)继续流程。
排他网关用内部带有’X’图标的标准网关(菱形)表示,'X’图标代表异或 的含义。请注意内部没有图标的网关默认为排他网关。BPMN 2.0规范不允许在同一个流程中混合使用有及没有X的菱形标志。
示例
绘制流程图
bpmn文件(注意在排它网关出口的2条分支上都设置了条件)
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/processdef">
<process id="gateway1" name="gateway1" isExecutable="true">
<documentation>gateway1</documentation>
<startEvent id="startEvent1"></startEvent>
<userTask id="sid-A0232A1C-F3EC-49AE-8A62-3366B45F4C3C" name="创建请假单" activiti:assignee="张三">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-92E862F7-1A33-4D26-A6E8-AA1437251E4C" sourceRef="startEvent1" targetRef="sid-A0232A1C-F3EC-49AE-8A62-3366B45F4C3C"></sequenceFlow>
<exclusiveGateway id="sid-9D11C4AF-5B82-4B36-9585-3887720148BC"></exclusiveGateway>
<sequenceFlow id="sid-2DB3C7BE-7E57-4E9A-AF75-B521CF3EBFCD" sourceRef="sid-A0232A1C-F3EC-49AE-8A62-3366B45F4C3C" targetRef="sid-9D11C4AF-5B82-4B36-9585-3887720148BC"></sequenceFlow>
<userTask id="sid-36D2F5F7-9AFF-4566-9B00-791239F8F06C" name="部门经理审批" activiti:assignee="李四">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<userTask id="sid-35FF0A14-DA92-4C74-8AD9-F9E94022DD06" name="总经理审批" activiti:assignee="王五">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<userTask id="sid-1AD5AC78-1DA1-48B9-B1BD-53F5A4EE8FEC" name="人事审批" activiti:assignee="赵六">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-E9C63888-F068-46B0-9D5E-DAFB280962DF" sourceRef="sid-36D2F5F7-9AFF-4566-9B00-791239F8F06C" targetRef="sid-1AD5AC78-1DA1-48B9-B1BD-53F5A4EE8FEC"></sequenceFlow>
<sequenceFlow id="sid-D7F481A0-D014-4846-A40B-E7AAC5B329F9" sourceRef="sid-35FF0A14-DA92-4C74-8AD9-F9E94022DD06" targetRef="sid-1AD5AC78-1DA1-48B9-B1BD-53F5A4EE8FEC"></sequenceFlow>
<endEvent id="sid-C52E5977-2BEB-4481-8C8B-127FE0D28175"></endEvent>
<sequenceFlow id="sid-6E900BE1-B417-4D07-B473-B777F3D88A67" sourceRef="sid-1AD5AC78-1DA1-48B9-B1BD-53F5A4EE8FEC" targetRef="sid-C52E5977-2BEB-4481-8C8B-127FE0D28175"></sequenceFlow>
<sequenceFlow id="sid-D95A77C9-DAE6-4659-A5A2-5D5A1EAF625C" name="请假天数大于等于3" sourceRef="sid-9D11C4AF-5B82-4B36-9585-3887720148BC" targetRef="sid-35FF0A14-DA92-4C74-8AD9-F9E94022DD06">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${days>=3}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-66691670-6D21-4E7E-805B-1AEBC8793E1A" name="请假天数小于3" sourceRef="sid-9D11C4AF-5B82-4B36-9585-3887720148BC" targetRef="sid-36D2F5F7-9AFF-4566-9B00-791239F8F06C">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${days<3}]]></conditionExpression>
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_gateway1">
...
</bpmndi:BPMNDiagram>
</definitions>
示例代码
当days这个流程变量指定不同的值时,在经过排它网关时,会根据条件走不同的分支
public class Activiti7Test09 {
/**
* 流程部署操作
*/
@Test
public void test1(){
// 1.获取ProcessEngine对象(注意配置文件默认的名字必须是activiti.cfg.xml)
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2.完成流程的部署操作 需要通过RepositoryService来完成
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3.完成部署操作
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("flow/gateway1.bpmn20.xml")
.name("排他网关")
.deploy(); // 是一个流程部署的行为 可以部署多个流程定义的
System.out.println(deploy.getId());
System.out.println(deploy.getName());
}
/**
* 发起一个流程
*/
@Test
public void test3(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 发起流程 需要通过 runtimeService来实现
RuntimeService runtimeService = engine.getRuntimeService();
// 通过流程定义ID来启动流程 返回的是流程实例对象
ProcessInstance processInstance = runtimeService
.startProcessInstanceById("gateway1:1:3");
System.out.println("processInstance.getId() = " + processInstance.getId());
System.out.println("processInstance.getDeploymentId() = " + processInstance.getDeploymentId());
System.out.println("processInstance.getDescription() = " + processInstance.getDescription());
}
/**
* 任务审批
*/
@Test
public void test4(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery().taskAssignee("张三").list();
Map<String,Object> map = new HashMap<>();
// 绑定对应的请假天数
map.put("days",1);
for (Task task : list) {
taskService.complete(task.getId(),map);
}
}
2. 并行网关
并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:
- fork分支:并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
- join汇聚: 所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。
注意:
-
如果同一个并行网关有多个进入和多个外出顺序流, 它就同时具有分支和汇聚功能。 这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支
-
与其他网关的主要区别是,
并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略
。
示例
绘制流程图
bpmn文件
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/processdef">
<process id="gateway2" name="gateway2" isExecutable="true">
<documentation>gateway2</documentation>
<startEvent id="startEvent1"></startEvent>
<userTask id="sid-94920C62-C90A-4A71-B77B-83CB92CD3385" name="创建请假单" activiti:assignee="s1">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-3C2ACAB7-CE47-4B5A-BD45-F4C9928C2EEC" sourceRef="startEvent1" targetRef="sid-94920C62-C90A-4A71-B77B-83CB92CD3385"></sequenceFlow>
<sequenceFlow id="sid-DB80C996-DEB2-4A7A-B9B3-33091BAE1C26" sourceRef="sid-94920C62-C90A-4A71-B77B-83CB92CD3385" targetRef="sid-03B39962-B25F-4F04-85DD-42216D6CEAF9"></sequenceFlow>
<parallelGateway id="sid-03B39962-B25F-4F04-85DD-42216D6CEAF9"></parallelGateway>
<userTask id="sid-A8DEF702-7766-4E53-86C1-595DCBF11F82" name="技术经理审批" activiti:assignee="s2">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-CD6D24D6-F5BE-410A-9C6A-AC3CE8CFA719" sourceRef="sid-03B39962-B25F-4F04-85DD-42216D6CEAF9" targetRef="sid-A8DEF702-7766-4E53-86C1-595DCBF11F82"></sequenceFlow>
<userTask id="sid-77CD3829-BB89-425A-9F19-5EE2BF0DFA11" name="项目经理审批" activiti:assignee="s3">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-05B794C7-C80C-4B45-9FB5-8954112DB254" sourceRef="sid-03B39962-B25F-4F04-85DD-42216D6CEAF9" targetRef="sid-77CD3829-BB89-425A-9F19-5EE2BF0DFA11"></sequenceFlow>
<userTask id="sid-CB48DD0C-9272-47C3-BD6D-DC00F79B04BE" name="人事经理审批" activiti:assignee="s4">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-3A24203B-7965-47B9-B17E-00ABC4DC2211" sourceRef="sid-03B39962-B25F-4F04-85DD-42216D6CEAF9" targetRef="sid-CB48DD0C-9272-47C3-BD6D-DC00F79B04BE"></sequenceFlow>
<sequenceFlow id="sid-422FA1F1-793E-4BEE-8D00-DE53E87DEFBD" sourceRef="sid-77CD3829-BB89-425A-9F19-5EE2BF0DFA11" targetRef="sid-B2F885CA-BFE3-44D9-AC46-FB0910F93153"></sequenceFlow>
<parallelGateway id="sid-B2F885CA-BFE3-44D9-AC46-FB0910F93153"></parallelGateway>
<sequenceFlow id="sid-FD38BAEA-AB28-4176-AB52-8985E0966CA5" sourceRef="sid-A8DEF702-7766-4E53-86C1-595DCBF11F82" targetRef="sid-B2F885CA-BFE3-44D9-AC46-FB0910F93153"></sequenceFlow>
<sequenceFlow id="sid-40B377C9-11FA-49F1-89E0-A6974C611E5A" sourceRef="sid-CB48DD0C-9272-47C3-BD6D-DC00F79B04BE" targetRef="sid-B2F885CA-BFE3-44D9-AC46-FB0910F93153"></sequenceFlow>
<userTask id="sid-26F15C51-7582-4DB6-BB92-3582BDE33B48" name="总经理审批" activiti:assignee="boss">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-C49ABC5D-E575-4C7B-B946-ACA80F542286" sourceRef="sid-B2F885CA-BFE3-44D9-AC46-FB0910F93153" targetRef="sid-26F15C51-7582-4DB6-BB92-3582BDE33B48"></sequenceFlow>
<endEvent id="sid-3D529485-A84C-4C1E-91C1-E0E506AE8E1A"></endEvent>
<sequenceFlow id="sid-657FA6C2-B48D-49B8-8FA1-C8FF7ECE5DAA" sourceRef="sid-26F15C51-7582-4DB6-BB92-3582BDE33B48" targetRef="sid-3D529485-A84C-4C1E-91C1-E0E506AE8E1A"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_gateway2">
...
</bpmndi:BPMNDiagram>
</definitions>
示例代码
public class Activiti7Test10 {
/**
* 流程部署操作
*/
@Test
public void test1(){
// 1.获取ProcessEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2.完成流程的部署操作 需要通过RepositoryService来完成
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3.完成部署操作
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("flow/gateway2.bpmn20.xml")
.name("并行网关")
.deploy(); // 是一个流程部署的行为 可以部署多个流程定义的
System.out.println(deploy.getId());
System.out.println(deploy.getName());
}
/**
* 发起一个流程
*/
@Test
public void test3(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 发起流程 需要通过 runtimeService来实现
RuntimeService runtimeService = engine.getRuntimeService();
// 通过流程定义ID来启动流程 返回的是流程实例对象
ProcessInstance processInstance = runtimeServicestartProcessInstanceById("gateway2:1:17503");
System.out.println("processInstance.getId() = " + processInstance.getId());
System.out.println("processInstance.getDeploymentId() = " + processInstance.getDeploymentId());
System.out.println("processInstance.getDescription() = " + processInstance.getDescription());
}
/**
* 任务审批
*/
@Test
public void test4(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
List<Task> list = taskService.createTaskQuery().taskAssignee("boss").list();
for (Task task : list) {
taskService.complete(task.getId());
}
}
}
执行实例
在并行网关中我们需要注意的是 执行实例 的概念。
-
主流程实例:流程启动就会维护的一条记录, ACT_RU_EXECUTION 中 parent_id_ 为null的记录
-
子流程实例:流程的每一步操作。都会更新子流程实例,表示当前流程的执行进度。如果进入的是并行网关。案例中的网关会产生3个子流程实例和一个主流程实例。
(
- 假设整个流程只有一条线路,并且开始和结束之间仅有2个节点,在启动1个流程实例后,act_ru_execution表中会有2条记录,1条主流程实例,1条子流程实例,其中子流程实例的parentIdd指向主流程实例的id,它们同属于同一个流程实例,当完成第1个节点的审批时,子流程实例的rev字段会+1,当完成第2个节点的审批时,任务都完成了,act_ru_execution表会被清空。
- 假设整个流程中的从开始起,经过1个userTask,再经过1个并行网关,并行网关经过3个分支,其中每个分支上都只有1个userTask,然后都汇聚在1个新的并行网关,再经过1个userTask。那么在启动1个流程实例后,act_ru_execution上会有2条记录,1条主流程实例,1条子流程实例,其中子流程实例的parentIdd指向主流程实例的id,它们同属于同一个流程实例,当完成第1个节点的审批时,子流程实例的rev字段会+1,并且在act_ru_execution表中还会生成2个新的子流程实例,生成的这2个子流程实例的id指向主流程实例,并且在act_ru_task表中还会生成2条任务记录,这2条任务记录的execution_id分别指向那2个新的子流程实例的id,此时当前这个流程实例对应了3个任务,此时,当每完成其中1个任务,这个任务就会从act_ru_task表中删除掉,但是act_ru_execution表中的数据不会随之删除掉,当所有的任务都完成了,此时act_ru_execution表中的数据新生成的那2条子流程数据才会删除掉,也就是现在经过并行网关汇聚后,act_ru_execution表有2条数据,1条主流程实例,1条原来的子流程实例
)
生成流程图
传入流程定义id,生成流程定义图
添加依赖
<!-- https://mvnrepository.com/artifact/org.activiti/activiti-image-generator -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
<version>${activiti.version}</version>
</dependency>
**代码**
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
代码示例
@Test
public void testGenerateSVG() throws IOException {
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = processEngine.getRepositoryService();
// 此处传入流程定义id(即表act_re_procdef表的id字段)
BpmnModel bpmnModel = repositoryService.getBpmnModel("gateway3:1:3");
ProcessDiagramGenerator generator = new DefaultProcessDiagramGenerator();
InputStream inputStream = generator.generateDiagram(bpmnModel, "endEvent", "宋体", "宋体");
String imageName = "image-" + Instant.now().getEpochSecond() + ".svg";
FileUtils.copyInputStreamToFile(inputStream, new File("process/" + imageName));
}
生成当前任务节点图
添加依赖
<!-- https://mvnrepository.com/artifact/org.activiti/activiti-image-generator -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-image-generator</artifactId>
<version>${activiti.version}</version>
</dependency>
**代码**
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
代码示例
/**
* 生成流程图
*
* @throws IOException 生成流程图异常!
*/
@Test
public void generateImage() throws IOException {
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
HistoryService historyService = processEngine.getHistoryService();
HistoricTaskInstance historicTaskInstance = historyService
.createHistoricTaskInstanceQuery()
.orderByTaskCreateTime().desc()
.list().get(0);
BpmnModel model = repositoryService.getBpmnModel(historicTaskInstance.getProcessDefinitionId());
ProcessDiagramGenerator generator = new DefaultProcessDiagramGenerator();
//lastTask是当前任务执行到的位置
List<HistoricActivityInstance> lastTasks =
historyService.createHistoricActivityInstanceQuery()
// 指定流程实例id-processInstanceId
.processInstanceId(historicTaskInstance.getProcessInstanceId())
// 按时间降序排序
.orderByHistoricActivityInstanceStartTime()
.desc()
.list();
// 拿到最后1个时间执行的活动实例id
List<String> lastTask = lastTasks.stream()
.map(HistoricActivityInstance::getActivityId)
.limit(1)
.collect(Collectors.toList());
lastTask.add(lastTasks.get(0).getActivityId());
//七个参数分别是:
// BPMNModel
// 高光节点
// 高光顺序流
// 活动字体名称
// 标签字体名称
// 批注字体名称
// 生成默认关系图
// 默认关系图映像文件名
InputStream inputStream = generator.generateDiagram(model, lastTask, Collections.emptyList(), "宋体", "宋体", "宋体", true, "test");
String imageName = "image-" + Instant.now().getEpochSecond() + ".svg";
FileUtils.copyInputStreamToFile(inputStream, new File("process/" + imageName));
}
3. 包含网关
包含网关可以看做是排他网关和并行网关的结合体。 和排他网关一样,你可以在外出顺序流上定义条件
,包含网关会解析它们。 但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样
。
包含网关的功能是基于进入和外出顺序流的:
-
分支: 所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。
-
汇聚:所有并行分支到达包含网关,会进入等待状态, 直到每个包含流程token的进入顺序流的分支都到达。 这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。 在汇聚之后,流程会穿过包含网关继续执行。
示例
绘制流程图
bpmn文件
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/processdef">
<process id="gateway3" name="gateway3" isExecutable="true">
<documentation>gateway3</documentation>
<startEvent id="startEvent1"></startEvent>
<userTask id="sid-1FD155B9-C6D7-4411-BECD-213DAE544F97" name="创建请假单" activiti:assignee="i1">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-6F1C3C71-BA7E-433E-8BB2-31BA69E4743C" sourceRef="startEvent1" targetRef="sid-1FD155B9-C6D7-4411-BECD-213DAE544F97"></sequenceFlow>
<sequenceFlow id="sid-818B3CC8-CE00-4B93-A021-822856FF9D03" sourceRef="sid-1FD155B9-C6D7-4411-BECD-213DAE544F97" targetRef="sid-E6802E81-6FDD-48C9-8D13-2F06400BE23F"></sequenceFlow>
<inclusiveGateway id="sid-E6802E81-6FDD-48C9-8D13-2F06400BE23F"></inclusiveGateway>
<userTask id="sid-EC323C6B-66EB-496B-93B7-31BC374C8588" name="项目经理" activiti:assignee="i2">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<userTask id="sid-D7A182B9-C8B0-4121-A7BE-4F4BB0F65EDB" name="技术总监" activiti:assignee="i4">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<userTask id="sid-BFA52BD2-2A16-42C8-B415-58E10AA902E7" name="人事审批" activiti:assignee="i3">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-89C0A43D-3DDF-4086-8A95-B5564029197D" sourceRef="sid-E6802E81-6FDD-48C9-8D13-2F06400BE23F" targetRef="sid-BFA52BD2-2A16-42C8-B415-58E10AA902E7"></sequenceFlow>
<sequenceFlow id="sid-18249E14-436A-4BD9-9A85-64DC70CA4D1B" sourceRef="sid-BFA52BD2-2A16-42C8-B415-58E10AA902E7" targetRef="sid-8D525F9E-E51C-4347-AAF1-AF8EF98DA64C"></sequenceFlow>
<inclusiveGateway id="sid-8D525F9E-E51C-4347-AAF1-AF8EF98DA64C"></inclusiveGateway>
<sequenceFlow id="sid-DE41B3E5-B17F-4E3A-9998-D69CC615DC71" sourceRef="sid-EC323C6B-66EB-496B-93B7-31BC374C8588" targetRef="sid-8D525F9E-E51C-4347-AAF1-AF8EF98DA64C"></sequenceFlow>
<sequenceFlow id="sid-72FAE487-1B83-4D83-94D3-56477E4886D4" sourceRef="sid-D7A182B9-C8B0-4121-A7BE-4F4BB0F65EDB" targetRef="sid-8D525F9E-E51C-4347-AAF1-AF8EF98DA64C"></sequenceFlow>
<userTask id="sid-16347E53-356A-4C84-9D99-0AFB1ECC2BDB" name="总经理审批" activiti:assignee="boss">
<extensionElements>
<modeler:initiator-can-complete xmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
</extensionElements>
</userTask>
<sequenceFlow id="sid-6F8D537A-2DD5-403B-8242-8E32841AF6D7" sourceRef="sid-8D525F9E-E51C-4347-AAF1-AF8EF98DA64C" targetRef="sid-16347E53-356A-4C84-9D99-0AFB1ECC2BDB"></sequenceFlow>
<endEvent id="sid-C0F6DA98-760D-4254-8B84-387617D37676"></endEvent>
<sequenceFlow id="sid-5CDF5261-54FE-40D0-8916-F8F0D485A702" sourceRef="sid-16347E53-356A-4C84-9D99-0AFB1ECC2BDB" targetRef="sid-C0F6DA98-760D-4254-8B84-387617D37676"></sequenceFlow>
<sequenceFlow id="sid-66A41C2D-7BC2-4A82-B562-D518D435FDEF" name="请假天数大于3" sourceRef="sid-E6802E81-6FDD-48C9-8D13-2F06400BE23F" targetRef="sid-EC323C6B-66EB-496B-93B7-31BC374C8588">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${days>3}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="sid-ECC15393-0D49-4340-B3E2-B4A1E562A261" name="请假天数小于等于3" sourceRef="sid-E6802E81-6FDD-48C9-8D13-2F06400BE23F" targetRef="sid-D7A182B9-C8B0-4121-A7BE-4F4BB0F65EDB">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${days<=3}]]></conditionExpression>
</sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_gateway3">
...
</bpmndi:BPMNDiagram>
</definitions>
示例代码
public class Activiti7Test11 {
/**
* 流程部署操作
*/
@Test
public void test1(){
// 1.获取ProcessEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2.完成流程的部署操作 需要通过RepositoryService来完成
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3.完成部署操作
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("flow/gateway3.bpmn20.xml")
.name("网关")
.deploy(); // 是一个流程部署的行为 可以部署多个流程定义的
System.out.println(deploy.getId());
System.out.println(deploy.getName());
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 发起一个流程
*/
@Test
public void test3(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 发起流程 需要通过 runtimeService来实现
RuntimeService runtimeService = engine.getRuntimeService();
// 通过流程定义ID来启动流程 返回的是流程实例对象
ProcessInstance processInstance = runtimeService
.startProcessInstanceById("gateway3:1:35003");
System.out.println("processInstance.getId() = " + processInstance.getId());
System.out.println("processInstance.getDeploymentId() = " + processInstance.getDeploymentId());
System.out.println("processInstance.getDescription() = " + processInstance.getDescription());
}
/**
* 任务审批
*/
@Test
public void test4(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
taskService.complete("65002");
}
/**
* 待办任务的 拾取 操作
* 从候选人 --> 处理人
* 一个任务如果被拾取后。其他的候选人就查询不到改任务信息了
*/
@Test
public void test5(){
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = engine.getTaskService();
String group = "销售部"; // 根据当前登录用户查询到的
List<Task> list = taskService.createTaskQuery()
.taskCandidateGroup(group) // 根据组来查询
.list();
if(list != null && list.size() > 0){
for (Task task : list) {
// 张三1 拾取了 这个任务的审批权限 --> 变成了这个任务的审批人
taskService.claim(task.getId(),"张三1");
}
}
}
}
查询历史
查询所有历史流程实例
(查表:act_hi_procinst)
@Test
public void testHistoryProcessInstance() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
HistoryService historyService = engine.getHistoryService();
// 查询所有历史流程实例
List<HistoricProcessInstance> historicProcessInstanceList = historyService.createHistoricProcessInstanceQuery().list();
for (HistoricProcessInstance historicProcessInstance : historicProcessInstanceList) {
log.info("历史流程实例id: {}", historicProcessInstance.getId());
}
}
查询结果
历史流程实例id: 2501
查询指定历史流程实例所对应的任务
(查表:act_hi_taskinst)
@Test
public void testHistoryTaskInstance() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
HistoryService historyService = engine.getHistoryService();
// 查询指定历史流程实例所对应的所有任务
List<HistoricTaskInstance> historicTaskInstanceList = historyService.createHistoricTaskInstanceQuery()
.processInstanceId("2501")
.orderByHistoricTaskInstanceStartTime()
.asc()
.list();
for (HistoricTaskInstance historicTaskInstance : historicTaskInstanceList) {
log.info("历史任务实例id: {}", historicTaskInstance.getId());
log.info("历史任务名: {}", historicTaskInstance.getName());
log.info("任务办理人: {}", historicTaskInstance.getAssignee());
log.info("历史任务开始时间: {}", DateUtils.fmtDate(historicTaskInstance.getStartTime()));
log.info("历史任务结束时间: {}", DateUtils.fmtDate(historicTaskInstance.getEndTime()));
log.info("---------------------------------------------");
}
}
查询结果(可以看到,所有的userTask都查询出来了)
历史任务实例id: 2505
历史任务名: 创建申请单
任务办理人: i1
历史任务开始时间: 2023-11-05 15:05:04
历史任务结束时间: 2023-11-05 15:06:34
---------------------------------------------
历史任务实例id: 7505
历史任务名: 人事审批
任务办理人: i3
历史任务开始时间: 2023-11-05 15:06:34
历史任务结束时间: 2023-11-05 15:13:39
---------------------------------------------
历史任务实例id: 7508
历史任务名: 部门经理审批
任务办理人: i4
历史任务开始时间: 2023-11-05 15:06:34
历史任务结束时间: 2023-11-05 15:12:46
---------------------------------------------
历史任务实例id: 12503
历史任务名: 总经理审批
任务办理人: i5
历史任务开始时间: 2023-11-05 15:13:39
历史任务结束时间:
---------------------------------------------
查询指定历史流程实例所对应的所有活动实例
(查表:act_hi_actinst)
@Test
public void testHistoryActivityInstance() {
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
HistoryService historyService = engine.getHistoryService();
// 查询指定历史流程实例所对应的所有活动实例
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId("2501")
.orderByHistoricActivityInstanceStartTime()
.asc()
.list();
for (HistoricActivityInstance historicActivityInstance : historicActivityInstanceList) {
log.info("历史活动实例id: {}", historicActivityInstance.getId());
log.info("历史活动名: {}", historicActivityInstance.getActivityName());
log.info("历史活动类型: {}", historicActivityInstance.getActivityType());
log.info("任务活动办理人: {}", historicActivityInstance.getAssignee());
log.info("历史活动开始时间: {}", DateUtils.fmtDate(historicActivityInstance.getStartTime()));
log.info("历史活动结束时间: {}", DateUtils.fmtDate(historicActivityInstance.getEndTime()));
log.info("---------------------------------------------");
}
}
查询结果(可以看到,不仅所有的userTask都查出来了,还包括网关节点信息都查出来了)
历史活动实例id: 2503
历史活动名: null
历史活动类型: startEvent
任务活动办理人: null
历史活动开始时间: 2023-11-05 15:05:04
历史活动结束时间: 2023-11-05 15:05:04
---------------------------------------------
历史活动实例id: 2504
历史活动名: 创建申请单
历史活动类型: userTask
任务活动办理人: i1
历史活动开始时间: 2023-11-05 15:05:04
历史活动结束时间: 2023-11-05 15:06:34
---------------------------------------------
历史活动实例id: 7502
历史活动名: null
历史活动类型: inclusiveGateway
任务活动办理人: null
历史活动开始时间: 2023-11-05 15:06:34
历史活动结束时间: 2023-11-05 15:06:34
---------------------------------------------
历史活动实例id: 7504
历史活动名: 人事审批
历史活动类型: userTask
任务活动办理人: i3
历史活动开始时间: 2023-11-05 15:06:34
历史活动结束时间: 2023-11-05 15:13:39
---------------------------------------------
历史活动实例id: 7507
历史活动名: 部门经理审批
历史活动类型: userTask
任务活动办理人: i4
历史活动开始时间: 2023-11-05 15:06:34
历史活动结束时间: 2023-11-05 15:12:46
---------------------------------------------
历史活动实例id: 10001
历史活动名: null
历史活动类型: inclusiveGateway
任务活动办理人: null
历史活动开始时间: 2023-11-05 15:12:46
历史活动结束时间: 2023-11-05 15:13:39
---------------------------------------------
历史活动实例id: 12501
历史活动名: null
历史活动类型: inclusiveGateway
任务活动办理人: null
历史活动开始时间: 2023-11-05 15:13:39
历史活动结束时间: 2023-11-05 15:13:39
---------------------------------------------
历史活动实例id: 12502
历史活动名: 总经理审批
历史活动类型: userTask
任务活动办理人: i5
历史活动开始时间: 2023-11-05 15:13:39
历史活动结束时间:
---------------------------------------------
除此之外还可以查询:historyicDetail、historicVariable
4. 事件网关
事件网关允许根据事件判断流向。网关的每个外出顺序流都要连接到一个中间捕获事件。 当流程到达一个基于事件网关,网关会进入等待状态:会暂停执行。与此同时,会为每个外出顺序流创建相对的事件订阅。
事件网关的外出顺序流和普通顺序流不同,这些顺序流不会真的"执行", 相反它们让流程引擎去决定执行到事件网关的流程需要订阅哪些事件。 要考虑以下条件:
- 事件网关必须有两条或以上外出顺序流;
- 事件网关后,只能使用intermediateCatchEvent类型(activiti不支持基于事件网关后连接ReceiveTask)
- 连接到事件网关的中间捕获事件必须只有一个入口顺序流。
三 、Activiti整合篇
1. 和Spring整合
1.1 添加相关的依赖
activiti-engine它默认就依赖了spring和mybatis的jar依赖
<?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.bobo</groupId>
<artifactId>ActivitiDemo02Spring</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<slf4j.version>1.6.6</slf4j.version>
<log4j.version>1.2.12</log4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>7.0.0.Beta1</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>7.0.0.Beta1</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-model</artifactId>
<version>7.0.0.Beta1</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-converter</artifactId>
<version>7.0.0.Beta1</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-converter</artifactId>
<version>7.0.0.Beta1</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-layout</artifactId>
<version>7.0.0.Beta1</version>
<exclusions>
<exclusion>
<groupId>com.github.jgraph</groupId>
<artifactId>jgraphx</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.activiti.cloud</groupId>
<artifactId>activiti-cloud-services-api</artifactId>
<version>7.0.0.Beta1</version>
</dependency>
<dependency>
<groupId>aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.7.RELEASE</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.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- log end -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>alfresco</id>
<name>Activiti Releases</name>
<url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2 添加整合的配置文件
添加一个Spring的配置文件,并在其中完成Activiti的整合操作
<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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://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?characterEncoding=utf-8&nullCatalogMeansCurrent=true&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="maxActive" value="3"/>
<property name="maxIdle" value="1"/>
</bean>
<!-- 工作流引擎配置bean -->
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 使用spring事务管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<!--
数据库策略
false: 默认值。activiti在启动时,会对比数据库表中保存的版本,如果没有表 或者版本不匹配,将抛出异常。(生产环境常用)
true: activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创 建。(开发时常用)
create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)。(单元测试常用)
drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)。
-->
<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>
<!-- 切面,根据具体项目修改切点配置
<aop:config proxy-target-class="true">
<aop:advisor advice-ref="txAdvice" pointcut="execution(*com.bobo.service.impl..(..))"/>
</aop:config>-->
</beans>
databaseSchemaUpdate的取值注意:
- false: 默认值。activiti在启动时,会对比数据库表中保存的版本,如果没有表或者版本不匹 配,将抛出异常。(生产环境常用)
- true: activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建。(开发时常用)
- create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)。(单元测试常用)
- drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)。
小示例
使用上面的配置文件,使用如下的代码,可快速重置activiti数据库(删除现有的全部数据,保留表结构)
/**
* 重置activiti数据库
*/
@Test
public void test_01() {
ProcessEngine processEngine = ProcessEngineConfiguration.createProcessEngineConfigurationFromResourceDefault()
.setDatabaseSchemaUpdate(ProcessEngineConfigurationImpl.DB_SCHEMA_UPDATE_DROP_CREATE)
.buildProcessEngine();
}
1.3 创建测试类测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:activiti-spring.xml"})
public class ActivitiTest {
@Autowired
private RepositoryService repositoryService;
@Test
public void test01() {
System.out.println(repositoryService);
}
}
通过方法的执行我们能够发现相关的表结构在数据库中完成了创建,说明Activiti和Spring的整合成功。
1.4 入口分析
在学习activiti整合spring的时候,我们可以从ProcessEngines.getDefaultProcessEngine()
这个入口开始。
-
SpringProcessEngineConfiguration需要数据源、事务管理器等,
SpringProcessEngineConfiguration 实际上继承自ProcessEngineConfigurationImpl, 而 ProcessEngineConfigurationImpl 继承自 ProcessEngineConfiguration,
所以相当于是说 SpringProcessEngineConfiguration 使用利用了activiti的ProcessEngineConfigurationImpl基础配置类。ProcessEngineConfigurationImpl里面包含了许多的配置信息,并且还有创建ProcessEngine的逻辑。 -
SpringProcessEngineConfiguration 重写了 ProcessEngineConfigurationImpl 的 buildProcessEngine()方法, 会去调用 init()方法,
来初始化 ProcessEngine 引擎对象需要的各种组件, -
注意到, ProcessEngineFactoryBean 会去调用 SpringProcessEngineConfiguration 的 buildProcessEngine()方法
-
使用ProcessEngines.getDefaultProcessEngine()方法会默认去读取自动读取类路径下的activiti.cfg.xml文件,并且以此文件作为spring的配置文件, 并创建该配置文件中的 SpringProcessEngineConfiguration 配置类对象。
-
所以,在整合springboot的时候,其实只要把SpringProcessEngineConfiguration和ProcessEngineFactoryBean定义到容器中,然后容器中就可以使用ProcessEngine对象了,其它的service也可以利用ProcessEngine对象使用@Bean的方式来暴露出来
2. 和SpringBoot的整合
Activiti7发布正式版本之后,它和SpringBoot2.x已经完全整合开发了
2.1 添加相关的依赖
注意:activiti的启动器强依赖了security,并且引入了spring-boot-starter-security,不太好彻底排除security,比较好的方式就是干脆不用这个启动器了(或者排除SecurityAutoConfiguration和ManagementWebSecurityAutoConfiguration),自己按照和spring整合的方式使用也可以或者之后需要熟悉activiti的源码,自己写启动器。
(activiti-spring-boot-starter的版本不要选太高,否则报错:类文件具有错误的版本55.0,应为52.0。因为依赖的jdk版本高,比如7.0.0.GA版本的就依赖的是jdk11了,下面的7.0.0.Beta2依赖的是jdk8)
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bobo</groupId>
<artifactId>act-springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>act-springboot</name>
<description>Activiti整合SpringBoot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.0.0.Beta2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 修改配置文件
# 配置Spring的数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql:///activiti?characterEncoding=utf-8&nullCatalogMeansCurrent=true&serverTimezone=UTC
spring.datasource.name=root
spring.datasource.password=123456
# activiti的配置
#1.flase:默认值。activiti在启动时,对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出 异常
#2.true: activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建
#3.create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)
#4.drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)
spring.activiti.database-schema-update=true
# 检测历史表是否存在, Activiti7中默认是没有开启数据库历史记录的,启动数据库历史记录
spring.activiti.db-history-used=true
#记录历史等级 可配置的历史级别有none, activity, audit, full
#none:不保存任何的历史数据,因此,在流程执行过程中,这是最高效的。#activity:级别高于none,保存流程实例与流程行为,其他数据不保存。
#audit:除activity级别会保存的数据外,还会保存全部的流程任务及其属性。audit为history的默认 值。
#full:保存历史数据的最高级别,除了会保存audit级别的数据外,还会保存其他全部流程相关的细节数 据,包括一些流程参数等。
spring.activiti.history-level=full
# 校验流程文件,默认校验resouces下的 process 文件夹里的流程文件
spring.activiti.check-process-definitions=false
spring.activiti.process-definition-location-prefix=classpath:processes/
2.3 整合SpringSecurity
因为Activiti7和SpringBoot整合后,默认情况下,集成了SpringSecurity安全框架,这样我们就要准备SpringSecurity的相关配置信息
添加一个SpringSecurity的工具类,下面这个类做的其实就是根据传入用户名,然后从已配置的用户中找到认证主体对象,然后绑定到当前线程中哦~
@Component
public class SecurityUtil {
private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
public void logInAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (user == null) {
throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
}
logger.info("> Logged in as: " + username);
SecurityContextHolder.setContext(new SecurityContextImpl(
new Authentication() {
@Override
public Collection<? extends GrantedAuthority>
getAuthorities() {
return user.getAuthorities();
}
@Override
public Object getCredentials() {
return user.getPassword();
}
@Override
public Object getDetails() {
return user;
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return user.getUsername();
}
}));
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
}
}
这个类可以从Activiti7官方提供的Example中找到。
添加一个SpringSecurity的配置文件
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
public class SpringSecurityConfiguration {
private Logger logger = LoggerFactory.getLogger(SpringSecurityConfiguration.class);
@Bean
public UserDetailsService myUserDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
//这里添加用户,后面处理流程时用到的任务负责人,需要添加在这里
String[][] usersGroupsAndRoles = {
{
"jack", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"
},
{
"rose", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"
},
{
"tom", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"
},
{
"other", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"
},
{
"system", "password", "ROLE_ACTIVITI_USER"
},
{
"admin", "password", "ROLE_ACTIVITI_ADMIN"
},
};
for (String[] user : usersGroupsAndRoles) {
List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user, 2, user.length));
logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]),
authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
}
return inMemoryUserDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.4 创建bpmn文件
创建一个简单的bpmn文件,并设置任务的用户组,CandidateGroups,并且CandidateGroups中的内容 要与在SpringSecurity的配置文件中配置的用户组的名称要保持一致,可以填写activitTeam或者otherTeam。这样填写的好处是,当不确定到底由谁来负责当前的任务的时候,只要是Groups内的用户 都可以拾取这个任务
Activiti7中可以自动部署流程,前提是在resources目录下,创建一个新的目录processes,用来放置bpmn文件(当项目启动时,会自动扫描指定路径(spring.activiti.process-definition-location-prefix=classpath:processes/)下的bpmn文件,并部署这些文件到activiti中)
2.5 单元测试
注意:下面这种使用ProcessRuntime和TaskRuntime都是需要spring-security的支持才能调用的,感觉还是不要用这种,直接用activiti原生的比较好。不过,也可以进去里面看看官方是怎么使用原生的
import com.bobo.utils.SecurityUtil;
import org.activiti.api.process.model.ProcessDefinition;
import org.activiti.api.process.model.ProcessInstance;
import org.activiti.api.process.model.builders.ProcessPayloadBuilder;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.activiti.api.task.model.Task;
import org.activiti.api.task.model.builders.ClaimTaskPayloadBuilder;
import org.activiti.api.task.model.builders.TaskPayloadBuilder;
import org.activiti.api.task.model.payloads.ClaimTaskPayload;
import org.activiti.api.task.runtime.TaskRuntime;
import org.activiti.engine.RepositoryService;
import org.activiti.runtime.api.ProcessRuntime;
import org.activiti.runtime.api.model.ProcessInstance;
import org.activiti.runtime.api.model.builders.ProcessPayloadBuilder;
import org.activiti.runtime.api.query.Pageable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ActSpringbootApplicationTests {
@Autowired
private ProcessRuntime processRuntime;
@Autowired
private TaskRuntime taskRuntime;
@Autowired
private SecurityUtil securityUtil;
@Autowired
private RepositoryService repositoryService;
@Test
void contextLoads() {
System.out.println(taskRuntime);
}
/**
* 查询流程的定义
*/
@Test
public void test02() {
// 不作此登录,后面会报错!
securityUtil.logInAs("system");
Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(0, 10));
System.out.println("可用的流程定义数量:" + processDefinitionPage.getTotalItems());
for (ProcessDefinition processDefinition : processDefinitionPage.getContent()) {
System.out.println("流程定义:" + processDefinition);
}
}
/**
* 部署流程(如果自动部署没有生效,那就手动部署它吧)
*/
@Test
public void test03() {
repositoryService.createDeployment()
.addClasspathResource("processes/my-evection.bpmn")
.addClasspathResource("processes/my-evection.png")
.name("出差申请单")
.deploy();
}
/**
* 启动流程实例(启动流程实例之后,在act_ru_task表中生成了1个任务,但这个任务的assignee此时为null)
* (注意下面的使用会默认去拿security中当前用户身份)
*/
@Test
public void test04() {
securityUtil.logInAs("system");
ProcessInstance processInstance =
processRuntime.start(ProcessPayloadBuilder
.start()
.withProcessDefinitionKey("my-evection")
.build()
);
System.out.println("流程实例id:" + processInstance.getId());
}
/**
* 任务查询、拾取及完成操作(注意下面的使用会默认去拿security中当前用户身份)
*/
@Test
public void test05() {
securityUtil.logInAs("jack");
Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
if (tasks != null && tasks.getTotalItems() > 0) {
for (Task task : tasks.getContent()) {
// 拾取任务
taskRuntime.claim(TaskPayloadBuilder
.claim()
.withTaskId(task.getId())
.build()
);
System.out.println(" 任 务 :" + task);
taskRuntime.complete(TaskPayloadBuilder
.complete()
.withTaskId(task.getId())
.build()
);
}
}
Page<Task> taskPage2 = taskRuntime.tasks(Pageable.of(0, 10));
if (taskPage2.getTotalItems() > 0) {
System.out.println("任务:" + taskPage2.getContent());
}
}
}