Liferay7 BPM门户开发之11: Activiti工作流程开发的一些统一规则和实现原理(完整版)
注意:以下规则是我为了规范流程的处理过程,不是Activiti公司的官方规定。
1、流程启动需要设置启动者,在Demo程序中,“启动者变量”名统一设置为initUserId
启动时要做的:
identityService.setAuthenticatedUserId(initUserId);
processInstance = runtimeService.startProcessInstanceByKey(流程ID, 业务Key, 变量map); or
startProcessInstanceById(String processDefinitionId, String businessKey, Map variables) 变量map定义的方法:
Map<String ,Object > variables = new HashMap<>();
variables.put("initUserId","wangxin");
variables.put("leaveReason","想休假了");
2、使用el表达式来做流程的动态属性或方法定义
比如完成一个“请假销假”的任务,需要流程发起者销假,销假环节就能找到正确的签收者(activiti:assignee)了:
<startevent id="startevent1" name="Start" activiti:initiator="initUserId"></startevent>
<usertask id="reportBack" name="销假" activiti:assignee="${initUserId}"></usertask>
3、“业务键”定义规则
业务键 = 流程ID + 实体实例ID;
businessKey = procDefId + "." + objId
4、根据“业务键”查询流程实例(反查)
在流程启动的时候,我们已经定义了业务Key,那么只需要反查,即可得到流程实例
//根据业务键获取流程实例
public ProcessInstance getProInstByBusinessKey(String businessKey) {
return runtimeService.createProcessInstanceQuery().processInstanceBusinessKey("LeaveBill.1").singleResult();
} //根据业务键获取任务
public List<Task> getTasksByBusinessKey(String businessKey) {
return taskService.createTaskQuery().processInstanceBusinessKey("LeaveBill.1").list();
}
5、通过流程实例ID获取“业务键”
//1、通过任务对象获取流程实例
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult(); //2、通过流程实例获取“业务键”
String businessKey = pi.getBusinessKey();
6、取得当前活动节点
String processInstanceId="1401";
// 通过流程实例ID查询流程实例
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
if(pi!=null){
System.out.println("当前流程节点在:" + pi.getActivityId());
}else{
System.out.println("流程已结束!!");
}
7、查询某人的“候选公共任务”,用于实现“抢签”
“候选公共任务”的认领者即属于一堆候选人其中一个,比如财务审批可以由张三、李四、王五审批,谁批都可以,手快者先认领就是签收者。
这个查询就是把符合条件的候选者的任务查出来,一般可以和“个人任务”合并一起放在“待办任务”菜单里。
也针对于把Task分配给一个角色时,例如部门领导,因为部门领导角色可以指定多个人所以需要先签收再办理,特点:抢占式。
// 创建任务查询对象
TaskQuery taskQuery = taskService.createTaskQuery();
// 配置查询对象
String candidateUser="张三";
taskQuery
// 过滤条件
.taskCandidateUser(candidateUser)
// 排序条件
.orderByTaskCreateTime().desc();
// 执行查询
List<Task> tasks = taskQuery.list();
System.out.println("======================【"+candidateUser+"】的候选公共任务列表=================");
for (Task task : tasks) {
System.out.print("id:"+task.getId()+",");
System.out.print("name:"+task.getName()+",");
System.out.print("createTime:"+task.getCreateTime()+",");
System.out.println("assignee:"+task.getAssignee());
}
8、查询某人的“个人任务”,即签收者(assignee)被明确指定。
比如销假人被变量明确指定了:
<usertask id="reportBack" name="销假" activiti:assignee="${initUserId}"></usertask>
// 创建任务查询对象
TaskQuery taskQuery = taskService.createTaskQuery();
// 配置查询对象
// String assignee="user";
String assignee="李四";
taskQuery
// 过滤条件
.taskAssignee(assignee)
// 分页条件
// .listPage(firstResult, maxResults)
// 排序条件
.orderByTaskCreateTime().desc();
// 执行查询
List<Task> tasks = taskQuery.list();
System.out.println("======================【"+assignee+"】的代办任务列表=================");
for (Task task : tasks) {
System.out.print("id:"+task.getId()+",");
System.out.print("name:"+task.getName()+",");
System.out.print("createTime:"+task.getCreateTime()+",");
System.out.println("assignee:"+task.getAssignee());
}
9、任务认领,通过认领,把“候选公共任务”变成指定用户的“个人任务”
// claim 认领
String taskId="1404";
String userId="李四";
// 让指定userId的用户认领指定taskId的任务
taskService.claim(taskId, userId);
10、结合Form表单提交(办理)任务
String formId = request.getParameter("formId");
String procInstId = request.getParameter("procInstId"); //流程实例ID Map<String, String[]> flowData = new HashMap<String, String[]>(); //将表单提交数据注入表单变量
flowData = request.getParameterMap(); formHelper.submitTaskFormData(request.getParameter("taskId"), flowData); // 完成任务
taskService.complete(taskId );
11、任务动态分配定制处理,比如寻找“某人的直属领导”
Activiti的签收人中只有候选人、候选组、分配人的概念,如果要实现更业务相关的签收逻辑,需要扩展监听器
比如MyLeaderHandler,即扩展实现了TaskListener接口:
<userTask id="task1" name="My task" >
<extensionElements>
<activiti:taskListener event="create" class="org.activiti.MyLeaderHandler" />
</extensionElements>
</userTask>
//动态实现任务分配
public class MyLeaderHandler implements TaskListener { public void notify(DelegateTask delegateTask) { LeaderService ls =....
String userLeader = ls.findLeaderbyUserId(XXXXXXX);
delegateTask.setAssignee(userLeader);
delegateTask.addCandidateUser(XXX);
delegateTask.addCandidateGroup(XXXX);
...
}
}
还有一种更方便的方法,即通过el表达式:
可以使用表达式把任务监听器设置为spring代理的bean, 让这个监听器监听任务的创建事件。
下面的例子中,执行者会通过调用ldapService这个spring bean的findManagerOfEmployee方法获得。
流程变量emp会作为参数传递给bean。
<userTask id="task" name="My Task" activiti:assignee="${ldapService.findManagerForEmployee(emp)}"/>
也可以用来设置候选人和候选组:
<userTask id="task" name="My Task" activiti:candidateUsers="${ldapService.findAllSales()}"/>
ps:注意方法返回类型只能为String或Collection<String> (对应候选人和候选组):
public class FakeLdapService { public String findManagerForEmployee(String employee) {
return "Kermit";
} public List<String> findAllSales() {
return Arrays.asList("kermit", "gonzo", "fozzie");
}
}
12、会签任务,即多实例
例如,一个任务必须所有领导都通过了才往下走。
activiti其实已经非常优雅的实现了,网上有一些繁琐的实现,其实完全没有必要,比如下面:
http://jee-soft.cn/htsite/html/fzyyj/jsyj/2012/08/08/1344421504026.html
正确的打开方式是通过在Task节点增加multiInstanceCharacteristics节点,设置 collection和 elementVariable属性
例子:
可以指定一个(判断完成)表达式,只有true的情况下全部实例完成,流程继续往下走。
如果表达式返回true,所有其他的实例都会销毁,多实例节点也会结束。 这个表达式必须定义在completionCondition子元素中。
<userTask id="miTasks" name="My Task" activiti:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false"
activiti:collection="assigneeList" activiti:elementVariable="assignee" >
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
这里例子中,会为assigneeList集合的每个元素创建一个并行的实例。 当60%的任务完成时,其他任务就会删除,流程继续执行。
会签环节中涉及的几个默认的自带流程变量:
- 1. nrOfInstances 该会签环节中总共有多少个实例
- 2. nrOfActiveInstances 当前活动的实例的数量,即还没有 完成的实例数量。
- 3. nrOfCompletedInstances 已经完成的实例的数量
实现会签人员分配
public class AssgineeMultiInstancePer implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) throws Exception {
System.out.println("设置会签环节的人员.");
execution.setVariable("pers", Arrays.asList("张三", "李四", "王五", "赵六"));
}
}
设置完成会签条件:
public class MulitiInstanceCompleteTask implements Serializable {
private static final long serialVersionUID = 1L;
public boolean completeTask(DelegateExecution execution) {
System.out.println("总的会签任务数量:" + execution.getVariable("nrOfInstances") + "当前获取的会签任务数量:" + execution.getVariable("nrOfActiveInstances") + " - " + "已经完成的会签任务数量:" + execution.getVariable("nrOfCompletedInstances"));
System.out.println("I am invoked.");
return false;
}
}
官方网站专门有一节(8.5.14. Multi-instance)详细介绍:
http://www.activiti.org/userguide/index.html#bpmnMultiInstance
也可以看:
http://itindex.net/detail/54540-activiti
https://my.oschina.net/winHerson/blog/139267
http://itindex.net/detail/51509-activiti-%E6%8C%82%E8%B5%B7-%E6%A0%B8%E5%BF%83
http://blog.csdn.net/chq1988/article/details/41513451
https://github.com/bluejoe2008/openwebflow
13、加签、减签任务,非常符合中国式流程
Activiti其实已经非常优雅的实现了,网上有一些繁琐的实现,其实完全没有必要
实际上一行代码就能实现:
//人员加签: runtimeService.addUserIdentityLink(String processInstanceId, String userId, String identityLinkType) //还可以这样实现,其实是addUserIdentityLink的一个继承,只是类型是IdentityLinkType.CANDIDATE,即参与者
runtimeService.addParticipantUser(String processInstanceId, String userId) //组加签: runtimeService.addParticipantGroup(String processInstanceId, String groupId) runtimeService.deleteParticipantUser(String processInstanceId, String userId) //人员、组减签: runtimeService.deleteParticipantUser(String processInstanceId, String userId) runtimeService.deleteParticipantGroup(String processInstanceId, String groupId)
值得注意的是,认证链类型IdentityLinkType是枚举,有5个:
- ASSIGNEE
- CANDIDATE
- OWNER
- PARTICIPANT
- STARTER
使用这里的ASSIGNEE和OWNER还可以有更加丰富的想象力。
14、实现自定义的用户和组织
http://www.verydemo.com/demo_c128_i11037.html
https://my.oschina.net/winHerson/blog/118172
http://rongjih.blog.163.com/blog/static/335744612012631112640968/
http://www.kafeitu.me/activiti/2012/04/23/synchronize-or-redesign-user-and-role-for-activiti.html
15、实现自定义表单
这个想法挺好:
http://blog.csdn.net/tuzongxun/article/details/51093881
但距离实用还差很远,实现产品化也是实现代价非常高,还是使用jsp + bootstrip作为前端最实际,也更容易扩展
笔者正好也做过动态自定义表单:
昕友silverlight表单设计器的使用 (原创 Form Designer)
积累过一些技术和思想。
16、外置表单
在实际的开发中,实际上其自身的内置表单无任何意义,而Activiti还不具备所见即所得的自动表单设计器(毕竟是开源产品),但另一方面这也带来了一个好处:即可以随心所欲的设计表单前端,可以满足任务的可控性和可变性,非常强大,而不是限制在厂商的表单技术里动弹不得,外置表单可以用到的技术:JSP、Spring MVC视图、Struct视图。
Activiti提供的集成方式异常简单而优雅,只需要注意4点:
- 1、在BPMN文件的任务节点设置FormKey属性,即<userTask activiti:formKey="XXX">...,这里XXX可以是Jsp页面,也可以是Struct Action,也可以是Spring Controller名,以便对应表单ID,真的方便的想哭了;
- 2、在流程运行时,根据TaskId获取FormKey:
StartFormData FormService.getStartFormData(String processDefinitionId)
//Or
TaskFormdata FormService.getTaskFormData(String taskId)
String formKey = formData.getFormKey()
... - 3、然后在Spring 控制器里重定向传递:XXXX?fromKey=XXX
- 4、Form数据提交:
Map map=request.getParameterMap();
Set set=map.keySet();//所有参数名的set集
Iterator it=set.iterator();
while(it.hasNext()){
String s=(String)it.next();//枚举出参数
String values[]=request.getParameterValues(s);//取得对应的参数值返回的数组
然后干你要干的操作,比如
for(int i=;i<values.length;i++){
out.println(values[i]);
}
} 提交:
ProcessInstance FormService.submitStartFormData(String processDefinitionId, Map<String,String> properties)
void FormService.submitStartFormData(String taskId, Map<String,String> properties)
17、表单属性和表单变量的关联关系
实际上在业务开发中可以不需要用到表单属性,只用表单变量即可,需要注意表单变量的作用域,有两个
- 流程实例内全局可用
- 仅任务内可用
在Activit内部,定义了流程属性,会自动增加流程变量,也可以手动设置关联, 手动流程属性和变量的关联关系举例:
1、属性speaker 和 变量SpeakerName 相互关联
<activiti:formProperty id="speaker" name="Speaker" variable="SpeakerName" type="string" />
2、和bean关联
<activiti:formProperty id="street" expression="#{address.street}" required="true" />
form property的5种类型:
string
(org.activiti.engine.impl.form.StringFormTypelong
(org.activiti.engine.impl.form.LongFormType)enum
(org.activiti.engine.impl.form.EnumFormType)date
(org.activiti.engine.impl.form.DateFormType)boolean
(org.activiti.engine.impl.form.BooleanFormType)
获取表单属性的办法:
List<FormProperty> formService.getStartFormData(String processDefinitionId).getFormProperties()
List<FormProperty> formService.getTaskFormData(String taskId).getFormProperties()
FormProperty实际是一个接口,容许去你去自由扩展,具体定义:
public interface FormProperty {
/** the key used to submit the property in {@link FormService#submitStartFormData(String, java.util.Map)}
* or {@link FormService#submitTaskFormData(String, java.util.Map)} */
String getId();
/** the display label */
String getName();
/** one of the types defined in this interface like e.g. {@link #TYPE_STRING} */
FormType getType();
/** optional value that should be used to display in this property */
String getValue();
/** is this property read to be displayed in the form and made accessible with the methods
* {@link FormService#getStartFormData(String)} and {@link FormService#getTaskFormData(String)}. */
boolean isReadable();
/** is this property expected when a user submits the form? */
boolean isWritable();
/** is this property a required input field */
boolean isRequired();
}
获取表单属性的名称:
formProperty.getType().getName()
获取表单属性的值:
formProperty.getType().getValue()
获取某一属性,比如XML定义:
<activiti:formProperty id="start" type="date" datePattern="dd-MMM-yyyy" />
formProperty.getType().getInformation("datePattern")
获取枚举值:
formProperty.getType().getInformation("values")
18、自定义“待办列表”的一个方法
Activit内置的待办任务查询类似:
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee("kermit")
.processVariableValueEquals("orderId", "")
.orderByDueDate().asc()
.list();
如果要直接SQL查询,可以这样:
List<Task> tasks = taskService.createNativeTaskQuery()
.sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T WHERE T.NAME_ = #{taskName}")
.parameter("taskName", "gonzoTask")
.list(); long count = taskService.createNativeTaskQuery()
.sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T1, "
+ managementService.getTableName(VariableInstanceEntity.class) + " V1 WHERE V1.TASK_ID_ = T1.ID_")
.count();
19、深入理解流程变量
首先需要明确一些容易混淆的概念。
"execution是什么?"
就是流程实例的当前执行节点;
"execution node和process instance的区别是什么?"
执行节点和流程实例的关系非常密切,可以想象一个树形结构,流程实例(process instance)是主干,由无数的执行树枝(execution)组成的;
一个重要的公式:process instance id = the root execution id
但一旦流程不是一条主干线一直到结束,而是分裂为多个分支了,那么就有多个execution id,可以看成树枝。
"activity、task、job的区别是什么?"
- Activity = 这个是模型定义BPMN的一个基本元素(注意这个不是运行时的概念),比如Start、user task、连接线......
- task = 这个是运行时的概念,就是任务的意思,比如用户任务、服务任务......
- job = 一个定时器任务
设置流程变量(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);
读取流程变量的方法:
//读取变量(基于TaskService)
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); //读取变量(基于execution 执行分支对象)
execution.getVariables();
execution.getVariables(Collection<String> variableNames);
execution.getVariable(String variableName);
execution.setVariables(Map<String, object> variables);
execution.setVariable(String variableName, Object value);
Activiti5.17版本之后的新API
由于历史版本原因,在执行任何上述方法的时候,activiti默认是将所有的变量从数据库取出来,意味着数据库存有10个变量,现在你需要取出名叫myVariable的变量,但是其余的9个也会被取出来并且缓存起来。这并不是很差,因为后期你取变量就不会再从数据库取出,当然如果你有大量的变量或者在查询方面你想进一步控制数据库,这时全部取出就不怎么合适了。从Activiti5.17版本开始,新添加了的方法支持是否全部查询输出到缓存:如果是true则全部抓取
Map<String, Object> getVariables(Collection<String> variableNames, boolean fetchAllVariables);
Object getVariable(String variableName, boolean fetchAllVariables);
void setVariable(String variableName, Object value, boolean fetchAllVariables);
Liferay7 BPM门户开发之11: Activiti工作流程开发的一些统一规则和实现原理(完整版)的更多相关文章
- A011 Activiti工作流程开发的一些统一规则和实现原理(完整版)
注意:以下规则是我为了规范流程的处理过程,不是Activiti公司的官方规定. 1.流程启动需要设置启动者,在Demo程序中,“启动者变量”名统一设置为initUserId 启动时要做的: ident ...
- Liferay7 BPM门户开发之8: Activiti实用问题集合
1.如何实现审核的上级获取(任务逐级审批) 这个是必备功能,通过Spring的注入+Activiti表达式可以很容易解决. 可参考: http://blog.csdn.net/sunxing007/a ...
- Liferay7 BPM门户开发之7: Activiti中的重要概念和主要数据库结构
流程的人员参与角色: Assignee :签收者(即待办人) Candidate:候选人 Owner:拥有者 Starter:启动者 participant:参与者,包含查阅 流程变量的类型: Str ...
- Liferay7 BPM门户开发之6: Activiti数据库换为mysql
第一步: 在mysql中创建数据库名字叫 'activiti' 执行D:\activiti-5.21.0\database\create下的脚本 第二步: 打开=> apache-tomcat/ ...
- Liferay7 BPM门户开发之5: Activiti和Spring集成
参考文档: https://github.com/jbarrez/spring-boot-with-activiti-examplehttps://github.com/sxyx2008/spring ...
- Liferay7 BPM门户开发之4: Activiti事件处理和监听Event handlers
事件机制从Activiti 5.15开始引入,这非常棒,他可以让你实现委托. 可以通过配置添加事件监听器,也可以通过Runtime API加入注册事件. 所有的事件参数子类型都来自org.activi ...
- Liferay7 BPM门户开发之3: Activiti开发环境搭建
下载地址: http://activiti.org/download.html 源码: https://github.com/Activiti/Activiti 环境准备(检查项): JDK 1.7 ...
- Liferay7 BPM门户开发之10: 通用流程实现从Servlet到Portlet(Part1)
开发目的: 实现通用流程自动化处理(即实现不需要hardcode代码的bpm统一处理后台,仅需要写少量前端html form代码和拖拽设计BPM定义) 既可独立运行或可依托于Liferay或依托其它门 ...
- Liferay7 BPM门户开发之17: Portlet 生命周期
Portlet 生命周期 init() =〉 render() =〉 processAction() =〉 processEvent() =〉 serveResource() =〉destroy() ...
随机推荐
- springcloud-知识点总结(一):Eureka
1.Spring Cloud简介 Spring Cloud是一系列框架的有序集合.它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册.配置中心.消息总线.负载 ...
- 解决xcode10打包报错:That command depends on command in Target ‘xxx’:scrpit phase"[CP] Copy Pods Resources"
问题:使用xcode10打包报错,提示 error:Multiple commands produce ‘xxxx/xxx.app’ 1)Target ‘xx’ has create director ...
- jsp中<c:if>标签的用法
<c:if test="${(tbl.column1 eq '值') and (tbl.column2 eq 'str')}"> <table>...< ...
- [leetcode]200. Number of Islands岛屿个数
Given a 2d grid map of '1's (land) and '0's (water), count the number of islands. An island is surro ...
- mysql解压缩版安装方法以及mysql无法启动1067错误
https://jingyan.baidu.com/article/f3ad7d0ffc061a09c3345bf0.html我下载的版本号是5.6.421.解压到C:\Program Files\M ...
- Android开发之页面跳转传递list集合
这篇随笔这里详细记录两个activity之间如何传递list集合中的数据. 1.首先要对javabean进行序列化处理,即实现Serializable. package com.anhua.bean; ...
- Connection lost: The server closed the connection
想必很多初学者都会遇到这个问题 其实很简单.mysql有个机制,就是8小时无通信,myslq就会自动关闭数据; 解决方案(2选1): 或者: 1.定时去做一个查询,就是 select * from X ...
- redis安全删key脚本(模糊匹配,长list,大set等)
两种情况: 1.删除指定前缀开头的rediskey ,扫描和删除过程中对线上无感知 2.删除一个大的list,set,zset,hash,这种得分批次减少大小,一直缩到0再删 第一种情况:只要知道线上 ...
- mysql自动删除90天前数据
#coding:utf-8import MySQLdb #方法1直接在Navicat中添加计划任务#DELETE FROM message2 where SEND_TIME < UNIX_TIM ...
- leveldb 学习记录(四)Log文件
前文记录 leveldb 学习记录(一) skiplistleveldb 学习记录(二) Sliceleveldb 学习记录(三) MemTable 与 Immutable Memtablelevel ...